Skip to content

Commit 959d52a

Browse files
authored
Merge pull request #78 from sblom/issues/21
Add support for HTTP operations on CoreCLR
2 parents b6637a4 + 9e39cb9 commit 959d52a

File tree

6 files changed

+149
-227
lines changed

6 files changed

+149
-227
lines changed

src/json-ld.net/Core/DocumentLoader.cs

Lines changed: 74 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -6,224 +6,108 @@
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+
enum JsonLDContentType
19+
{
20+
JsonLD,
21+
PlainJson,
22+
Other
23+
}
24+
25+
JsonLDContentType GetJsonLDContentType(string contentTypeStr)
26+
{
27+
JsonLDContentType contentType;
28+
29+
switch (contentTypeStr)
30+
{
31+
case "application/ld+json":
32+
contentType = JsonLDContentType.JsonLD;
33+
break;
34+
// From RFC 6839, it looks like plain JSON is content type application/json and any MediaType ending in "+json".
35+
case "application/json":
36+
case string type when type.EndsWith("+json"):
37+
contentType = JsonLDContentType.PlainJson;
38+
break;
39+
default:
40+
contentType = JsonLDContentType.Other;
41+
break;
42+
}
43+
44+
return contentType;
45+
}
46+
1447
/// <exception cref="JsonLDNet.Core.JsonLdError"></exception>
1548
public virtual RemoteDocument LoadDocument(string url)
1649
{
17-
#if !PORTABLE && !IS_CORECLR
18-
RemoteDocument doc = new RemoteDocument(url, null);
19-
HttpWebResponse resp;
50+
return LoadDocumentAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
51+
}
2052

53+
/// <exception cref="JsonLDNet.Core.JsonLdError"></exception>
54+
public virtual async Task<RemoteDocument> LoadDocumentAsync(string url)
55+
{
56+
RemoteDocument doc = new RemoteDocument(url, null);
2157
try
2258
{
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"))
59+
using (HttpResponseMessage response = await JsonLD.Util.LDHttpClient.FetchAsync(url).ConfigureAwait(false))
2860
{
29-
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url);
30-
}
3161

32-
string[] linkHeaders = resp.Headers.GetValues("Link");
33-
if (!isJsonld && linkHeaders != null)
34-
{
35-
linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray()))
36-
.Select(h => h.Trim()).ToArray();
37-
IEnumerable<string> linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\""));
38-
if (linkedContexts.Count() > 1)
62+
var code = (int)response.StatusCode;
63+
64+
if (code >= 400)
3965
{
40-
throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders);
66+
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"HTTP {code} {url}");
4167
}
42-
string header = linkedContexts.First();
43-
string linkedUrl = header.Substring(1, header.IndexOf(">") - 1);
44-
string resolvedUrl = URL.Resolve(url, linkedUrl);
45-
var remoteContext = this.LoadDocument(resolvedUrl);
46-
doc.contextUrl = remoteContext.documentUrl;
47-
doc.context = remoteContext.document;
48-
}
4968

50-
Stream stream = resp.GetResponseStream();
69+
var finalUrl = response.RequestMessage.RequestUri.ToString();
5170

52-
doc.DocumentUrl = req.Address.ToString();
53-
doc.Document = JSONUtils.FromInputStream(stream);
54-
}
55-
catch (JsonLdError)
56-
{
57-
throw;
58-
}
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)
71+
var contentType = GetJsonLDContentType(response.Content.Headers.ContentType.MediaType);
72+
73+
if (contentType == JsonLDContentType.Other)
6674
{
67-
string location = resp.Headers[HttpResponseHeader.Location];
68-
if (!string.IsNullOrWhiteSpace(location))
75+
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url);
76+
}
77+
78+
// For plain JSON, see if there's a context document linked in the HTTP response headers.
79+
if (contentType == JsonLDContentType.PlainJson && response.Headers.TryGetValues("Link", out var linkHeaders))
80+
{
81+
linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray()))
82+
.Select(h => h.Trim()).ToArray();
83+
IEnumerable<string> linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\""));
84+
if (linkedContexts.Count() > 1)
6985
{
70-
// TODO: Add recursion break or simply switch to HttpClient so we don't have to recurse on HTTP redirects.
71-
return LoadDocument(location);
86+
throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders);
7287
}
88+
string header = linkedContexts.First();
89+
string linkedUrl = header.Substring(1, header.IndexOf(">") - 1);
90+
string resolvedUrl = URL.Resolve(finalUrl, linkedUrl);
91+
var remoteContext = await this.LoadDocumentAsync(resolvedUrl).ConfigureAwait(false);
92+
doc.contextUrl = remoteContext.documentUrl;
93+
doc.context = remoteContext.document;
7394
}
74-
}
75-
catch (Exception innerException)
76-
{
77-
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, innerException);
78-
}
7995

80-
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, webException);
96+
Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
97+
98+
doc.DocumentUrl = finalUrl;
99+
doc.Document = JSONUtils.FromInputStream(stream);
100+
}
101+
}
102+
catch (JsonLdError)
103+
{
104+
throw;
81105
}
82106
catch (Exception exception)
83107
{
84108
throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, exception);
85109
}
86110
return doc;
87-
#else
88-
throw new PlatformNotSupportedException();
89-
#endif
90111
}
91-
92-
/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
93-
/// <remarks>An HTTP Accept header that prefers JSONLD.</remarks>
94-
private 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-
// }
228112
}
229113
}

src/json-ld.net/Util/JSONUtils.cs

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,24 @@
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
internal class JSONUtils
1417
{
15-
/// <summary>An HTTP Accept header that prefers JSONLD.</summary>
16-
/// <remarks>An HTTP Accept header that prefers JSONLD.</remarks>
17-
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";
18-
19-
//private static readonly ObjectMapper JsonMapper = new ObjectMapper();
20-
21-
//private static readonly JsonFactory JsonFactory = new JsonFactory(JsonMapper);
22-
2318
static JSONUtils()
2419
{
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);
3320
}
3421

35-
// private static volatile IHttpClient httpClient;
36-
3722
/// <exception cref="Com.Fasterxml.Jackson.Core.JsonParseException"></exception>
3823
/// <exception cref="System.IO.IOException"></exception>
3924
public static JToken FromString(string jsonString)
@@ -130,6 +115,11 @@ public static string ToString(JToken obj)
130115
return sw.ToString();
131116
}
132117

118+
public static JToken FromURL(Uri url)
119+
{
120+
return FromURLAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
121+
}
122+
133123
/// <summary>
134124
/// Returns a Map, List, or String containing the contents of the JSON
135125
/// resource resolved from the URL.
@@ -147,17 +137,11 @@ public static string ToString(JToken obj)
147137
/// </exception>
148138
/// <exception cref="System.IO.IOException">If there was an error resolving the resource.
149139
/// </exception>
150-
public static JToken FromURL(Uri url)
140+
public static async Task<JToken> FromURLAsync(Uri url)
151141
{
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
142+
using (var response = await LDHttpClient.FetchAsync(url.ToString()).ConfigureAwait(false)) {
143+
return FromInputStream(await response.Content.ReadAsStreamAsync().ConfigureAwait(false));
144+
}
161145
}
162146
}
163147
}

0 commit comments

Comments
 (0)