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