Skip to content

Commit 4aa1fe7

Browse files
committed
Add support for HTTP operations on CoreCLR
Closes linked-data-dotnet#21 Closes linked-data-dotnet#12
1 parent be00189 commit 4aa1fe7

File tree

5 files changed

+99
-209
lines changed

5 files changed

+99
-209
lines changed

Diff for: src/json-ld.net/Core/DocumentLoader.cs

+55-175
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,73 @@
66
using JsonLD.Util;
77
using System.Net;
88
using System.Collections.Generic;
9+
using System.Net.Http;
10+
using System.Net.Http.Headers;
11+
using System.Threading.Tasks;
12+
using System.Runtime.InteropServices;
913

1014
namespace JsonLD.Core
1115
{
1216
public class DocumentLoader
1317
{
18+
const int MAX_REDIRECTS = 20;
19+
20+
/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
21+
/// <remarks>An HTTP Accept header that prefers JSONLD.</remarks>
22+
public const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1";
23+
24+
/// <exception cref="JsonLDNet.Core.JsonLdError"></exception>
25+
public RemoteDocument LoadDocument(string url)
26+
{
27+
return LoadDocumentAsync(url).GetAwaiter().GetResult();
28+
}
29+
1430
/// <exception cref="JsonLDNet.Core.JsonLdError"></exception>
15-
public virtual RemoteDocument LoadDocument(string url)
31+
public async Task<RemoteDocument> LoadDocumentAsync(string url)
1632
{
17-
#if !PORTABLE && !IS_CORECLR
1833
RemoteDocument doc = new RemoteDocument(url, null);
19-
HttpWebResponse resp;
2034

2135
try
2236
{
23-
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url);
24-
req.Accept = AcceptHeader;
25-
resp = (HttpWebResponse)req.GetResponse();
26-
bool isJsonld = resp.Headers[HttpResponseHeader.ContentType] == "application/ld+json";
27-
if (!resp.Headers[HttpResponseHeader.ContentType].Contains("json"))
37+
HttpResponseMessage httpResponseMessage;
38+
39+
int redirects = 0;
40+
int code;
41+
string redirectedUrl = url;
42+
43+
// Manually follow redirects because .NET Core refuses to auto-follow HTTPS->HTTP redirects.
44+
do
45+
{
46+
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, redirectedUrl);
47+
httpRequestMessage.Headers.Add("Accept", AcceptHeader);
48+
httpResponseMessage = await JSONUtils._HttpClient.SendAsync(httpRequestMessage);
49+
if (httpResponseMessage.Headers.TryGetValues("Location", out var location))
50+
{
51+
redirectedUrl = location.First();
52+
}
53+
54+
code = (int)httpResponseMessage.StatusCode;
55+
} while (redirects++ < MAX_REDIRECTS && code >= 300 && code < 400);
56+
57+
if (redirects >= MAX_REDIRECTS)
58+
{
59+
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"Too many redirects - {url}");
60+
}
61+
62+
if (code >= 400)
63+
{
64+
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"HTTP {code} {url}");
65+
}
66+
67+
bool isJsonld = httpResponseMessage.Content.Headers.ContentType.MediaType == "application/ld+json";
68+
69+
// From RFC 6839, it looks like we should accept application/json and any MediaType ending in "+json".
70+
if (httpResponseMessage.Content.Headers.ContentType.MediaType != "application/json" && !httpResponseMessage.Content.Headers.ContentType.MediaType.EndsWith("+json"))
2871
{
2972
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url);
3073
}
3174

32-
string[] linkHeaders = resp.Headers.GetValues("Link");
33-
if (!isJsonld && linkHeaders != null)
75+
if (!isJsonld && httpResponseMessage.Headers.TryGetValues("Link", out var linkHeaders))
3476
{
3577
linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray()))
3678
.Select(h => h.Trim()).ToArray();
@@ -41,189 +83,27 @@ public virtual RemoteDocument LoadDocument(string url)
4183
}
4284
string header = linkedContexts.First();
4385
string linkedUrl = header.Substring(1, header.IndexOf(">") - 1);
44-
string resolvedUrl = URL.Resolve(url, linkedUrl);
86+
string resolvedUrl = URL.Resolve(redirectedUrl, linkedUrl);
4587
var remoteContext = this.LoadDocument(resolvedUrl);
4688
doc.contextUrl = remoteContext.documentUrl;
4789
doc.context = remoteContext.document;
4890
}
4991

50-
Stream stream = resp.GetResponseStream();
92+
Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync();
5193

