Project

General

Profile

1
// CatalogEntityResolver.java - SAX entityResolver using OASIS Catalogs
2
// Written by Norman Walsh, nwalsh@arbortext.com
3
// NO WARRANTY! This class is in the public domain.
4

    
5
package com.arbortext.catalog;
6

    
7
import java.io.IOException;
8
import java.io.InputStream;
9
import java.lang.Integer;
10
import java.net.URL;
11
import java.net.MalformedURLException;
12

    
13
import com.arbortext.catalog.Catalog;
14
import org.xml.sax.EntityResolver;
15
import org.xml.sax.InputSource;
16

    
17
/**
18
 * <p>Implements SAX entityResolver using OASIS Open Catalogs.</p>
19
 *
20
 * <blockquote>
21
 * <em>This module, both source code and documentation, is in the
22
 * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
23
 * </blockquote>
24
 *
25
 * <p>This class implements the SAX entityResolver interface. It uses
26
 * OASIS Open catalog files to provide a facility for mapping public
27
 * or system identifiers in source documents to local system identifiers.
28
 *
29
 * <p>This code interrogates the following non-standard system properties:</p>
30
 *
31
 * <dl>
32
 * <dt><b>xml.catalog.debug</b></dt>
33
 * <dd><p>Sets the debug level. A value of 0 is assumed if the
34
 * property is not set or is not a number.</p></dd>
35
 * </dl>
36
 *
37
 * @see Catalog
38
 *
39
 * @author Arbortext, Inc.
40
 * @author Norman Walsh
41
 *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
42
 * @version 1.0
43
 */
