View Javadoc
1   /*
2    * reserved comment block
3    * DO NOT REMOVE OR ALTER!
4    */
5   // Catalog.java - Represents OASIS Open Catalog files.
6   
7   /*
8    * Copyright 2001-2004 The Apache Software Foundation or its licensors,
9    * as applicable.
10   *
11   * Licensed under the Apache License, Version 2.0 (the "License");
12   * you may not use this file except in compliance with the License.
13   * You may obtain a copy of the License at
14   *
15   *      http://www.apache.org/licenses/LICENSE-2.0
16   *
17   * Unless required by applicable law or agreed to in writing, software
18   * distributed under the License is distributed on an "AS IS" BASIS,
19   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20   * See the License for the specific language governing permissions and
21   * limitations under the License.
22   */
23  
24  package com.sun.org.apache.xml.internal.resolver;
25  
26  import com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl;
27  import com.sun.org.apache.xerces.internal.utils.SecuritySupport;
28  import java.io.IOException;
29  import java.io.FileNotFoundException;
30  import java.io.InputStream;
31  import java.io.UnsupportedEncodingException;
32  import java.io.DataInputStream;
33  
34  import java.util.Enumeration;
35  import java.util.Hashtable;
36  import java.util.Vector;
37  
38  import java.net.URL;
39  import java.net.MalformedURLException;
40  
41  import javax.xml.parsers.SAXParserFactory;
42  
43  import com.sun.org.apache.xml.internal.resolver.CatalogManager;
44  import com.sun.org.apache.xml.internal.resolver.helpers.PublicId;
45  import com.sun.org.apache.xml.internal.resolver.readers.CatalogReader;
46  import com.sun.org.apache.xml.internal.resolver.readers.SAXCatalogReader;
47  import com.sun.org.apache.xml.internal.resolver.readers.TR9401CatalogReader;
48  import com.sun.org.apache.xml.internal.resolver.readers.OASISXMLCatalogReader;
49  import com.sun.org.apache.xml.internal.resolver.helpers.FileURL;
50  
51  /**
52   * Represents OASIS Open Catalog files.
53   *
54   * <p>This class implements the semantics of OASIS Open Catalog files
55   * (defined by
56   * <a href="http://www.oasis-open.org/html/a401.htm">OASIS Technical
57   * Resolution 9401:1997 (Amendment 2 to TR 9401)</a>).</p>
58   *
59   * <p>The primary purpose of the Catalog is to associate resources in the
60   * document with local system identifiers. Some entities
61   * (document types, XML entities, and notations) have names and all of them
62   * can have either public or system identifiers or both. (In XML, only a
63   * notation can have a public identifier without a system identifier, but
64   * the methods implemented in this class obey the Catalog semantics
65   * from the SGML
66   * days when system identifiers were optional.)</p>
67   *
68   * <p>The system identifiers returned by the resolution methods in this
69   * class are valid, i.e. usable by, and in fact constructed by, the
70   * <tt>java.net.URL</tt> class. Unfortunately, this class seems to behave in
71   * somewhat non-standard ways and the system identifiers returned may
72   * not be directly usable in a browser or filesystem context.
73   *
74   * <p>This class recognizes all of the Catalog entries defined in
75   * TR9401:1997:</p>
76   *
77   * <ul>
78   * <li><b>BASE</b>
79   * changes the base URI for resolving relative system identifiers. The
80   * initial base URI is the URI of the location of the catalog (which is,
81   * in turn, relative to the location of the current working directory
82   * at startup, as returned by the <tt>user.dir</tt> system property).</li>
83   * <li><b>CATALOG</b>
84   * processes other catalog files. An included catalog occurs logically
85   * at the end of the including catalog.</li>
86   * <li><b>DELEGATE_PUBLIC</b>
87   * specifies alternate catalogs for some public identifiers. The delegated
88   * catalogs are not loaded until they are needed, but they are cached
89   * once loaded.</li>
90   * <li><b>DELEGATE_SYSTEM</b>
91   * specifies alternate catalogs for some system identifiers. The delegated
92   * catalogs are not loaded until they are needed, but they are cached
93   * once loaded.</li>
94   * <li><b>DELEGATE_URI</b>
95   * specifies alternate catalogs for some URIs. The delegated
96   * catalogs are not loaded until they are needed, but they are cached
97   * once loaded.</li>
98   * <li><b>REWRITE_SYSTEM</b>
99   * specifies alternate prefix for a system identifier.</li>
100  * <li><b>REWRITE_URI</b>
101  * specifies alternate prefix for a URI.</li>
102  * <li><b>SYSTEM_SUFFIX</b>
103  * maps any system identifier that ends with a particular suffix to another
104  * system identifier.</li>
105  * <li><b>URI_SUFFIX</b>
106  * maps any URI that ends with a particular suffix to another URI.</li>
107  * <li><b>DOCTYPE</b>
108  * associates the names of root elements with URIs. (In other words, an XML
109  * processor might infer the doctype of an XML document that does not include
110  * a doctype declaration by looking for the DOCTYPE entry in the
111  * catalog which matches the name of the root element of the document.)</li>
112  * <li><b>DOCUMENT</b>
113  * provides a default document.</li>
114  * <li><b>DTDDECL</b>
115  * recognized and silently ignored. Not relevant for XML.</li>
116  * <li><b>ENTITY</b>
117  * associates entity names with URIs.</li>
118  * <li><b>LINKTYPE</b>
119  * recognized and silently ignored. Not relevant for XML.</li>
120  * <li><b>NOTATION</b>
121  * associates notation names with URIs.</li>
122  * <li><b>OVERRIDE</b>
123  * changes the override behavior. Initial behavior is set by the
124  * system property <tt>xml.catalog.override</tt>. The default initial
125  * behavior is 'YES', that is, entries in the catalog override
126  * system identifiers specified in the document.</li>
127  * <li><b>PUBLIC</b>
128  * maps a public identifier to a system identifier.</li>
129  * <li><b>SGMLDECL</b>
130  * recognized and silently ignored. Not relevant for XML.</li>
131  * <li><b>SYSTEM</b>
132  * maps a system identifier to another system identifier.</li>
133  * <li><b>URI</b>
134  * maps a URI to another URI.</li>
135  * </ul>
136  *
137  * <p>Note that BASE entries are treated as described by RFC2396. In
138  * particular, this has the counter-intuitive property that after a BASE
139  * entry identifing "http://example.com/a/b/c" as the base URI,
140  * the relative URI "foo" is resolved to the absolute URI
141  * "http://example.com/a/b/foo". You must provide the trailing slash if
142  * you do not want the final component of the path to be discarded as a
143  * filename would in a URI for a resource: "http://example.com/a/b/c/".
144  * </p>
145  *
146  * <p>Note that subordinate catalogs (all catalogs except the first,
147  * including CATALOG and DELEGATE* catalogs) are only loaded if and when
148  * they are required.</p>
149  *
150  * <p>This class relies on classes which implement the CatalogReader
151  * interface to actually load catalog files. This allows the catalog
152  * semantics to be implemented for TR9401 text-based catalogs, XML
153  * catalogs, or any number of other storage formats.</p>
154  *
155  * <p>Additional catalogs may also be loaded with the
156  * {@link #parseCatalog} method.</p>
157  * </dd>
158  * </dl>
159  *
160  * <p><b>Change Log:</b></p>
161  * <dl>
162  * <dt>2.0</dt>
163  * <dd><p>Rewrite to use CatalogReaders.</p></dd>
164  * <dt>1.1</dt>
165  * <dd><p>Allow quoted components in <tt>xml.catalog.files</tt>
166  * so that URLs containing colons can be used on Unix.
167  * The string passed to <tt>xml.catalog.files</tt> can now have the form:</p>
168  * <pre>
169  * unquoted-path-with-no-sep-chars:"double-quoted path with or without sep chars":'single-quoted path with or without sep chars'
170  * </pre>
171  * <p>(Where ":" is the separater character in this example.)</p>
172  * <p>If an unquoted path contains an embedded double or single quote
173  * character, no special processig is performed on that character. No
174  * path can contain separater characters, double, and single quotes
175  * simultaneously.</p>
176  * <p>Fix bug in calculation of BASE entries: if
177  * a catalog contains multiple BASE entries, each is relative to the preceding
178  * base, not the default base URI of the catalog.</p>
179  * </dd>
180  * <dt>1.0.1</dt>
181  * <dd><p>Fixed a bug in the calculation of the list of subordinate catalogs.
182  * This bug caused an infinite loop where parsing would alternately process
183  * two catalogs indefinitely.</p>
184  * </dd>
185  * </dl>
186  *
187  * @see CatalogReader
188  * @see CatalogEntry
189  *
190  * @author Norman Walsh
191  * <a href="mailto:Norman.Walsh@Sun.COM">Norman.Walsh@Sun.COM</a>
192  *
193  * @version 1.0
194  *
195  * <p>Derived from public domain code originally published by Arbortext,
196  * Inc.</p>
197  */
198 public class Catalog {
199   /** The BASE Catalog Entry type. */
200   public static final int BASE     = CatalogEntry.addEntryType("BASE", 1);
201 
202   /** The CATALOG Catalog Entry type. */
203   public static final int CATALOG  = CatalogEntry.addEntryType("CATALOG", 1);
204 
205   /** The DOCUMENT Catalog Entry type. */
206   public static final int DOCUMENT = CatalogEntry.addEntryType("DOCUMENT", 1);
207 
208   /** The OVERRIDE Catalog Entry type. */
209   public static final int OVERRIDE = CatalogEntry.addEntryType("OVERRIDE", 1);
210 
211   /** The SGMLDECL Catalog Entry type. */
212   public static final int SGMLDECL = CatalogEntry.addEntryType("SGMLDECL", 1);
213 
214   /** The DELEGATE_PUBLIC Catalog Entry type. */
215   public static final int DELEGATE_PUBLIC = CatalogEntry.addEntryType("DELEGATE_PUBLIC", 2);
216 
217   /** The DELEGATE_SYSTEM Catalog Entry type. */
218   public static final int DELEGATE_SYSTEM = CatalogEntry.addEntryType("DELEGATE_SYSTEM", 2);
219 
220   /** The DELEGATE_URI Catalog Entry type. */
221   public static final int DELEGATE_URI = CatalogEntry.addEntryType("DELEGATE_URI", 2);
222 
223   /** The DOCTYPE Catalog Entry type. */
224   public static final int DOCTYPE  = CatalogEntry.addEntryType("DOCTYPE", 2);
225 
226   /** The DTDDECL Catalog Entry type. */
227   public static final int DTDDECL  = CatalogEntry.addEntryType("DTDDECL", 2);
228 
229   /** The ENTITY Catalog Entry type. */
230   public static final int ENTITY   = CatalogEntry.addEntryType("ENTITY", 2);
231 
232   /** The LINKTYPE Catalog Entry type. */
233   public static final int LINKTYPE = CatalogEntry.addEntryType("LINKTYPE", 2);
234 
235   /** The NOTATION Catalog Entry type. */
236   public static final int NOTATION = CatalogEntry.addEntryType("NOTATION", 2);
237 
238   /** The PUBLIC Catalog Entry type. */
239   public static final int PUBLIC   = CatalogEntry.addEntryType("PUBLIC", 2);
240 
241   /** The SYSTEM Catalog Entry type. */
242   public static final int SYSTEM   = CatalogEntry.addEntryType("SYSTEM", 2);
243 
244   /** The URI Catalog Entry type. */
245   public static final int URI      = CatalogEntry.addEntryType("URI", 2);
246 
247   /** The REWRITE_SYSTEM Catalog Entry type. */
248   public static final int REWRITE_SYSTEM = CatalogEntry.addEntryType("REWRITE_SYSTEM", 2);
249 
250   /** The REWRITE_URI Catalog Entry type. */
251   public static final int REWRITE_URI = CatalogEntry.addEntryType("REWRITE_URI", 2);
252   /** The SYSTEM_SUFFIX Catalog Entry type. */
253   public static final int SYSTEM_SUFFIX = CatalogEntry.addEntryType("SYSTEM_SUFFIX", 2);
254   /** The URI_SUFFIX Catalog Entry type. */
255   public static final int URI_SUFFIX = CatalogEntry.addEntryType("URI_SUFFIX", 2);
256 
257   /**
258    * The base URI for relative system identifiers in the catalog.
259    * This may be changed by BASE entries in the catalog.
260    */
261   protected URL base;
262 
263   /** The base URI of the Catalog file currently being parsed. */
264   protected URL catalogCwd;
265 
266   /** The catalog entries currently known to the system. */
267   protected Vector catalogEntries = new Vector();
268 
269   /** The default initial override setting. */
270   protected boolean default_override = true;
271 
272   /** The catalog manager in use for this instance. */
273   protected CatalogManager catalogManager = CatalogManager.getStaticManager();
274 
275   /**
276    * A vector of catalog files to be loaded.
277    *
278    * <p>This list is initially established by
279    * <code>loadSystemCatalogs</code> when
280    * it parses the system catalog list, but CATALOG entries may
281    * contribute to it during the course of parsing.</p>
282    *
283    * @see #loadSystemCatalogs
284    * @see #localCatalogFiles
285    */
286   protected Vector catalogFiles = new Vector();
287 
288   /**
289    * A vector of catalog files constructed during processing of
290    * CATALOG entries in the current catalog.
291    *
292    * <p>This two-level system is actually necessary to correctly implement
293    * the semantics of the CATALOG entry. If one catalog file includes
294    * another with a CATALOG entry, the included catalog logically
295    * occurs <i>at the end</i> of the including catalog, and after any
296    * preceding CATALOG entries. In other words, the CATALOG entry
297    * cannot insert anything into the middle of a catalog file.</p>
298    *
299    * <p>When processing reaches the end of each catalog files, any
300    * elements on this vector are added to the front of the
301    * <code>catalogFiles</code> vector.</p>
302    *
303    * @see #catalogFiles
304    */
305   protected Vector localCatalogFiles = new Vector();
306 
307   /**
308    * A vector of Catalogs.
309    *
310    * <p>The semantics of Catalog resolution are such that each
311    * catalog is effectively a list of Catalogs (in other words,
312    * a recursive list of Catalog instances).</p>
313    *
314    * <p>Catalogs that are processed as the result of CATALOG or
315    * DELEGATE* entries are subordinate to the catalog that contained
316    * them, but they may in turn have subordinate catalogs.</p>
317    *
318    * <p>Catalogs are only loaded when they are needed, so this vector
319    * initially contains a list of Catalog filenames (URLs). If, during
320    * processing, one of these catalogs has to be loaded, the resulting
321    * Catalog object is placed in the vector, effectively caching it
322    * for the next query.</p>
323    */
324   protected Vector catalogs = new Vector();
325 
326   /**
327    * A vector of DELEGATE* Catalog entries constructed during
328    * processing of the Catalog.
329    *
330    * <p>This two-level system has two purposes; first, it allows
331    * us to sort the DELEGATE* entries by the length of the partial
332    * public identifier so that a linear search encounters them in
333    * the correct order and second, it puts them all at the end of
334    * the Catalog.</p>
335    *
336    * <p>When processing reaches the end of each catalog file, any
337    * elements on this vector are added to the end of the
338    * <code>catalogEntries</code> vector. This assures that matching
339    * PUBLIC keywords are encountered before DELEGATE* entries.</p>
340    */
341   protected Vector localDelegate = new Vector();
342 
343   /**
344    * A hash of CatalogReaders.
345    *
346    * <p>This hash maps MIME types to elements in the readerArr
347    * vector. This allows the Catalog to quickly locate the reader
348    * for a particular MIME type.</p>
349    */
350   protected Hashtable readerMap = new Hashtable();
351 
352   /**
353    * A vector of CatalogReaders.
354    *
355    * <p>This vector contains all of the readers in the order that they
356    * were added. In the event that a catalog is read from a file, where
357    * the MIME type is unknown, each reader is attempted in turn until
358    * one succeeds.</p>
359    */
360   protected Vector readerArr = new Vector();
361 
362   /**
363    * Constructs an empty Catalog.
364    *
365    * <p>The constructor interrogates the relevant system properties
366    * using the default (static) CatalogManager
367    * and initializes the catalog data structures.</p>
368    */
369   public Catalog() {
370     // nop;
371   }
372 
373   /**
374    * Constructs an empty Catalog with a specific CatalogManager.
375    *
376    * <p>The constructor interrogates the relevant system properties
377    * using the specified Catalog Manager
378    * and initializes the catalog data structures.</p>
379    */
380   public Catalog(CatalogManager manager) {
381     catalogManager = manager;
382   }
383 
384   /**
385    * Return the CatalogManager used by this catalog.
386    *
387    */
388   public CatalogManager getCatalogManager() {
389     return catalogManager;
390   }
391 
392   /**
393    * Establish the CatalogManager used by this catalog.
394    *
395    */
396   public void setCatalogManager(CatalogManager manager) {
397     catalogManager = manager;
398   }
399 
400   /**
401    * Setup readers.
402    */
403   public void setupReaders() {
404     SAXParserFactory spf = catalogManager.useServicesMechanism() ?
405                     SAXParserFactory.newInstance() : new SAXParserFactoryImpl();
406     spf.setNamespaceAware(true);
407     spf.setValidating(false);
408 
409     SAXCatalogReader saxReader = new SAXCatalogReader(spf);
410 
411     saxReader.setCatalogParser(null, "XMLCatalog",
412                                "com.sun.org.apache.xml.internal.resolver.readers.XCatalogReader");
413 
414     saxReader.setCatalogParser(OASISXMLCatalogReader.namespaceName,
415                                "catalog",
416                                "com.sun.org.apache.xml.internal.resolver.readers.OASISXMLCatalogReader");
417 
418     addReader("application/xml", saxReader);
419 
420     TR9401CatalogReader textReader = new TR9401CatalogReader();
421     addReader("text/plain", textReader);
422   }
423 
424   /**
425    * Add a new CatalogReader to the Catalog.
426    *
427    * <p>This method allows you to add a new CatalogReader to the
428    * catalog. The reader will be associated with the specified mimeType.
429    * You can only have one reader per mimeType.</p>
430    *
431    * <p>In the absence of a mimeType (e.g., when reading a catalog
432    * directly from a file on the local system), the readers are attempted
433    * in the order that you add them to the Catalog.</p>
434    *
435    * <p>Note that subordinate catalogs (created by CATALOG or
436    * DELEGATE* entries) get a copy of the set of readers present in
437    * the primary catalog when they are created. Readers added subsequently
438    * will not be available. For this reason, it is best to add all
439    * of the readers before the first call to parse a catalog.</p>
440    *
441    * @param mimeType The MIME type associated with this reader.
442    * @param reader The CatalogReader to use.
443    */
444   public void addReader(String mimeType, CatalogReader reader) {
445     if (readerMap.containsKey(mimeType)) {
446       Integer pos = (Integer) readerMap.get(mimeType);
447       readerArr.set(pos.intValue(), reader);
448     } else {
449       readerArr.add(reader);
450       Integer pos = new Integer(readerArr.size()-1);
451       readerMap.put(mimeType, pos);
452     }
453   }
454 
455   /**
456    * Copies the reader list from the current Catalog to a new Catalog.
457    *
458    * <p>This method is used internally when constructing a new catalog.
459    * It copies the current reader associations over to the new catalog.
460    * </p>
461    *
462    * @param newCatalog The new Catalog.
463    */
464   protected void copyReaders(Catalog newCatalog) {
465     // Have to copy the readers in the right order...convert hash to arr
466     Vector mapArr = new Vector(readerMap.size());
467 
468     // Pad the mapArr out to the right length
469     for (int count = 0; count < readerMap.size(); count++) {
470       mapArr.add(null);
471     }
472 
473     Enumeration en = readerMap.keys();
474     while (en.hasMoreElements()) {
475       String mimeType = (String) en.nextElement();
476       Integer pos = (Integer) readerMap.get(mimeType);
477       mapArr.set(pos.intValue(), mimeType);
478     }
479 
480     for (int count = 0; count < mapArr.size(); count++) {
481       String mimeType = (String) mapArr.get(count);
482       Integer pos = (Integer) readerMap.get(mimeType);
483       newCatalog.addReader(mimeType,
484                            (CatalogReader)
485                            readerArr.get(pos.intValue()));
486     }
487   }
488 
489   /**
490    * Create a new Catalog object.
491    *
492    * <p>This method constructs a new instance of the running Catalog
493    * class (which might be a subtype of com.sun.org.apache.xml.internal.resolver.Catalog).
494    * All new catalogs are managed by the same CatalogManager.
495    * </p>
496    *
497    * <p>N.B. All Catalog subtypes should call newCatalog() to construct
498    * a new Catalog. Do not simply use "new Subclass()" since that will
499    * confuse future subclasses.</p>
500    */
501   protected Catalog newCatalog() {
502     String catalogClass = this.getClass().getName();
503 
504     try {
505       Catalog c = (Catalog) (Class.forName(catalogClass).newInstance());
506       c.setCatalogManager(catalogManager);
507       copyReaders(c);
508       return c;
509     } catch (ClassNotFoundException cnfe) {
510       catalogManager.debug.message(1, "Class Not Found Exception: " + catalogClass);
511     } catch (IllegalAccessException iae) {
512       catalogManager.debug.message(1, "Illegal Access Exception: " + catalogClass);
513     } catch (InstantiationException ie) {
514       catalogManager.debug.message(1, "Instantiation Exception: " + catalogClass);
515     } catch (ClassCastException cce) {
516       catalogManager.debug.message(1, "Class Cast Exception: " + catalogClass);
517     } catch (Exception e) {
518       catalogManager.debug.message(1, "Other Exception: " + catalogClass);
519     }
520 
521     Catalog c = new Catalog();
522     c.setCatalogManager(catalogManager);
523     copyReaders(c);
524     return c;
525   }
526 
527   /**
528    * Returns the current base URI.
529    */
530   public String getCurrentBase() {
531     return base.toString();
532   }
533 
534   /**
535    * Returns the default override setting associated with this
536    * catalog.
537    *
538    * <p>All catalog files loaded by this catalog will have the
539    * initial override setting specified by this default.</p>
540    */
541   public String getDefaultOverride() {
542     if (default_override) {
543       return "yes";
544     } else {
545       return "no";
546     }
547   }
548 
549   /**
550    * Load the system catalog files.
551    *
552    * <p>The method adds all of the
553    * catalogs specified in the <tt>xml.catalog.files</tt> property
554    * to the Catalog list.</p>
555    *
556    * @throws MalformedURLException  One of the system catalogs is
557    * identified with a filename that is not a valid URL.
558    * @throws IOException One of the system catalogs cannot be read.
559    */
560   public void loadSystemCatalogs()
561     throws MalformedURLException, IOException {
562 
563     Vector catalogs = catalogManager.getCatalogFiles();
564     if (catalogs != null) {
565       for (int count = 0; count < catalogs.size(); count++) {
566         catalogFiles.addElement(catalogs.elementAt(count));
567       }
568     }
569 
570     if (catalogFiles.size() > 0) {
571       // This is a little odd. The parseCatalog() method expects
572       // a filename, but it adds that name to the end of the
573       // catalogFiles vector, and then processes that vector.
574       // This allows the system to handle CATALOG entries
575       // correctly.
576       //
577       // In this init case, we take the last element off the
578       // catalogFiles vector and pass it to parseCatalog. This
579       // will "do the right thing" in the init case, and allow
580       // parseCatalog() to do the right thing in the non-init
581       // case. Honest.
582       //
583       String catfile = (String) catalogFiles.lastElement();
584       catalogFiles.removeElement(catfile);
585       parseCatalog(catfile);
586     }
587   }
588 
589   /**
590    * Parse a catalog file, augmenting internal data structures.
591    *
592    * @param fileName The filename of the catalog file to process
593    *
594    * @throws MalformedURLException The fileName cannot be turned into
595    * a valid URL.
596    * @throws IOException Error reading catalog file.
597    */
598   public synchronized void parseCatalog(String fileName)
599     throws MalformedURLException, IOException {
600 
601     default_override = catalogManager.getPreferPublic();
602     catalogManager.debug.message(4, "Parse catalog: " + fileName);
603 
604     // Put the file into the list of catalogs to process...
605     // In all cases except the case when initCatalog() is the
606     // caller, this will be the only catalog initially in the list...
607     catalogFiles.addElement(fileName);
608 
609     // Now process all the pending catalogs...
610     parsePendingCatalogs();
611   }
612 
613   /**
614    * Parse a catalog file, augmenting internal data structures.
615    *
616    * <p>Catalogs retrieved over the net may have an associated MIME type.
617    * The MIME type can be used to select an appropriate reader.</p>
618    *
619    * @param mimeType The MIME type of the catalog file.
620    * @param is The InputStream from which the catalog should be read
621    *
622    * @throws CatalogException Failed to load catalog
623    * mimeType.
624    * @throws IOException Error reading catalog file.
625    */
626   public synchronized void parseCatalog(String mimeType, InputStream is)
627     throws IOException, CatalogException {
628 
629     default_override = catalogManager.getPreferPublic();
630     catalogManager.debug.message(4, "Parse " + mimeType + " catalog on input stream");
631 
632     CatalogReader reader = null;
633 
634     if (readerMap.containsKey(mimeType)) {
635       int arrayPos = ((Integer) readerMap.get(mimeType)).intValue();
636       reader = (CatalogReader) readerArr.get(arrayPos);
637     }
638 
639     if (reader == null) {
640       String msg = "No CatalogReader for MIME type: " + mimeType;
641       catalogManager.debug.message(2, msg);
642       throw new CatalogException(CatalogException.UNPARSEABLE, msg);
643     }
644 
645     reader.readCatalog(this, is);
646 
647     // Now process all the pending catalogs...
648     parsePendingCatalogs();
649   }
650 
651   /**
652    * Parse a catalog document, augmenting internal data structures.
653    *
654    * <p>This method supports catalog files stored in jar files: e.g.,
655    * jar:file:///path/to/filename.jar!/path/to/catalog.xml". That URI
656    * doesn't survive transmogrification through the URI processing that
657    * the parseCatalog(String) performs and passing it as an input stream
658    * doesn't set the base URI appropriately.</p>
659    *
660    * <p>Written by Stefan Wachter (2002-09-26)</p>
661    *
662    * @param aUrl The URL of the catalog document to process
663    *
664    * @throws IOException Error reading catalog file.
665    */
666   public synchronized void parseCatalog(URL aUrl) throws IOException {
667     catalogCwd = aUrl;
668     base = aUrl;
669 
670     default_override = catalogManager.getPreferPublic();
671     catalogManager.debug.message(4, "Parse catalog: " + aUrl.toString());
672 
673     DataInputStream inStream = null;
674     boolean parsed = false;
675 
676     for (int count = 0; !parsed && count < readerArr.size(); count++) {
677       CatalogReader reader = (CatalogReader) readerArr.get(count);
678 
679       try {
680         inStream = new DataInputStream(aUrl.openStream());
681       } catch (FileNotFoundException fnfe) {
682         // No catalog; give up!
683         break;
684       }
685 
686       try {
687         reader.readCatalog(this, inStream);
688         parsed=true;
689       } catch (CatalogException ce) {
690         if (ce.getExceptionType() == CatalogException.PARSE_FAILED) {
691           // give up!
692           break;
693         } else {
694           // try again!
695         }
696       }
697 
698       try {
699         inStream.close();
700       } catch (IOException e) {
701         //nop
702       }
703     }
704 
705     if (parsed) parsePendingCatalogs();
706   }
707 
708   /**
709    * Parse all of the pending catalogs.
710    *
711    * <p>Catalogs may refer to other catalogs, this method parses
712    * all of the currently pending catalog files.</p>
713    */
714   protected synchronized void parsePendingCatalogs()
715     throws MalformedURLException, IOException {
716 
717     if (!localCatalogFiles.isEmpty()) {
718       // Move all the localCatalogFiles into the front of
719       // the catalogFiles queue
720       Vector newQueue = new Vector();
721       Enumeration q = localCatalogFiles.elements();
722       while (q.hasMoreElements()) {
723         newQueue.addElement(q.nextElement());
724       }
725 
726       // Put the rest of the catalogs on the end of the new list
727       for (int curCat = 0; curCat < catalogFiles.size(); curCat++) {
728         String catfile = (String) catalogFiles.elementAt(curCat);
729         newQueue.addElement(catfile);
730       }
731 
732       catalogFiles = newQueue;
733       localCatalogFiles.clear();
734     }
735 
736     // Suppose there are no catalog files to process, but the
737     // single catalog already parsed included some delegate
738     // entries? Make sure they don't get lost.
739     if (catalogFiles.isEmpty() && !localDelegate.isEmpty()) {
740       Enumeration e = localDelegate.elements();
741       while (e.hasMoreElements()) {
742         catalogEntries.addElement(e.nextElement());
743       }
744       localDelegate.clear();
745     }
746 
747     // Now process all the files on the catalogFiles vector. This
748     // vector can grow during processing if CATALOG entries are
749     // encountered in the catalog
750     while (!catalogFiles.isEmpty()) {
751       String catfile = (String) catalogFiles.elementAt(0);
752       try {
753         catalogFiles.remove(0);
754       } catch (ArrayIndexOutOfBoundsException e) {
755         // can't happen
756       }
757 
758       if (catalogEntries.size() == 0 && catalogs.size() == 0) {
759         // We haven't parsed any catalogs yet, let this
760         // catalog be the first...
761         try {
762           parseCatalogFile(catfile);
763         } catch (CatalogException ce) {
764           System.out.println("FIXME: " + ce.toString());
765         }
766       } else {
767         // This is a subordinate catalog. We save its name,
768         // but don't bother to load it unless it's necessary.
769         catalogs.addElement(catfile);
770       }
771 
772       if (!localCatalogFiles.isEmpty()) {
773         // Move all the localCatalogFiles into the front of
774         // the catalogFiles queue
775         Vector newQueue = new Vector();
776         Enumeration q = localCatalogFiles.elements();
777         while (q.hasMoreElements()) {
778           newQueue.addElement(q.nextElement());
779         }
780 
781         // Put the rest of the catalogs on the end of the new list
782         for (int curCat = 0; curCat < catalogFiles.size(); curCat++) {
783           catfile = (String) catalogFiles.elementAt(curCat);
784           newQueue.addElement(catfile);
785         }
786 
787         catalogFiles = newQueue;
788         localCatalogFiles.clear();
789       }
790 
791       if (!localDelegate.isEmpty()) {
792         Enumeration e = localDelegate.elements();
793         while (e.hasMoreElements()) {
794           catalogEntries.addElement(e.nextElement());
795         }
796         localDelegate.clear();
797       }
798     }
799 
800     // We've parsed them all, reinit the vector...
801     catalogFiles.clear();
802   }
803 
804   /**
805    * Parse a single catalog file, augmenting internal data structures.
806    *
807    * @param fileName The filename of the catalog file to process
808    *
809    * @throws MalformedURLException The fileName cannot be turned into
810    * a valid URL.
811    * @throws IOException Error reading catalog file.
812    */
813   protected synchronized void parseCatalogFile(String fileName)
814     throws MalformedURLException, IOException, CatalogException {
815 
816     CatalogEntry entry;
817 
818     // The base-base is the cwd. If the catalog file is specified
819     // with a relative path, this assures that it gets resolved
820     // properly...
821     try {
822       // tack on a basename because URLs point to files not dirs
823       catalogCwd = FileURL.makeURL("basename");
824     } catch (MalformedURLException e) {
825       String userdir = SecuritySupport.getSystemProperty("user.dir");
826       userdir.replace('\\', '/');
827       catalogManager.debug.message(1, "Malformed URL on cwd", userdir);
828       catalogCwd = null;
829     }
830 
831     // The initial base URI is the location of the catalog file
832     try {
833       base = new URL(catalogCwd, fixSlashes(fileName));
834     } catch (MalformedURLException e) {
835       try {
836         base = new URL("file:" + fixSlashes(fileName));
837       } catch (MalformedURLException e2) {
838         catalogManager.debug.message(1, "Malformed URL on catalog filename",
839                       fixSlashes(fileName));
840         base = null;
841       }
842     }
843 
844     catalogManager.debug.message(2, "Loading catalog", fileName);
845     catalogManager.debug.message(4, "Default BASE", base.toString());
846 
847     fileName = base.toString();
848 
849     DataInputStream inStream = null;
850     boolean parsed = false;
851     boolean notFound = false;
852 
853     for (int count = 0; !parsed && count < readerArr.size(); count++) {
854       CatalogReader reader = (CatalogReader) readerArr.get(count);
855 
856       try {
857         notFound = false;
858         inStream = new DataInputStream(base.openStream());
859       } catch (FileNotFoundException fnfe) {
860         // No catalog; give up!
861         notFound = true;
862         break;
863       }
864 
865       try {
866         reader.readCatalog(this, inStream);
867         parsed = true;
868       } catch (CatalogException ce) {
869         if (ce.getExceptionType() == CatalogException.PARSE_FAILED) {
870           // give up!
871           break;
872         } else {
873           // try again!
874         }
875       }
876 
877       try {
878         inStream.close();
879       } catch (IOException e) {
880         //nop
881       }
882     }
883 
884     if (!parsed) {
885       if (notFound) {
886         catalogManager.debug.message(3, "Catalog does not exist", fileName);
887       } else {
888         catalogManager.debug.message(1, "Failed to parse catalog", fileName);
889       }
890     }
891   }
892 
893   /**
894    * Cleanup and process a Catalog entry.
895    *
896    * <p>This method processes each Catalog entry, changing mapped
897    * relative system identifiers into absolute ones (based on the current
898    * base URI), and maintaining other information about the current
899    * catalog.</p>
900    *
901    * @param entry The CatalogEntry to process.
902    */
903   public void addEntry(CatalogEntry entry) {
904     int type = entry.getEntryType();
905 
906     if (type == BASE) {
907       String value = entry.getEntryArg(0);
908       URL newbase = null;
909 
910       if (base == null) {
911         catalogManager.debug.message(5, "BASE CUR", "null");
912       } else {
913         catalogManager.debug.message(5, "BASE CUR", base.toString());
914       }
915       catalogManager.debug.message(4, "BASE STR", value);
916 
917       try {
918         value = fixSlashes(value);
919         newbase = new URL(base, value);
920       } catch (MalformedURLException e) {
921         try {
922           newbase = new URL("file:" + value);
923         } catch (MalformedURLException e2) {
924           catalogManager.debug.message(1, "Malformed URL on base", value);
925           newbase = null;
926         }
927       }
928 
929       if (newbase != null) {
930         base = newbase;
931       }
932 
933       catalogManager.debug.message(5, "BASE NEW", base.toString());
934     } else if (type == CATALOG) {
935       String fsi = makeAbsolute(entry.getEntryArg(0));
936 
937       catalogManager.debug.message(4, "CATALOG", fsi);
938 
939       localCatalogFiles.addElement(fsi);
940     } else if (type == PUBLIC) {
941       String publicid = PublicId.normalize(entry.getEntryArg(0));
942       String systemid = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
943 
944       entry.setEntryArg(0, publicid);
945       entry.setEntryArg(1, systemid);
946 
947       catalogManager.debug.message(4, "PUBLIC", publicid, systemid);
948 
949       catalogEntries.addElement(entry);
950     } else if (type == SYSTEM) {
951       String systemid = normalizeURI(entry.getEntryArg(0));
952       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
953 
954       entry.setEntryArg(1, fsi);
955 
956       catalogManager.debug.message(4, "SYSTEM", systemid, fsi);
957 
958       catalogEntries.addElement(entry);
959     } else if (type == URI) {
960       String uri = normalizeURI(entry.getEntryArg(0));
961       String altURI = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
962 
963       entry.setEntryArg(1, altURI);
964 
965       catalogManager.debug.message(4, "URI", uri, altURI);
966 
967       catalogEntries.addElement(entry);
968     } else if (type == DOCUMENT) {
969       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(0)));
970       entry.setEntryArg(0, fsi);
971 
972       catalogManager.debug.message(4, "DOCUMENT", fsi);
973 
974       catalogEntries.addElement(entry);
975     } else if (type == OVERRIDE) {
976       catalogManager.debug.message(4, "OVERRIDE", entry.getEntryArg(0));
977 
978       catalogEntries.addElement(entry);
979     } else if (type == SGMLDECL) {
980       // meaningless in XML
981       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(0)));
982       entry.setEntryArg(0, fsi);
983 
984       catalogManager.debug.message(4, "SGMLDECL", fsi);
985 
986       catalogEntries.addElement(entry);
987     } else if (type == DELEGATE_PUBLIC) {
988       String ppi = PublicId.normalize(entry.getEntryArg(0));
989       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
990 
991       entry.setEntryArg(0, ppi);
992       entry.setEntryArg(1, fsi);
993 
994       catalogManager.debug.message(4, "DELEGATE_PUBLIC", ppi, fsi);
995 
996       addDelegate(entry);
997     } else if (type == DELEGATE_SYSTEM) {
998       String psi = normalizeURI(entry.getEntryArg(0));
999       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1000 
1001       entry.setEntryArg(0, psi);
1002       entry.setEntryArg(1, fsi);
1003 
1004       catalogManager.debug.message(4, "DELEGATE_SYSTEM", psi, fsi);
1005 
1006       addDelegate(entry);
1007     } else if (type == DELEGATE_URI) {
1008       String pui = normalizeURI(entry.getEntryArg(0));
1009       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1010 
1011       entry.setEntryArg(0, pui);
1012       entry.setEntryArg(1, fsi);
1013 
1014       catalogManager.debug.message(4, "DELEGATE_URI", pui, fsi);
1015 
1016       addDelegate(entry);
1017     } else if (type == REWRITE_SYSTEM) {
1018       String psi = normalizeURI(entry.getEntryArg(0));
1019       String rpx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1020 
1021       entry.setEntryArg(0, psi);
1022       entry.setEntryArg(1, rpx);
1023 
1024       catalogManager.debug.message(4, "REWRITE_SYSTEM", psi, rpx);
1025 
1026       catalogEntries.addElement(entry);
1027     } else if (type == REWRITE_URI) {
1028       String pui = normalizeURI(entry.getEntryArg(0));
1029       String upx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1030 
1031       entry.setEntryArg(0, pui);
1032       entry.setEntryArg(1, upx);
1033 
1034       catalogManager.debug.message(4, "REWRITE_URI", pui, upx);
1035 
1036       catalogEntries.addElement(entry);
1037     } else if (type == SYSTEM_SUFFIX) {
1038       String pui = normalizeURI(entry.getEntryArg(0));
1039       String upx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1040 
1041       entry.setEntryArg(0, pui);
1042       entry.setEntryArg(1, upx);
1043 
1044       catalogManager.debug.message(4, "SYSTEM_SUFFIX", pui, upx);
1045 
1046       catalogEntries.addElement(entry);
1047     } else if (type == URI_SUFFIX) {
1048       String pui = normalizeURI(entry.getEntryArg(0));
1049       String upx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1050 
1051       entry.setEntryArg(0, pui);
1052       entry.setEntryArg(1, upx);
1053 
1054       catalogManager.debug.message(4, "URI_SUFFIX", pui, upx);
1055 
1056       catalogEntries.addElement(entry);
1057     } else if (type == DOCTYPE) {
1058       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1059       entry.setEntryArg(1, fsi);
1060 
1061       catalogManager.debug.message(4, "DOCTYPE", entry.getEntryArg(0), fsi);
1062 
1063       catalogEntries.addElement(entry);
1064     } else if (type == DTDDECL) {
1065       // meaningless in XML
1066       String fpi = PublicId.normalize(entry.getEntryArg(0));
1067       entry.setEntryArg(0, fpi);
1068       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1069       entry.setEntryArg(1, fsi);
1070 
1071       catalogManager.debug.message(4, "DTDDECL", fpi, fsi);
1072 
1073       catalogEntries.addElement(entry);
1074     } else if (type == ENTITY) {
1075       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1076       entry.setEntryArg(1, fsi);
1077 
1078       catalogManager.debug.message(4, "ENTITY", entry.getEntryArg(0), fsi);
1079 
1080       catalogEntries.addElement(entry);
1081     } else if (type == LINKTYPE) {
1082       // meaningless in XML
1083       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1084       entry.setEntryArg(1, fsi);
1085 
1086       catalogManager.debug.message(4, "LINKTYPE", entry.getEntryArg(0), fsi);
1087 
1088       catalogEntries.addElement(entry);
1089     } else if (type == NOTATION) {
1090       String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
1091       entry.setEntryArg(1, fsi);
1092 
1093       catalogManager.debug.message(4, "NOTATION", entry.getEntryArg(0), fsi);
1094 
1095       catalogEntries.addElement(entry);
1096     } else {
1097       catalogEntries.addElement(entry);
1098     }
1099   }
1100 
1101   /**
1102    * Handle unknown CatalogEntry types.
1103    *
1104    * <p>This method exists to allow subclasses to deal with unknown
1105    * entry types.</p>
1106    */
1107   public void unknownEntry(Vector strings) {
1108     if (strings != null && strings.size() > 0) {
1109       String keyword = (String) strings.elementAt(0);
1110       catalogManager.debug.message(2, "Unrecognized token parsing catalog", keyword);
1111     }
1112   }
1113 
1114   /**
1115    * Parse all subordinate catalogs.
1116    *
1117    * <p>This method recursively parses all of the subordinate catalogs.
1118    * If this method does not throw an exception, you can be confident that
1119    * no subsequent call to any resolve*() method will either, with two
1120    * possible exceptions:</p>
1121    *
1122    * <ol>
1123    * <li><p>Delegated catalogs are re-parsed each time they are needed
1124    * (because a variable list of them may be needed in each case,
1125    * depending on the length of the matching partial public identifier).</p>
1126    * <p>But they are parsed by this method, so as long as they don't
1127    * change or disappear while the program is running, they shouldn't
1128    * generate errors later if they don't generate errors now.</p>
1129    * <li><p>If you add new catalogs with <code>parseCatalog</code>, they
1130    * won't be loaded until they are needed or until you call
1131    * <code>parseAllCatalogs</code> again.</p>
1132    * </ol>
1133    *
1134    * <p>On the other hand, if you don't call this method, you may
1135    * successfully parse documents without having to load all possible
1136    * catalogs.</p>
1137    *
1138    * @throws MalformedURLException The filename (URL) for a
1139    * subordinate or delegated catalog is not a valid URL.
1140    * @throws IOException Error reading some subordinate or delegated
1141    * catalog file.
1142    */
1143   public void parseAllCatalogs()
1144     throws MalformedURLException, IOException {
1145 
1146     // Parse all the subordinate catalogs
1147     for (int catPos = 0; catPos < catalogs.size(); catPos++) {
1148       Catalog c = null;
1149 
1150       try {
1151         c = (Catalog) catalogs.elementAt(catPos);
1152       } catch (ClassCastException e) {
1153         String catfile = (String) catalogs.elementAt(catPos);
1154         c = newCatalog();
1155 
1156         c.parseCatalog(catfile);
1157         catalogs.setElementAt(c, catPos);
1158         c.parseAllCatalogs();
1159       }
1160     }
1161 
1162     // Parse all the DELEGATE catalogs
1163     Enumeration en = catalogEntries.elements();
1164     while (en.hasMoreElements()) {
1165       CatalogEntry e = (CatalogEntry) en.nextElement();
1166       if (e.getEntryType() == DELEGATE_PUBLIC
1167           || e.getEntryType() == DELEGATE_SYSTEM
1168           || e.getEntryType() == DELEGATE_URI) {
1169         Catalog dcat = newCatalog();
1170         dcat.parseCatalog(e.getEntryArg(1));
1171       }
1172     }
1173   }
1174 
1175 
1176   /**
1177    * Return the applicable DOCTYPE system identifier.
1178    *
1179    * @param entityName The name of the entity (element) for which
1180    * a doctype is required.
1181    * @param publicId The nominal public identifier for the doctype
1182    * (as provided in the source document).
1183    * @param systemId The nominal system identifier for the doctype
1184    * (as provided in the source document).
1185    *
1186    * @return The system identifier to use for the doctype.
1187    *
1188    * @throws MalformedURLException The formal system identifier of a
1189    * subordinate catalog cannot be turned into a valid URL.
1190    * @throws IOException Error reading subordinate catalog file.
1191    */
1192   public String resolveDoctype(String entityName,
1193                                String publicId,
1194                                String systemId)
1195     throws MalformedURLException, IOException {
1196     String resolved = null;
1197 
1198     catalogManager.debug.message(3, "resolveDoctype("
1199                   +entityName+","+publicId+","+systemId+")");
1200 
1201     systemId = normalizeURI(systemId);
1202 
1203     if (publicId != null && publicId.startsWith("urn:publicid:")) {
1204       publicId = PublicId.decodeURN(publicId);
1205     }
1206 
1207     if (systemId != null && systemId.startsWith("urn:publicid:")) {
1208       systemId = PublicId.decodeURN(systemId);
1209       if (publicId != null && !publicId.equals(systemId)) {
1210         catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
1211         systemId = null;
1212       } else {
1213         publicId = systemId;
1214         systemId = null;
1215       }
1216     }
1217 
1218     if (systemId != null) {
1219       // If there's a SYSTEM entry in this catalog, use it
1220       resolved = resolveLocalSystem(systemId);
1221       if (resolved != null) {
1222         return resolved;
1223       }
1224     }
1225 
1226     if (publicId != null) {
1227       // If there's a PUBLIC entry in this catalog, use it
1228       resolved = resolveLocalPublic(DOCTYPE,
1229                                     entityName,
1230                                     publicId,
1231                                     systemId);
1232       if (resolved != null) {
1233         return resolved;
1234       }
1235     }
1236 
1237     // If there's a DOCTYPE entry in this catalog, use it
1238     boolean over = default_override;
1239     Enumeration en = catalogEntries.elements();
1240     while (en.hasMoreElements()) {
1241       CatalogEntry e = (CatalogEntry) en.nextElement();
1242       if (e.getEntryType() == OVERRIDE) {
1243         over = e.getEntryArg(0).equalsIgnoreCase("YES");
1244         continue;
1245       }
1246 
1247       if (e.getEntryType() == DOCTYPE
1248           && e.getEntryArg(0).equals(entityName)) {
1249         if (over || systemId == null) {
1250           return e.getEntryArg(1);
1251         }
1252       }
1253     }
1254 
1255     // Otherwise, look in the subordinate catalogs
1256     return resolveSubordinateCatalogs(DOCTYPE,
1257                                       entityName,
1258                                       publicId,
1259                                       systemId);
1260   }
1261 
1262   /**
1263    * Return the applicable DOCUMENT entry.
1264    *
1265    * @return The system identifier to use for the doctype.
1266    *
1267    * @throws MalformedURLException The formal system identifier of a
1268    * subordinate catalog cannot be turned into a valid URL.
1269    * @throws IOException Error reading subordinate catalog file.
1270    */
1271   public String resolveDocument()
1272     throws MalformedURLException, IOException {
1273     // If there's a DOCUMENT entry, return it
1274 
1275     catalogManager.debug.message(3, "resolveDocument");
1276 
1277     Enumeration en = catalogEntries.elements();
1278     while (en.hasMoreElements()) {
1279       CatalogEntry e = (CatalogEntry) en.nextElement();
1280       if (e.getEntryType() == DOCUMENT) {
1281         return e.getEntryArg(0);
1282       }
1283     }
1284 
1285     return resolveSubordinateCatalogs(DOCUMENT,
1286                                       null, null, null);
1287   }
1288 
1289   /**
1290    * Return the applicable ENTITY system identifier.
1291    *
1292    * @param entityName The name of the entity for which
1293    * a system identifier is required.
1294    * @param publicId The nominal public identifier for the entity
1295    * (as provided in the source document).
1296    * @param systemId The nominal system identifier for the entity
1297    * (as provided in the source document).
1298    *
1299    * @return The system identifier to use for the entity.
1300    *
1301    * @throws MalformedURLException The formal system identifier of a
1302    * subordinate catalog cannot be turned into a valid URL.
1303    * @throws IOException Error reading subordinate catalog file.
1304    */
1305   public String resolveEntity(String entityName,
1306                               String publicId,
1307                               String systemId)
1308     throws MalformedURLException, IOException {
1309     String resolved = null;
1310 
1311     catalogManager.debug.message(3, "resolveEntity("
1312                   +entityName+","+publicId+","+systemId+")");
1313 
1314     systemId = normalizeURI(systemId);
1315 
1316     if (publicId != null && publicId.startsWith("urn:publicid:")) {
1317       publicId = PublicId.decodeURN(publicId);
1318     }
1319 
1320     if (systemId != null && systemId.startsWith("urn:publicid:")) {
1321       systemId = PublicId.decodeURN(systemId);
1322       if (publicId != null && !publicId.equals(systemId)) {
1323         catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
1324         systemId = null;
1325       } else {
1326         publicId = systemId;
1327         systemId = null;
1328       }
1329     }
1330 
1331     if (systemId != null) {
1332       // If there's a SYSTEM entry in this catalog, use it
1333       resolved = resolveLocalSystem(systemId);
1334       if (resolved != null) {
1335         return resolved;
1336       }
1337     }
1338 
1339     if (publicId != null) {
1340       // If there's a PUBLIC entry in this catalog, use it
1341       resolved = resolveLocalPublic(ENTITY,
1342                                     entityName,
1343                                     publicId,
1344                                     systemId);
1345       if (resolved != null) {
1346         return resolved;
1347       }
1348     }
1349 
1350     // If there's a ENTITY entry in this catalog, use it
1351     boolean over = default_override;
1352     Enumeration en = catalogEntries.elements();
1353     while (en.hasMoreElements()) {
1354       CatalogEntry e = (CatalogEntry) en.nextElement();
1355       if (e.getEntryType() == OVERRIDE) {
1356         over = e.getEntryArg(0).equalsIgnoreCase("YES");
1357         continue;
1358       }
1359 
1360       if (e.getEntryType() == ENTITY
1361           && e.getEntryArg(0).equals(entityName)) {
1362         if (over || systemId == null) {
1363           return e.getEntryArg(1);
1364         }
1365       }
1366     }
1367 
1368     // Otherwise, look in the subordinate catalogs
1369     return resolveSubordinateCatalogs(ENTITY,
1370                                       entityName,
1371                                       publicId,
1372                                       systemId);
1373   }
1374 
1375   /**
1376    * Return the applicable NOTATION system identifier.
1377    *
1378    * @param notationName The name of the notation for which
1379    * a doctype is required.
1380    * @param publicId The nominal public identifier for the notation
1381    * (as provided in the source document).
1382    * @param systemId The nominal system identifier for the notation
1383    * (as provided in the source document).
1384    *
1385    * @return The system identifier to use for the notation.
1386    *
1387    * @throws MalformedURLException The formal system identifier of a
1388    * subordinate catalog cannot be turned into a valid URL.
1389    * @throws IOException Error reading subordinate catalog file.
1390    */
1391   public String resolveNotation(String notationName,
1392                                 String publicId,
1393                                 String systemId)
1394     throws MalformedURLException, IOException {
1395     String resolved = null;
1396 
1397     catalogManager.debug.message(3, "resolveNotation("
1398                   +notationName+","+publicId+","+systemId+")");
1399 
1400     systemId = normalizeURI(systemId);
1401 
1402     if (publicId != null && publicId.startsWith("urn:publicid:")) {
1403       publicId = PublicId.decodeURN(publicId);
1404     }
1405 
1406     if (systemId != null && systemId.startsWith("urn:publicid:")) {
1407       systemId = PublicId.decodeURN(systemId);
1408       if (publicId != null && !publicId.equals(systemId)) {
1409         catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
1410         systemId = null;
1411       } else {
1412         publicId = systemId;
1413         systemId = null;
1414       }
1415     }
1416 
1417     if (systemId != null) {
1418       // If there's a SYSTEM entry in this catalog, use it
1419       resolved = resolveLocalSystem(systemId);
1420       if (resolved != null) {
1421         return resolved;
1422       }
1423     }
1424 
1425     if (publicId != null) {
1426       // If there's a PUBLIC entry in this catalog, use it
1427       resolved = resolveLocalPublic(NOTATION,
1428                                     notationName,
1429                                     publicId,
1430                                     systemId);
1431       if (resolved != null) {
1432         return resolved;
1433       }
1434     }
1435 
1436     // If there's a NOTATION entry in this catalog, use it
1437     boolean over = default_override;
1438     Enumeration en = catalogEntries.elements();
1439     while (en.hasMoreElements()) {
1440       CatalogEntry e = (CatalogEntry) en.nextElement();
1441       if (e.getEntryType() == OVERRIDE) {
1442         over = e.getEntryArg(0).equalsIgnoreCase("YES");
1443         continue;
1444       }
1445 
1446       if (e.getEntryType() == NOTATION
1447           && e.getEntryArg(0).equals(notationName)) {
1448         if (over || systemId == null) {
1449           return e.getEntryArg(1);
1450         }
1451       }
1452     }
1453 
1454     // Otherwise, look in the subordinate catalogs
1455     return resolveSubordinateCatalogs(NOTATION,
1456                                       notationName,
1457                                       publicId,
1458                                       systemId);
1459   }
1460 
1461   /**
1462    * Return the applicable PUBLIC or SYSTEM identifier.
1463    *
1464    * <p>This method searches the Catalog and returns the system
1465    * identifier specified for the given system or
1466    * public identifiers. If
1467    * no appropriate PUBLIC or SYSTEM entry is found in the Catalog,
1468    * null is returned.</p>
1469    *
1470    * @param publicId The public identifier to locate in the catalog.
1471    * Public identifiers are normalized before comparison.
1472    * @param systemId The nominal system identifier for the entity
1473    * in question (as provided in the source document).
1474    *
1475    * @throws MalformedURLException The formal system identifier of a
1476    * subordinate catalog cannot be turned into a valid URL.
1477    * @throws IOException Error reading subordinate catalog file.
1478    *
1479    * @return The system identifier to use.
1480    * Note that the nominal system identifier is not returned if a
1481    * match is not found in the catalog, instead null is returned
1482    * to indicate that no match was found.
1483    */
1484   public String resolvePublic(String publicId, String systemId)
1485     throws MalformedURLException, IOException {
1486 
1487     catalogManager.debug.message(3, "resolvePublic("+publicId+","+systemId+")");
1488 
1489     systemId = normalizeURI(systemId);
1490 
1491     if (publicId != null && publicId.startsWith("urn:publicid:")) {
1492       publicId = PublicId.decodeURN(publicId);
1493     }
1494 
1495     if (systemId != null && systemId.startsWith("urn:publicid:")) {
1496       systemId = PublicId.decodeURN(systemId);
1497       if (publicId != null && !publicId.equals(systemId)) {
1498         catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
1499         systemId = null;
1500       } else {
1501         publicId = systemId;
1502         systemId = null;
1503       }
1504     }
1505 
1506     // If there's a SYSTEM entry in this catalog, use it
1507     if (systemId != null) {
1508       String resolved = resolveLocalSystem(systemId);
1509       if (resolved != null) {
1510         return resolved;
1511       }
1512     }
1513 
1514     // If there's a PUBLIC entry in this catalog, use it
1515     String resolved = resolveLocalPublic(PUBLIC,
1516                                          null,
1517                                          publicId,
1518                                          systemId);
1519     if (resolved != null) {
1520       return resolved;
1521     }
1522 
1523     // Otherwise, look in the subordinate catalogs
1524     return resolveSubordinateCatalogs(PUBLIC,
1525                                       null,
1526                                       publicId,
1527                                       systemId);
1528   }
1529 
1530   /**
1531    * Return the applicable PUBLIC or SYSTEM identifier.
1532    *
1533    * <p>This method searches the Catalog and returns the system
1534    * identifier specified for the given system or public identifiers.
1535    * If no appropriate PUBLIC or SYSTEM entry is found in the Catalog,
1536    * delegated Catalogs are interrogated.</p>
1537    *
1538    * <p>There are four possible cases:</p>
1539    *
1540    * <ul>
1541    * <li>If the system identifier provided matches a SYSTEM entry
1542    * in the current catalog, the SYSTEM entry is returned.
1543    * <li>If the system identifier is not null, the PUBLIC entries
1544    * that were encountered when OVERRIDE YES was in effect are
1545    * interrogated and the first matching entry is returned.</li>
1546    * <li>If the system identifier is null, then all of the PUBLIC
1547    * entries are interrogated and the first matching entry
1548    * is returned. This may not be the same as the preceding case, if
1549    * some PUBLIC entries are encountered when OVERRIDE NO is in effect. In
1550    * XML, the only place where a public identifier may occur without
1551    * a system identifier is in a notation declaration.</li>
1552    * <li>Finally, if the public identifier matches one of the partial
1553    * public identifiers specified in a DELEGATE* entry in
1554    * the Catalog, the delegated catalog is interrogated. The first
1555    * time that the delegated catalog is required, it will be
1556    * retrieved and parsed. It is subsequently cached.
1557    * </li>
1558    * </ul>
1559    *
1560    * @param entityType The CatalogEntry type for which this query is
1561    * being conducted. This is necessary in order to do the approprate
1562    * query on a delegated catalog.
1563    * @param entityName The name of the entity being searched for, if
1564    * appropriate.
1565    * @param publicId The public identifier of the entity in question.
1566    * @param systemId The nominal system identifier for the entity
1567    * in question (as provided in the source document).
1568    *
1569    * @throws MalformedURLException The formal system identifier of a
1570    * delegated catalog cannot be turned into a valid URL.
1571    * @throws IOException Error reading delegated catalog file.
1572    *
1573    * @return The system identifier to use.
1574    * Note that the nominal system identifier is not returned if a
1575    * match is not found in the catalog, instead null is returned
1576    * to indicate that no match was found.
1577    */
1578   protected synchronized String resolveLocalPublic(int entityType,
1579                                                    String entityName,
1580                                                    String publicId,
1581                                                    String systemId)
1582     throws MalformedURLException, IOException {
1583 
1584     // Always normalize the public identifier before attempting a match
1585     publicId = PublicId.normalize(publicId);
1586 
1587     // If there's a SYSTEM entry in this catalog, use it
1588     if (systemId != null) {
1589       String resolved = resolveLocalSystem(systemId);
1590       if (resolved != null) {
1591         return resolved;
1592       }
1593     }
1594 
1595     // If there's a PUBLIC entry in this catalog, use it
1596     boolean over = default_override;
1597     Enumeration en = catalogEntries.elements();
1598     while (en.hasMoreElements()) {
1599       CatalogEntry e = (CatalogEntry) en.nextElement();
1600       if (e.getEntryType() == OVERRIDE) {
1601         over = e.getEntryArg(0).equalsIgnoreCase("YES");
1602         continue;
1603       }
1604 
1605       if (e.getEntryType() == PUBLIC
1606           && e.getEntryArg(0).equals(publicId)) {
1607         if (over || systemId == null) {
1608           return e.getEntryArg(1);
1609         }
1610       }
1611     }
1612 
1613     // If there's a DELEGATE_PUBLIC entry in this catalog, use it
1614     over = default_override;
1615     en = catalogEntries.elements();
1616     Vector delCats = new Vector();
1617     while (en.hasMoreElements()) {
1618       CatalogEntry e = (CatalogEntry) en.nextElement();
1619       if (e.getEntryType() == OVERRIDE) {
1620         over = e.getEntryArg(0).equalsIgnoreCase("YES");
1621         continue;
1622       }
1623 
1624       if (e.getEntryType() == DELEGATE_PUBLIC
1625           && (over || systemId == null)) {
1626         String p = (String) e.getEntryArg(0);
1627         if (p.length() <= publicId.length()
1628             && p.equals(publicId.substring(0, p.length()))) {
1629           // delegate this match to the other catalog
1630 
1631           delCats.addElement(e.getEntryArg(1));
1632         }
1633       }
1634     }
1635 
1636     if (delCats.size() > 0) {
1637       Enumeration enCats = delCats.elements();
1638 
1639       if (catalogManager.debug.getDebug() > 1) {
1640         catalogManager.debug.message(2, "Switching to delegated catalog(s):");
1641         while (enCats.hasMoreElements()) {
1642           String delegatedCatalog = (String) enCats.nextElement();
1643           catalogManager.debug.message(2, "\t" + delegatedCatalog);
1644         }
1645       }
1646 
1647       Catalog dcat = newCatalog();
1648 
1649       enCats = delCats.elements();
1650       while (enCats.hasMoreElements()) {
1651         String delegatedCatalog = (String) enCats.nextElement();
1652         dcat.parseCatalog(delegatedCatalog);
1653       }
1654 
1655       return dcat.resolvePublic(publicId, null);
1656     }
1657 
1658     // Nada!
1659     return null;
1660   }
1661 
1662   /**
1663    * Return the applicable SYSTEM system identifier.
1664    *
1665    * <p>If a SYSTEM entry exists in the Catalog
1666    * for the system ID specified, return the mapped value.</p>
1667    *
1668    * <p>On Windows-based operating systems, the comparison between
1669    * the system identifier provided and the SYSTEM entries in the
1670    * Catalog is case-insensitive.</p>
1671    *
1672    * @param systemId The system ID to locate in the catalog.
1673    *
1674    * @return The resolved system identifier.
1675    *
1676    * @throws MalformedURLException The formal system identifier of a
1677    * subordinate catalog cannot be turned into a valid URL.
1678    * @throws IOException Error reading subordinate catalog file.
1679    */
1680   public String resolveSystem(String systemId)
1681     throws MalformedURLException, IOException {
1682 
1683     catalogManager.debug.message(3, "resolveSystem("+systemId+")");
1684 
1685     systemId = normalizeURI(systemId);
1686 
1687     if (systemId != null && systemId.startsWith("urn:publicid:")) {
1688       systemId = PublicId.decodeURN(systemId);
1689       return resolvePublic(systemId, null);
1690     }
1691 
1692     // If there's a SYSTEM entry in this catalog, use it
1693     if (systemId != null) {
1694       String resolved = resolveLocalSystem(systemId);
1695       if (resolved != null) {
1696         return resolved;
1697       }
1698     }
1699 
1700     // Otherwise, look in the subordinate catalogs
1701     return resolveSubordinateCatalogs(SYSTEM,
1702                                       null,
1703                                       null,
1704                                       systemId);
1705   }
1706 
1707   /**
1708    * Return the applicable SYSTEM system identifier in this
1709    * catalog.
1710    *
1711    * <p>If a SYSTEM entry exists in the catalog file
1712    * for the system ID specified, return the mapped value.</p>
1713    *
1714    * @param systemId The system ID to locate in the catalog
1715    *
1716    * @return The mapped system identifier or null
1717    */
1718   protected String resolveLocalSystem(String systemId)
1719     throws MalformedURLException, IOException {
1720 
1721     String osname = SecuritySupport.getSystemProperty("os.name");
1722     boolean windows = (osname.indexOf("Windows") >= 0);
1723     Enumeration en = catalogEntries.elements();
1724     while (en.hasMoreElements()) {
1725       CatalogEntry e = (CatalogEntry) en.nextElement();
1726       if (e.getEntryType() == SYSTEM
1727           && (e.getEntryArg(0).equals(systemId)
1728               || (windows
1729                   && e.getEntryArg(0).equalsIgnoreCase(systemId)))) {
1730         return e.getEntryArg(1);
1731       }
1732     }
1733 
1734     // If there's a REWRITE_SYSTEM entry in this catalog, use it
1735     en = catalogEntries.elements();
1736     String startString = null;
1737     String prefix = null;
1738     while (en.hasMoreElements()) {
1739       CatalogEntry e = (CatalogEntry) en.nextElement();
1740 
1741       if (e.getEntryType() == REWRITE_SYSTEM) {
1742         String p = (String) e.getEntryArg(0);
1743         if (p.length() <= systemId.length()
1744             && p.equals(systemId.substring(0, p.length()))) {
1745           // Is this the longest prefix?
1746           if (startString == null
1747               || p.length() > startString.length()) {
1748             startString = p;
1749             prefix = e.getEntryArg(1);
1750           }
1751         }
1752       }
1753     }
1754 
1755     if (prefix != null) {
1756       // return the systemId with the new prefix
1757       return prefix + systemId.substring(startString.length());
1758     }
1759 
1760     // If there's a SYSTEM_SUFFIX entry in this catalog, use it
1761     en = catalogEntries.elements();
1762     String suffixString = null;
1763     String suffixURI = null;
1764     while (en.hasMoreElements()) {
1765       CatalogEntry e = (CatalogEntry) en.nextElement();
1766 
1767       if (e.getEntryType() == SYSTEM_SUFFIX) {
1768         String p = (String) e.getEntryArg(0);
1769         if (p.length() <= systemId.length()
1770             && systemId.endsWith(p)) {
1771           // Is this the longest prefix?
1772           if (suffixString == null
1773               || p.length() > suffixString.length()) {
1774             suffixString = p;
1775             suffixURI = e.getEntryArg(1);
1776           }
1777         }
1778       }
1779     }
1780 
1781     if (suffixURI != null) {
1782       // return the systemId for the suffix
1783       return suffixURI;
1784     }
1785 
1786     // If there's a DELEGATE_SYSTEM entry in this catalog, use it
1787     en = catalogEntries.elements();
1788     Vector delCats = new Vector();
1789     while (en.hasMoreElements()) {
1790       CatalogEntry e = (CatalogEntry) en.nextElement();
1791 
1792       if (e.getEntryType() == DELEGATE_SYSTEM) {
1793         String p = (String) e.getEntryArg(0);
1794         if (p.length() <= systemId.length()
1795             && p.equals(systemId.substring(0, p.length()))) {
1796           // delegate this match to the other catalog
1797 
1798           delCats.addElement(e.getEntryArg(1));
1799         }
1800       }
1801     }
1802 
1803     if (delCats.size() > 0) {
1804       Enumeration enCats = delCats.elements();
1805 
1806       if (catalogManager.debug.getDebug() > 1) {
1807         catalogManager.debug.message(2, "Switching to delegated catalog(s):");
1808         while (enCats.hasMoreElements()) {
1809           String delegatedCatalog = (String) enCats.nextElement();
1810           catalogManager.debug.message(2, "\t" + delegatedCatalog);
1811         }
1812       }
1813 
1814       Catalog dcat = newCatalog();
1815 
1816       enCats = delCats.elements();
1817       while (enCats.hasMoreElements()) {
1818         String delegatedCatalog = (String) enCats.nextElement();
1819         dcat.parseCatalog(delegatedCatalog);
1820       }
1821 
1822       return dcat.resolveSystem(systemId);
1823     }
1824 
1825     return null;
1826   }
1827 
1828   /**
1829    * Return the applicable URI.
1830    *
1831    * <p>If a URI entry exists in the Catalog
1832    * for the URI specified, return the mapped value.</p>
1833    *
1834    * <p>URI comparison is case sensitive.</p>
1835    *
1836    * @param uri The URI to locate in the catalog.
1837    *
1838    * @return The resolved URI.
1839    *
1840    * @throws MalformedURLException The system identifier of a
1841    * subordinate catalog cannot be turned into a valid URL.
1842    * @throws IOException Error reading subordinate catalog file.
1843    */
1844   public String resolveURI(String uri)
1845     throws MalformedURLException, IOException {
1846 
1847     catalogManager.debug.message(3, "resolveURI("+uri+")");
1848 
1849     uri = normalizeURI(uri);
1850 
1851     if (uri != null && uri.startsWith("urn:publicid:")) {
1852       uri = PublicId.decodeURN(uri);
1853       return resolvePublic(uri, null);
1854     }
1855 
1856     // If there's a URI entry in this catalog, use it
1857     if (uri != null) {
1858       String resolved = resolveLocalURI(uri);
1859       if (resolved != null) {
1860         return resolved;
1861       }
1862     }
1863 
1864     // Otherwise, look in the subordinate catalogs
1865     return resolveSubordinateCatalogs(URI,
1866                                       null,
1867                                       null,
1868                                       uri);
1869   }
1870 
1871   /**
1872    * Return the applicable URI in this catalog.
1873    *
1874    * <p>If a URI entry exists in the catalog file
1875    * for the URI specified, return the mapped value.</p>
1876    *
1877    * @param uri The URI to locate in the catalog
1878    *
1879    * @return The mapped URI or null
1880    */
1881   protected String resolveLocalURI(String uri)
1882     throws MalformedURLException, IOException {
1883     Enumeration en = catalogEntries.elements();
1884     while (en.hasMoreElements()) {
1885       CatalogEntry e = (CatalogEntry) en.nextElement();
1886       if (e.getEntryType() == URI
1887           && (e.getEntryArg(0).equals(uri))) {
1888         return e.getEntryArg(1);
1889       }
1890     }
1891 
1892     // If there's a REWRITE_URI entry in this catalog, use it
1893     en = catalogEntries.elements();
1894     String startString = null;
1895     String prefix = null;
1896     while (en.hasMoreElements()) {
1897       CatalogEntry e = (CatalogEntry) en.nextElement();
1898 
1899       if (e.getEntryType() == REWRITE_URI) {
1900         String p = (String) e.getEntryArg(0);
1901         if (p.length() <= uri.length()
1902             && p.equals(uri.substring(0, p.length()))) {
1903           // Is this the longest prefix?
1904           if (startString == null
1905               || p.length() > startString.length()) {
1906             startString = p;
1907             prefix = e.getEntryArg(1);
1908           }
1909         }
1910       }
1911     }
1912 
1913     if (prefix != null) {
1914       // return the uri with the new prefix
1915       return prefix + uri.substring(startString.length());
1916     }
1917 
1918     // If there's a URI_SUFFIX entry in this catalog, use it
1919     en = catalogEntries.elements();
1920     String suffixString = null;
1921     String suffixURI = null;
1922     while (en.hasMoreElements()) {
1923       CatalogEntry e = (CatalogEntry) en.nextElement();
1924 
1925       if (e.getEntryType() == URI_SUFFIX) {
1926         String p = (String) e.getEntryArg(0);
1927         if (p.length() <= uri.length()
1928             && uri.endsWith(p)) {
1929           // Is this the longest prefix?
1930           if (suffixString == null
1931               || p.length() > suffixString.length()) {
1932             suffixString = p;
1933             suffixURI = e.getEntryArg(1);
1934           }
1935         }
1936       }
1937     }
1938 
1939     if (suffixURI != null) {
1940       // return the uri for the suffix
1941       return suffixURI;
1942     }
1943 
1944     // If there's a DELEGATE_URI entry in this catalog, use it
1945     en = catalogEntries.elements();
1946     Vector delCats = new Vector();
1947     while (en.hasMoreElements()) {
1948       CatalogEntry e = (CatalogEntry) en.nextElement();
1949 
1950       if (e.getEntryType() == DELEGATE_URI) {
1951         String p = (String) e.getEntryArg(0);
1952         if (p.length() <= uri.length()
1953             && p.equals(uri.substring(0, p.length()))) {
1954           // delegate this match to the other catalog
1955 
1956           delCats.addElement(e.getEntryArg(1));
1957         }
1958       }
1959     }
1960 
1961     if (delCats.size() > 0) {
1962       Enumeration enCats = delCats.elements();
1963 
1964       if (catalogManager.debug.getDebug() > 1) {
1965         catalogManager.debug.message(2, "Switching to delegated catalog(s):");
1966         while (enCats.hasMoreElements()) {
1967           String delegatedCatalog = (String) enCats.nextElement();
1968           catalogManager.debug.message(2, "\t" + delegatedCatalog);
1969         }
1970       }
1971 
1972       Catalog dcat = newCatalog();
1973 
1974       enCats = delCats.elements();
1975       while (enCats.hasMoreElements()) {
1976         String delegatedCatalog = (String) enCats.nextElement();
1977         dcat.parseCatalog(delegatedCatalog);
1978       }
1979 
1980       return dcat.resolveURI(uri);
1981     }
1982 
1983     return null;
1984   }
1985 
1986   /**
1987    * Search the subordinate catalogs, in order, looking for a match.
1988    *
1989    * <p>This method searches the Catalog and returns the system
1990    * identifier specified for the given entity type with the given
1991    * name, public, and system identifiers. In some contexts, these
1992    * may be null.</p>
1993    *
1994    * @param entityType The CatalogEntry type for which this query is
1995    * being conducted. This is necessary in order to do the approprate
1996    * query on a subordinate catalog.
1997    * @param entityName The name of the entity being searched for, if
1998    * appropriate.
1999    * @param publicId The public identifier of the entity in question
2000    * (as provided in the source document).
2001    * @param systemId The nominal system identifier for the entity
2002    * in question (as provided in the source document). This parameter is
2003    * overloaded for the URI entry type.
2004    *
2005    * @throws MalformedURLException The formal system identifier of a
2006    * delegated catalog cannot be turned into a valid URL.
2007    * @throws IOException Error reading delegated catalog file.
2008    *
2009    * @return The system identifier to use.
2010    * Note that the nominal system identifier is not returned if a
2011    * match is not found in the catalog, instead null is returned
2012    * to indicate that no match was found.
2013    */
2014   protected synchronized String resolveSubordinateCatalogs(int entityType,
2015                                                            String entityName,
2016                                                            String publicId,
2017                                                            String systemId)
2018     throws MalformedURLException, IOException {
2019 
2020     for (int catPos = 0; catPos < catalogs.size(); catPos++) {
2021       Catalog c = null;
2022 
2023       try {
2024         c = (Catalog) catalogs.elementAt(catPos);
2025       } catch (ClassCastException e) {
2026         String catfile = (String) catalogs.elementAt(catPos);
2027         c = newCatalog();
2028 
2029         try {
2030           c.parseCatalog(catfile);
2031         } catch (MalformedURLException mue) {
2032           catalogManager.debug.message(1, "Malformed Catalog URL", catfile);
2033         } catch (FileNotFoundException fnfe) {
2034           catalogManager.debug.message(1, "Failed to load catalog, file not found",
2035                         catfile);
2036         } catch (IOException ioe) {
2037           catalogManager.debug.message(1, "Failed to load catalog, I/O error", catfile);
2038         }
2039 
2040         catalogs.setElementAt(c, catPos);
2041       }
2042 
2043       String resolved = null;
2044 
2045       // Ok, now what are we supposed to call here?
2046       if (entityType == DOCTYPE) {
2047         resolved = c.resolveDoctype(entityName,
2048                                     publicId,
2049                                     systemId);
2050       } else if (entityType == DOCUMENT) {
2051         resolved = c.resolveDocument();
2052       } else if (entityType == ENTITY) {
2053         resolved = c.resolveEntity(entityName,
2054                                    publicId,
2055                                    systemId);
2056       } else if (entityType == NOTATION) {
2057         resolved = c.resolveNotation(entityName,
2058                                      publicId,
2059                                      systemId);
2060       } else if (entityType == PUBLIC) {
2061         resolved = c.resolvePublic(publicId, systemId);
2062       } else if (entityType == SYSTEM) {
2063         resolved = c.resolveSystem(systemId);
2064       } else if (entityType == URI) {
2065         resolved = c.resolveURI(systemId);
2066       }
2067 
2068       if (resolved != null) {
2069         return resolved;
2070       }
2071     }
2072 
2073     return null;
2074   }
2075 
2076   // -----------------------------------------------------------------
2077 
2078   /**
2079    * Replace backslashes with forward slashes. (URLs always use
2080    * forward slashes.)
2081    *
2082    * @param sysid The input system identifier.
2083    * @return The same system identifier with backslashes turned into
2084    * forward slashes.
2085    */
2086   protected String fixSlashes (String sysid) {
2087     return sysid.replace('\\', '/');
2088   }
2089 
2090   /**
2091    * Construct an absolute URI from a relative one, using the current
2092    * base URI.
2093    *
2094    * @param sysid The (possibly relative) system identifier
2095    * @return The system identifier made absolute with respect to the
2096    * current {@link #base}.
2097    */
2098   protected String makeAbsolute(String sysid) {
2099     URL local = null;
2100 
2101     sysid = fixSlashes(sysid);
2102 
2103     try {
2104       local = new URL(base, sysid);
2105     } catch (MalformedURLException e) {
2106       catalogManager.debug.message(1, "Malformed URL on system identifier", sysid);
2107     }
2108 
2109     if (local != null) {
2110       return local.toString();
2111     } else {
2112       return sysid;
2113     }
2114   }
2115 
2116   /**
2117    * Perform character normalization on a URI reference.
2118    *
2119    * @param uriref The URI reference
2120    * @return The normalized URI reference.
2121    */
2122   protected String normalizeURI(String uriref) {
2123     if (uriref == null) {
2124       return null;
2125     }
2126 
2127     byte[] bytes;
2128     try {
2129       bytes = uriref.getBytes("UTF-8");
2130     } catch (UnsupportedEncodingException uee) {
2131       // this can't happen
2132       catalogManager.debug.message(1, "UTF-8 is an unsupported encoding!?");
2133       return uriref;
2134     }
2135 
2136     StringBuilder newRef = new StringBuilder(bytes.length);
2137     for (int count = 0; count < bytes.length; count++) {
2138       int ch = bytes[count] & 0xFF;
2139 
2140       if ((ch <= 0x20)    // ctrl
2141           || (ch > 0x7F)  // high ascii
2142           || (ch == 0x22) // "
2143           || (ch == 0x3C) // <
2144           || (ch == 0x3E) // >
2145           || (ch == 0x5C) // \
2146           || (ch == 0x5E) // ^
2147           || (ch == 0x60) // `
2148           || (ch == 0x7B) // {
2149           || (ch == 0x7C) // |
2150           || (ch == 0x7D) // }
2151           || (ch == 0x7F)) {
2152         newRef.append(encodedByte(ch));
2153       } else {
2154         newRef.append((char) bytes[count]);
2155       }
2156     }
2157 
2158     return newRef.toString();
2159   }
2160 
2161   /**
2162    * Perform %-encoding on a single byte.
2163    *
2164    * @param b The 8-bit integer that represents th byte. (Bytes are signed
2165               but encoding needs to look at the bytes unsigned.)
2166    * @return The %-encoded string for the byte in question.
2167    */
2168   protected String encodedByte (int b) {
2169     String hex = Integer.toHexString(b).toUpperCase();
2170     if (hex.length() < 2) {
2171       return "%0" + hex;
2172     } else {
2173       return "%" + hex;
2174     }
2175   }
2176 
2177   // -----------------------------------------------------------------
2178 
2179   /**
2180    * Add to the current list of delegated catalogs.
2181    *
2182    * <p>This method always constructs the {@link #localDelegate}
2183    * vector so that it is ordered by length of partial
2184    * public identifier.</p>
2185    *
2186    * @param entry The DELEGATE catalog entry
2187    */
2188   protected void addDelegate(CatalogEntry entry) {
2189     int pos = 0;
2190     String partial = entry.getEntryArg(0);
2191 
2192     Enumeration local = localDelegate.elements();
2193     while (local.hasMoreElements()) {
2194       CatalogEntry dpe = (CatalogEntry) local.nextElement();
2195       String dp = dpe.getEntryArg(0);
2196       if (dp.equals(partial)) {
2197         // we already have this prefix
2198         return;
2199       }
2200       if (dp.length() > partial.length()) {
2201         pos++;
2202       }
2203       if (dp.length() < partial.length()) {
2204         break;
2205       }
2206     }
2207 
2208     // now insert partial into the vector at [pos]
2209     if (localDelegate.size() == 0) {
2210       localDelegate.addElement(entry);
2211     } else {
2212       localDelegate.insertElementAt(entry, pos);
2213     }
2214   }
2215 }