Skip to content

Commit 604077a

Browse files
authored
Merge pull request #132 from ndw/extend-sources
Add an option to always resolve resources
2 parents 700ad3d + 6eda6da commit 604077a

23 files changed

+451
-28
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
basename=xmlresolver
22

3-
resolverVersion=4.6.4
3+
resolverVersion=5.0.0
44

55
group=org.xmlresolver
66

src/main/java/org/xmlresolver/CatalogResolver.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ public ResolvedResource resolveEntity(String name, String publicId, String syste
253253

254254
URI absSystem = null;
255255

256+
boolean throwExceptions = config.getFeature(ResolverFeature.THROW_URI_EXCEPTIONS);
256257
CatalogManager catalog = config.getFeature(ResolverFeature.CATALOG_MANAGER);
257258
ResolvedResourceImpl result = null;
258259
URI resolved = catalog.lookupEntity(name, systemId, publicId);
@@ -262,8 +263,6 @@ public ResolvedResource resolveEntity(String name, String publicId, String syste
262263
if (resolved != null) {
263264
result = resource(systemId, resolved, cache.cachedSystem(resolved, publicId));
264265
} else {
265-
boolean throwExceptions = config.getFeature(ResolverFeature.THROW_URI_EXCEPTIONS);
266-
267266
try {
268267
if (systemId != null) {
269268
absSystem = new URI(systemId);
@@ -314,8 +313,6 @@ public ResolvedResource resolveEntity(String name, String publicId, String syste
314313
return null;
315314
}
316315

317-
boolean throwExceptions = config.getFeature(ResolverFeature.THROW_URI_EXCEPTIONS);
318-
319316
try {
320317
// There's no point attempting to cache data: URIs.
321318
if ("data".equals(absSystem.getScheme())) {

src/main/java/org/xmlresolver/ResolvedResource.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package org.xmlresolver;
22

3+
import org.xmlresolver.sources.ResolverResourceInfo;
4+
35
import java.io.InputStream;
46
import java.net.URI;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.Map;
510

611
/** A resolved resource represents a successfully resolved resource.
712
*
@@ -49,7 +54,7 @@
4954
* can continue with all of the URIs resolved locally.</p>
5055
*/
5156

52-
public abstract class ResolvedResource {
57+
public abstract class ResolvedResource implements ResolverResourceInfo {
5358
/** The resolved URI.
5459
*
5560
* <p>This is the URI that should be reported as the resolved URI.</p>
@@ -84,4 +89,27 @@ public abstract class ResolvedResource {
8489
* @return The content type, possibly null.
8590
*/
8691
public abstract String getContentType();
92+
93+
/** The status code.
94+
*
95+
* <p>This is the status code for this resource. For http: requests, it should be the
96+
* code returned. For other resource types, it defaults to 200 for convenience.</p>
97+
*
98+
* @return The status code of the (final) request.
99+
*/
100+
public int getStatusCode() {
101+
return 200;
102+
}
103+
104+
/** The headers.
105+
*
106+
* <p>This is the set of headers returned for the resolved resource. This may be empty, for example,
107+
* if the URI was a file: URI. The headers are returned unchanged from the <code>URLConnection</code>,
108+
* so accessing them has to consider the case-insensitive nature of header names.</p>
109+
*
110+
* @return The headers associated with a resource.
111+
*/
112+
public Map<String, List<String>> getHeaders() {
113+
return Collections.emptyMap();
114+
}
87115
}

src/main/java/org/xmlresolver/ResolvedResourceImpl.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
11
package org.xmlresolver;
22

3+
import org.xmlresolver.utils.RsrcUtils;
4+
35
import java.io.InputStream;
46
import java.net.URI;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.Map;
510

611
public class ResolvedResourceImpl extends ResolvedResource {
712
private final URI resolvedURI;
813
private final URI localURI;
914
private final InputStream inputStream;
1015
private final String contentType;
16+
private final Map<String, List<String>> headers;
17+
private final int statusCode;
1118

1219
public ResolvedResourceImpl(URI resolvedURI, URI localURI, InputStream stream, String contentType) {
1320
this.resolvedURI = resolvedURI;
1421
this.localURI = localURI;
1522
this.inputStream = stream;
1623
this.contentType = contentType;
24+
this.headers = Collections.emptyMap();
25+
this.statusCode = 200;
26+
}
27+
28+
public ResolvedResourceImpl(URI resolvedURI, URI localURI, InputStream stream, int status, Map<String,List<String>> headers) {
29+
this.resolvedURI = resolvedURI;
30+
this.localURI = localURI;
31+
this.inputStream = stream;
32+
this.headers = headers;
33+
34+
String ctype = null;
35+
for (String name : headers.keySet()) {
36+
if ("content-type".equalsIgnoreCase(name)) {
37+
ctype = headers.get(name).get(0);
38+
}
39+
}
40+
this.contentType = ctype;
41+
this.statusCode = status;
1742
}
1843

1944
@Override
@@ -35,4 +60,18 @@ public InputStream getInputStream() {
3560
public String getContentType() {
3661
return contentType;
3762
}
63+
64+
@Override
65+
public int getStatusCode() {
66+
return statusCode;
67+
}
68+
69+
@Override
70+
public Map<String,List<String>> getHeaders() {
71+
return headers;
72+
}
73+
74+
public String getHeader(String headerName) {
75+
return RsrcUtils.getHeader(headerName, headers);
76+
}
3877
}

src/main/java/org/xmlresolver/Resolver.java

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@
1111
import org.xmlresolver.sources.ResolverInputSource;
1212
import org.xmlresolver.sources.ResolverLSInput;
1313
import org.xmlresolver.sources.ResolverSAXSource;
14+
import org.xmlresolver.utils.URIUtils;
1415

1516
import javax.xml.transform.Source;
1617
import javax.xml.transform.TransformerException;
1718
import javax.xml.transform.URIResolver;
1819
import java.io.IOException;
20+
import java.net.HttpURLConnection;
21+
import java.net.MalformedURLException;
22+
import java.net.URI;
23+
import java.net.URISyntaxException;
24+
import java.net.URLConnection;
25+
import java.util.HashSet;
1926

2027
/** An implementation of many resolver interfaces.
2128
*
@@ -111,10 +118,27 @@ public CatalogResolver getCatalogResolver() {
111118
public Source resolve(String href, String base) throws TransformerException {
112119
ResolvedResource rsrc = resolver.resolveURI(href, base);
113120
if (rsrc == null) {
114-
return null;
121+
if (href == null || !config.getFeature(ResolverFeature.ALWAYS_RESOLVE)) {
122+
return null;
123+
}
124+
125+
try {
126+
URI uri = base == null ? null : new URI(base);
127+
uri = uri == null ? new URI(href) : uri.resolve(href);
128+
rsrc = openConnection(uri);
129+
} catch (URISyntaxException | IOException ex) {
130+
if (resolver.getConfiguration().getFeature(ResolverFeature.THROW_URI_EXCEPTIONS)) {
131+
throw new TransformerException(ex);
132+
}
133+
return null;
134+
}
135+
136+
if (rsrc == null) {
137+
return null;
138+
}
115139
}
116140

117-
ResolverSAXSource source = new ResolverSAXSource(rsrc.getLocalURI(), new InputSource(rsrc.getInputStream()));
141+
ResolverSAXSource source = new ResolverSAXSource(rsrc);
118142
source.setSystemId(rsrc.getResolvedURI().toString());
119143
return source;
120144
}
@@ -159,7 +183,7 @@ public InputSource getExternalSubset(String name, String baseURI) throws SAXExce
159183
return null;
160184
}
161185

162-
ResolverInputSource source = new ResolverInputSource(rsrc.getLocalURI(), rsrc.getInputStream());
186+
ResolverInputSource source = new ResolverInputSource(rsrc);
163187
source.setSystemId(rsrc.getResolvedURI().toString());
164188
return source;
165189
}
@@ -169,10 +193,16 @@ public InputSource getExternalSubset(String name, String baseURI) throws SAXExce
169193
public InputSource resolveEntity(String name, String publicId, String baseURI, String systemId) throws SAXException, IOException {
170194
ResolvedResource rsrc = resolver.resolveEntity(name, publicId, systemId, baseURI);
171195
if (rsrc == null) {
172-
return null;
196+
if (systemId == null || !config.getFeature(ResolverFeature.ALWAYS_RESOLVE)) {
197+
return null;
198+
}
199+
rsrc = openConnection(systemId);
200+
if (rsrc == null) {
201+
return null;
202+
}
173203
}
174204

175-
ResolverInputSource source = new ResolverInputSource(rsrc.getLocalURI(), rsrc.getInputStream());
205+
ResolverInputSource source = new ResolverInputSource(rsrc);
176206
source.setSystemId(rsrc.getResolvedURI().toString());
177207
return source;
178208
}
@@ -182,10 +212,16 @@ public InputSource resolveEntity(String name, String publicId, String baseURI, S
182212
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
183213
ResolvedResource rsrc = resolver.resolveEntity(null, publicId, systemId, null);
184214
if (rsrc == null) {
185-
return null;
215+
if (systemId == null || !config.getFeature(ResolverFeature.ALWAYS_RESOLVE)) {
216+
return null;
217+
}
218+
rsrc = openConnection(systemId);
219+
if (rsrc == null) {
220+
return null;
221+
}
186222
}
187223

188-
ResolverInputSource source = new ResolverInputSource(rsrc.getLocalURI(), rsrc.getInputStream());
224+
ResolverInputSource source = new ResolverInputSource(rsrc);
189225
source.setSystemId(rsrc.getResolvedURI().toString());
190226
return source;
191227
}
@@ -202,4 +238,69 @@ public Source resolveNamespace(String uri, String nature, String purpose) throws
202238
source.setSystemId(rsrc.getResolvedURI().toString());
203239
return source;
204240
}
241+
242+
protected ResolvedResource openConnection(String absuri) throws IOException {
243+
try {
244+
return openConnection(URIUtils.cwd().resolve(absuri));
245+
} catch (IllegalArgumentException ex) {
246+
if (config.getFeature(ResolverFeature.THROW_URI_EXCEPTIONS)) {
247+
throw ex;
248+
}
249+
return null;
250+
}
251+
}
252+
253+
protected ResolvedResource openConnection(URI originalURI) throws IOException {
254+
boolean throwExceptions = config.getFeature(ResolverFeature.THROW_URI_EXCEPTIONS);
255+
256+
HashSet<URI> seen = new HashSet<>();
257+
int count = 100;
258+
259+
URI absoluteURI = originalURI;
260+
URLConnection connection = null;
261+
boolean done = false;
262+
int code = 200;
263+
264+
while (!done) {
265+
if (seen.contains(absoluteURI)) {
266+
if (throwExceptions) {
267+
throw new IOException("Redirect loop on " + absoluteURI);
268+
}
269+
return null;
270+
}
271+
if (count <= 0) {
272+
if (throwExceptions) {
273+
throw new IOException("Too many redirects on " + absoluteURI);
274+
}
275+
return null;
276+
}
277+
seen.add(absoluteURI);
278+
count--;
279+
280+
try {
281+
connection = absoluteURI.toURL().openConnection();
282+
connection.connect();
283+
} catch (Exception ex) {
284+
if (throwExceptions) {
285+
throw ex;
286+
}
287+
return null;
288+
}
289+
290+
done = !(connection instanceof HttpURLConnection);
291+
if (!done) {
292+
HttpURLConnection conn = (HttpURLConnection) connection;
293+
code = conn.getResponseCode();
294+
if (code >= 300 && code < 400) {
295+
String loc = conn.getHeaderField("location");
296+
absoluteURI = absoluteURI.resolve(loc);
297+
} else {
298+
done = true;
299+
}
300+
}
301+
}
302+
303+
ResolvedResourceImpl rsrc = new ResolvedResourceImpl(originalURI, absoluteURI, connection.getInputStream(), code, connection.getHeaderFields());
304+
return rsrc;
305+
}
205306
}

src/main/java/org/xmlresolver/ResolverFeature.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public static Iterator<String> getNames() {
9898
"http://xmlresolver.org/feature/prefer-property-file", false);
9999

100100
/**
101-
* Determines whether or not the catalog PI in a document
101+
* Determines whether the catalog PI in a document
102102
* may change the list of catalog files to be consulted.
103103
*
104104
* <p>It defaults to <code>true</code>, but there's a small performance cost. Each parse needs
@@ -109,6 +109,19 @@ public static Iterator<String> getNames() {
109109
public static final ResolverFeature<Boolean> ALLOW_CATALOG_PI = new ResolverFeature<>(
110110
"http://xmlresolver.org/feature/allow-catalog-pi", true);
111111

112+
/**
113+
* Should the resolver always return a resource, even when it didn't find it in the catalog?
114+
*
115+
* <p>It defaults to <code>true</code>, but this is in violation of the entity resolver contract.
116+
* The resolver should return null if it fails to find the resource. But some parsers don't follow
117+
* redirects and therefore cannot http-to-https redirected URIs. And the source returned by the
118+
* resolver contains additional, useful information.</p>
119+
* <p>It's worth noting that the .NET contract for the resolver *is* that it always returns
120+
* something, so there's that.</p>
121+
*/
122+
public static final ResolverFeature<Boolean> ALWAYS_RESOLVE = new ResolverFeature<>(
123+
"http://xmlresolver.org/feature/always-resolve", true);
124+
112125
/**
113126
* Sets the location of the cache directory.
114127
*

src/main/java/org/xmlresolver/Resource.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import java.net.*;
1818
import java.nio.charset.StandardCharsets;
1919
import java.util.Base64;
20+
import java.util.List;
21+
import java.util.Map;
2022

2123
/** Represents a web resource.
2224
*
@@ -289,11 +291,6 @@ public URI localUri() {
289291
return localURI;
290292
}
291293

292-
/** Return the resolved URI.
293-
*
294-
* <p>The resolved URI may be different from the local URI of the resource.</p>
295-
*/
296-
297294
/** Return the MIME content type associated with the resource.
298295
*
299296
* @return The content type

0 commit comments

Comments
 (0)