|
6 | 6 | using JsonLD.Util;
|
7 | 7 | using System.Net;
|
8 | 8 | 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; |
9 | 13 |
|
10 | 14 | namespace JsonLD.Core
|
11 | 15 | {
|
12 | 16 | public class DocumentLoader
|
13 | 17 | {
|
| 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 | + |
14 | 47 | /// <exception cref="JsonLDNet.Core.JsonLdError"></exception>
|
15 | 48 | public virtual RemoteDocument LoadDocument(string url)
|
16 | 49 | {
|
17 |
| -#if !PORTABLE && !IS_CORECLR |
18 |
| - RemoteDocument doc = new RemoteDocument(url, null); |
19 |
| - HttpWebResponse resp; |
| 50 | + return LoadDocumentAsync(url).ConfigureAwait(false).GetAwaiter().GetResult(); |
| 51 | + } |
20 | 52 |
|
| 53 | + /// <exception cref="JsonLDNet.Core.JsonLdError"></exception> |
| 54 | + public virtual async Task<RemoteDocument> LoadDocumentAsync(string url) |
| 55 | + { |
| 56 | + RemoteDocument doc = new RemoteDocument(url, null); |
21 | 57 | try
|
22 | 58 | {
|
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)) |
28 | 60 | {
|
29 |
| - throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url); |
30 |
| - } |
31 | 61 |
|
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) |
39 | 65 | {
|
40 |
| - throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders); |
| 66 | + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"HTTP {code} {url}"); |
41 | 67 | }
|
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 |
| - } |
49 | 68 |
|
50 |
| - Stream stream = resp.GetResponseStream(); |
| 69 | + var finalUrl = response.RequestMessage.RequestUri.ToString(); |
51 | 70 |
|
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) |
66 | 74 | {
|
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) |
69 | 85 | {
|
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); |
72 | 87 | }
|
| 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; |
73 | 94 | }
|
74 |
| - } |
75 |
| - catch (Exception innerException) |
76 |
| - { |
77 |
| - throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, innerException); |
78 |
| - } |
79 | 95 |
|
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; |
81 | 105 | }
|
82 | 106 | catch (Exception exception)
|
83 | 107 | {
|
84 | 108 | throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, exception);
|
85 | 109 | }
|
86 | 110 | return doc;
|
87 |
| -#else |
88 |
| - throw new PlatformNotSupportedException(); |
89 |
| -#endif |
90 | 111 | }
|
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 |
| -// } |
228 | 112 | }
|
229 | 113 | }
|
0 commit comments