52-
doc.DocumentUrl = req.Address.ToString();
94+
doc.DocumentUrl = redirectedUrl;
5395
doc.Document = JSONUtils.FromInputStream(stream);
5496
}
5597
catch (JsonLdError)
5698
{
5799
throw;
58100
}
59-
catch (WebException webException)
60-
{
61-
try
62-
{
63-
resp = (HttpWebResponse)webException.Response;
64-
int baseStatusCode = (int)(Math.Floor((double)resp.StatusCode / 100)) * 100;
65-
if (baseStatusCode == 300)
66-
{
67-
string location = resp.Headers[HttpResponseHeader.Location];
68-
if (!string.IsNullOrWhiteSpace(location))
69-
{
70-
// TODO: Add recursion break or simply switch to HttpClient so we don't have to recurse on HTTP redirects.
71-
return LoadDocument(location);
72-
}
73-
}
74-
}
75-
catch (Exception innerException)
76-
{
77-
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, innerException);
78-
}
79-
80-
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, webException);
81-
}
82101
catch (Exception exception)
83102
{
84103
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, exception);
85104
}
86105
return doc;
87-
#else
88-
throw new PlatformNotSupportedException();
89-
#endif
90106
}
91107

92-
/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
93-
/// <remarks>An HTTP Accept header that prefers JSONLD.</remarks>
94-
public const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1";
95-
96-
// private static volatile IHttpClient httpClient;
97-
98-
// /// <summary>
99-
// /// Returns a Map, List, or String containing the contents of the JSON
100-
// /// resource resolved from the URL.
101-
// /// </summary>
102-
// /// <remarks>
103-
// /// Returns a Map, List, or String containing the contents of the JSON
104-
// /// resource resolved from the URL.
105-
// /// </remarks>
106-
// /// <param name="url">The URL to resolve</param>
107-
// /// <returns>
108-
// /// The Map, List, or String that represent the JSON resource
109-
// /// resolved from the URL
110-
// /// </returns>
111-
// /// <exception cref="Com.Fasterxml.Jackson.Core.JsonParseException">If the JSON was not valid.
112-
// /// </exception>
113-
// /// <exception cref="System.IO.IOException">If there was an error resolving the resource.
114-
// /// </exception>
115-
// public static object FromURL(URL url)
116-
// {
117-
// MappingJsonFactory jsonFactory = new MappingJsonFactory();
118-
// InputStream @in = OpenStreamFromURL(url);
119-
// try
120-
// {
121-
// JsonParser parser = jsonFactory.CreateParser(@in);
122-
// try
123-
// {
124-
// JsonToken token = parser.NextToken();
125-
// Type type;
126-
// if (token == JsonToken.StartObject)
127-
// {
128-
// type = typeof(IDictionary);
129-
// }
130-
// else
131-
// {
132-
// if (token == JsonToken.StartArray)
133-
// {
134-
// type = typeof(IList);
135-
// }
136-
// else
137-
// {
138-
// type = typeof(string);
139-
// }
140-
// }
141-
// return parser.ReadValueAs(type);
142-
// }
143-
// finally
144-
// {
145-
// parser.Close();
146-
// }
147-
// }
148-
// finally
149-
// {
150-
// @in.Close();
151-
// }
152-
// }
153-
154-
// /// <summary>
155-
// /// Opens an
156-
// /// <see cref="Java.IO.InputStream">Java.IO.InputStream</see>
157-
// /// for the given
158-
// /// <see cref="Java.Net.URL">Java.Net.URL</see>
159-
// /// , including support
160-
// /// for http and https URLs that are requested using Content Negotiation with
161-
// /// application/ld+json as the preferred content type.
162-
// /// </summary>
163-
// /// <param name="url">The URL identifying the source.</param>
164-
// /// <returns>An InputStream containing the contents of the source.</returns>
165-
// /// <exception cref="System.IO.IOException">If there was an error resolving the URL.</exception>
166-
// public static InputStream OpenStreamFromURL(URL url)
167-
// {
168-
// string protocol = url.GetProtocol();
169-
// if (!JsonLDNet.Shims.EqualsIgnoreCase(protocol, "http") && !JsonLDNet.Shims.EqualsIgnoreCase
170-
// (protocol, "https"))
171-
// {
172-
// // Can't use the HTTP client for those!
173-
// // Fallback to Java's built-in URL handler. No need for
174-
// // Accept headers as it's likely to be file: or jar:
175-
// return url.OpenStream();
176-
// }
177-
// IHttpUriRequest request = new HttpGet(url.ToExternalForm());
178-
// // We prefer application/ld+json, but fallback to application/json
179-
// // or whatever is available
180-
// request.AddHeader("Accept", AcceptHeader);
181-
// IHttpResponse response = GetHttpClient().Execute(request);
182-
// int status = response.GetStatusLine().GetStatusCode();
183-
// if (status != 200 && status != 203)
184-
// {
185-
// throw new IOException("Can't retrieve " + url + ", status code: " + status);
186-
// }
187-
// return response.GetEntity().GetContent();
188-
// }
189-
190-
// public static IHttpClient GetHttpClient()
191-
// {
192-
// IHttpClient result = httpClient;
193-
// if (result == null)
194-
// {
195-
// lock (typeof(JSONUtils))
196-
// {
197-
// result = httpClient;
198-
// if (result == null)
199-
// {
200-
// // Uses Apache SystemDefaultHttpClient rather than
201-
// // DefaultHttpClient, thus the normal proxy settings for the
202-
// // JVM will be used
203-
// DefaultHttpClient client = new SystemDefaultHttpClient();
204-
// // Support compressed data
205-
// // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/httpagent.html#d5e1238
206-
// client.AddRequestInterceptor(new RequestAcceptEncoding());
207-
// client.AddResponseInterceptor(new ResponseContentEncoding());
208-
// CacheConfig cacheConfig = new CacheConfig();
209-
// cacheConfig.SetMaxObjectSize(1024 * 128);
210-
// // 128 kB
211-
// cacheConfig.SetMaxCacheEntries(1000);
212-
// // and allow caching
213-
// httpClient = new CachingHttpClient(client, cacheConfig);
214-
// result = httpClient;
215-
// }
216-
// }
217-
// }
218-
// return result;
219-
// }
220-
221-
// public static void SetHttpClient(IHttpClient nextHttpClient)
222-
// {
223-
// lock (typeof(JSONUtils))
224-
// {
225-
// httpClient = nextHttpClient;
226-
// }
227-
// }
228108
}
229109
}