44
public class CatalogEntityResolver implements EntityResolver {
45
    /**
46
     * <p>The debug level</p>
47
     *
48
     * <p>In general, higher numbers produce more information:</p>
49
     * <ul>
50
     * <li>0, no messages
51
     * <li>1, minimal messages (high-level status)
52
     * <li>2, detailed messages
53
     * </ul>
54
     */
55
    public int debug = 0;
56

    
57
    /**
58
     * <p>Indicates that unusable system identifiers should be ignored.</p>
59
     */
60
    private boolean retryBadSystemIds = false;
61

    
62
    /**
63
     * <p>The OASIS Open Catalog used for entity resolution.</p>
64
     *
65
     * <p>This field is exposed so that the catalog can be updated
66
     * after creating the instance of CatalogEntityResolver that will
67
     * be used by the parser.</p>
68
     */
69
    public Catalog catalog = null;
70

    
71
    /**
72
     * <p>Constructs a CatalogEntityResolver with an empty catalog.</p>
73
     */
74
    public CatalogEntityResolver() {
75
	String property = System.getProperty("xml.catalog.debug");
76

    
77
	if (property != null) {
78
	    try {
79
		debug = Integer.parseInt(property);
80
	    } catch (NumberFormatException e) {
81
		debug = 0;
82
	    }
83
	}
84
    }
85

    
86
    /**
87
     * <p>Set the Catalog that will be used to resolve entities.</p>
88
     *
89
     * <p>This is a convenience method for setting the
90
     * <a href="#catalog"><code>catalog</code></a> field.</p>
91
     */
92
    public void setCatalog(Catalog cat) {
93
	catalog = cat;
94
    }
95

    
96
    /**
97
     * <p>Parse a Catalog file.</p>
98
     *
99
     * <p>This is really just a convenience method which calls
100
     * <a href="#catalog"><code>catalog.parseCatalog()</code></a>.</p>
101
     *
102
     * @param fileName The filename of the catalog file to process
103
     *
104
     * @throws MalformedURLException The fileName cannot be turned into
105
     * a valid URL.
106
     * @throws IOException Error reading catalog file.
107
     */
108
    public synchronized void parseCatalog(String fileName)
109
	throws MalformedURLException, IOException {
110
	catalog.parseCatalog(fileName);
111
    }
112

    
113
    /**
114
     * <p>Establish whether or not bad system identifiers should be
115
     * ignored.</p>
116
     *
117
     * <p>The semantics of catalog file lookup are such that if a system
118
     * identifier is supplied in the instance document, it is possible
119
     * that it will be used in preference to alternative system identifiers
120
     * in the catalog.</p>
121
     *
122
     * <p>If this variable is <code>true</code> and the system identifier
123
     * passed to the entity resolver would be returned, the entity resolver
124
     * attempts to open it. If it cannot be opened, the resolver
125
     * does another catalog search,
126
     * ignoring the fact that a system identifier was specified. If this
127
     * second search locates a system identifer, it will be returned.</p>
128
     *
129
     * <p>This setting is initially <code>false</code> meaning that
130
     * system identifiers
131
     * in the document will be used in preference to some entries in
132
     * the catalog.</p>
133
     *
134
     * @param retry If true, the resolver will retry Catalog lookups when
135
     * the supplied system identifer cannot be opened.
136
     */
137
    public void setRetry(boolean retry) {
138
	retryBadSystemIds = retry;
139
    }
140

    
141
    /**
142
     * <p>Implements the <code>resolveEntity</code> method
143
     * for the SAX interface.</p>
144
     *
145
     * <p>Presented with an optional public identifier and a system
146
     * identifier, this function attempts to locate a mapping in the
147
     * catalogs.</p>
148
     *
149
     * <p>If such a mapping is found, the resolver attempts to open
150
     * the mapped value as an InputSource and return it. Exceptions are
151
     * ignored and null is returned if the mapped value cannot be opened
152
     * as an input source.</p>
153
     *
154
     * If no mapping is found (or an error occurs attempting to open
155
     * the mapped value as an input source), null is returned and the system
156
     * will use the specified system identifier as if no entityResolver
157
     * was specified.</p>
158
     *
159
     * @param publicId  The public identifier for the entity in question.
160
     * This may be null.
161
     *
162
     * @param systemId  The system identifier for the entity in question.
163
     * XML requires a system identifier on all external entities, so this
164
     * value is always specified.
165
     *
166
     * @return An InputSource for the mapped identifier, or null.
167
     */
168
    public InputSource resolveEntity (String publicId, String systemId) {
169
	String resolved = null;
170

    
171
	if (systemId != null) {
172
	    try {
173
		resolved = catalog.resolveSystem(systemId);
174
	    } catch (MalformedURLException me) {
175
		debug(1, "Malformed URL exception trying to resolve",
176
		      publicId);
177
		resolved = null;
178
	    } catch (IOException ie) {
179
		debug(1, "I/O exception trying to resolve", publicId);
180
		resolved = null;
181
	    }
182
	}
183

    
184
	if (resolved == null) {
185
	    if (publicId != null) {
186
		try {
187
		    resolved = catalog.resolvePublic(publicId, systemId);
188
		} catch (MalformedURLException me) {
189
		    debug(1, "Malformed URL exception trying to resolve",
190
			  publicId);
191
		} catch (IOException ie) {
192
		    debug(1, "I/O exception trying to resolve", publicId);
193
		}
194
	    }
195

    
196
	    if (resolved != null) {
197
		debug(2, "Resolved", publicId, resolved);
198
	    }
199
	} else {
200
	    debug(2, "Resolved", systemId, resolved);
201
	}
202

    
203
	if (resolved == null && retryBadSystemIds
204
	    && publicId != null && systemId != null) {
205
	    URL systemURL = null;
206
	    try {
207
		systemURL = new URL(systemId);
208
	    } catch (MalformedURLException e) {
209
		try {
210
		    systemURL = new URL("file:///" + systemId);
211
		} catch (MalformedURLException e2) {
212
		    systemURL = null;
213
		}
214
	    }
215

    
216
	    if (systemURL != null) {
217
		try {
218
		    InputStream iStream = systemURL.openStream();
219

    
220
		    // The systemId can be opened, so that's the one that
221
		    // we'll use. There's no point making the caller open
222
		    // it again though...
223

    
224
		    InputSource iSource = new InputSource(systemId);
225
		    iSource.setPublicId(publicId);
226
		    iSource.setByteStream(iStream);
227
		    return iSource;
228
		} catch (Exception e) {
229
		    // nop
230
		}
231
	    }
232

    
233
	    // we got here, so it must be that the systemId cannot be
234
	    // opened and the caller wants us to retry...
235
	    debug(2, "Failed to open", systemId);
236
	    debug(2, "\tAttempting catalog lookup without system identifier.");
237
	    return resolveEntity(publicId, null);
238
	}
239

    
240
	if (resolved != null) {
241
	    try {
242
		InputSource iSource = new InputSource(resolved);
243
		iSource.setPublicId(publicId);
244

    
245
		// Ideally this method would not attempt to open the
246
		// InputStream, but there is a bug (in Xerces, at least)
247
		// that causes the parser to mistakenly open the wrong
248
		// system identifier if the returned InputSource does
249
		// not have a byteStream.
250
		//
251
		// It could be argued that we still shouldn't do this here,
252
		// but since the purpose of calling the entityResolver is
253
		// almost certainly to open the input stream, it seems to
254
		// do little harm.
255
		//
256
		URL url = new URL(resolved);
257
		InputStream iStream = url.openStream();
258
		iSource.setByteStream(iStream);
259

    
260
		return iSource;
261
	    } catch (Exception e) {
262
		debug(1, "Failed to create InputSource", resolved);
263
		return null;
264
	    }
265
	}
266
	return null;
267
    }
268

    
269
    /**
270
     * <p>Print debug message (if the debug level is high enough).</p>
271
     *
272
     * @param level The debug level of this message. This message
273
     * will only be
274
     * displayed if the current debug level is at least equal to this
275
     * value.
276
     * @param message The text of the message.
277
     */
278
    private void debug(int level, String message) {
279
	if (debug >= level) {
280
	    System.out.println(message);
281
	}
282
    }
283

    
284
    /**
285
     * <p>Print debug message (if the debug level is high enough).</p>
286
     *
287
     * @param level The debug level of this message. This message
288
     * will only be
289
     * displayed if the current debug level is at least equal to this
290
     * value.
291
     * @param message The text of the message.
292
     * @param spec An argument to the message.
293
     */
294
    private void debug(int level, String message, String spec) {
295
	if (debug >= level) {
296
	    System.out.println(message + ": " + spec);
297
	}
298
    }
299

    
300
    /**
301
     * <p>Print debug message (if the debug level is high enough).</p>
302
     *
303
     * @param level The debug level of this message. This message
304
     * will only be
305
     * displayed if the current debug level is at least equal to this
306
     * value.
307
     * @param message The text of the message.
308
     * @param spec1 An argument to the message.
309
     * @param spec1 Another argument to the message.
310
     */
311
    private void debug(int level, String message, String spec1, String spec2) {
312
	if (debug >= level) {
313
	    System.out.println(message + ": " + spec1);
314
	    System.out.println("\t" + spec2);
315
	}
316
    }
317
}
(2-2/11)