Diff for: src/json-ld.net/Util/JSONUtils.cs

+33-25
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,30 @@
11
using System;
22
using System.Collections;
33
using System.IO;
4+
using System.Linq;
45
using JsonLD.Util;
56
using Newtonsoft.Json;
67
using System.Net;
78
using Newtonsoft.Json.Linq;
9+
using System.Net.Http;
10+
using System.Threading.Tasks;
811

912
namespace JsonLD.Util
1013
{
1114
/// <summary>A bunch of functions to make loading JSON easy</summary>
1215
/// <author>tristan</author>
1316
public class JSONUtils
1417
{
18+
const int MAX_REDIRECTS = 20;
19+
static internal HttpClient _HttpClient = new HttpClient();
20+
1521
/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
16-
/// <remarks>An HTTP Accept header that prefers JSONLD.</remarks>
1722
protected internal const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1";
1823

19-
//private static readonly ObjectMapper JsonMapper = new ObjectMapper();
20-
21-
//private static readonly JsonFactory JsonFactory = new JsonFactory(JsonMapper);
22-
2324
static JSONUtils()
2425
{
25-
// Disable default Jackson behaviour to close
26-
// InputStreams/Readers/OutputStreams/Writers
27-
//JsonFactory.Disable(JsonGenerator.Feature.AutoCloseTarget);
28-
// Disable string retention features that may work for most JSON where
29-
// the field names are in limited supply, but does not work for JSON-LD
30-
// where a wide range of URIs are used for subjects and predicates
31-
//JsonFactory.Disable(JsonFactory.Feature.InternFieldNames);
32-
//JsonFactory.Disable(JsonFactory.Feature.CanonicalizeFieldNames);
3326
}
3427

35-
// private static volatile IHttpClient httpClient;
36-
3728
/// <exception cref="Com.Fasterxml.Jackson.Core.JsonParseException"></exception>
3829
/// <exception cref="System.IO.IOException"></exception>
3930
public static JToken FromString(string jsonString)
@@ -130,6 +121,11 @@ public static string ToString(JToken obj)
130121
return sw.ToString();
131122
}
132123

124+
public static JToken FromURL(Uri url)
125+
{
126+
return FromURLAsync(url).GetAwaiter().GetResult();
127+
}
128+
133129
/// <summary>
134130
/// Returns a Map, List, or String containing the contents of the JSON
135131
/// resource resolved from the URL.
@@ -147,17 +143,29 @@ public static string ToString(JToken obj)
147143
/// </exception>
148144
/// <exception cref="System.IO.IOException">If there was an error resolving the resource.
149145
/// </exception>
150-
public static JToken FromURL(Uri url)
146+
public static async Task<JToken> FromURLAsync(Uri url)
151147
{
152-
#if !PORTABLE && !IS_CORECLR
153-
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url);
154-
req.Accept = AcceptHeader;
155-
WebResponse resp = req.GetResponse();
156-
Stream stream = resp.GetResponseStream();
157-
return FromInputStream(stream);
158-
#else
159-
throw new PlatformNotSupportedException();
160-
#endif
148+
HttpResponseMessage httpResponseMessage = null;
149+
int redirects = 0;
150+
151+
// Manually follow redirects because .NET Core refuses to auto-follow HTTPS->HTTP redirects.
152+
do
153+
{
154+
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
155+
httpRequestMessage.Headers.Add("Accept", AcceptHeader);
156+
httpResponseMessage = await _HttpClient.SendAsync(httpRequestMessage);
157+
if (httpResponseMessage.Headers.TryGetValues("Location", out var location))
158+
{
159+
url = new Uri(location.First());
160+
}
161+
} while (redirects++ < MAX_REDIRECTS && (int)httpResponseMessage.StatusCode >= 300 && (int)httpResponseMessage.StatusCode < 400);
162+
163+
if (redirects >= MAX_REDIRECTS || (int)httpResponseMessage.StatusCode >= 400)
164+
{
165+
throw new InvalidOperationException("Couldn't load JSON from URL");
166+
}
167+
168+
return FromInputStream(await httpResponseMessage.Content.ReadAsStreamAsync());
161169
}
162170
}
163171
}

0 commit comments

Comments
 (0)