content: // Catalog.java - Represents OASIS Open Catalog files.
  
  /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   * 
   *      http://www.apache.org/licenses/LICENSE-2.0
   * 
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   *
   */
  
  package mf.org.apache.xml.resolver;
  
  import java.io.DataInputStream;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.UnsupportedEncodingException;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.util.Enumeration;
  import java.util.Hashtable;
  import java.util.Vector;
  
  import mf.javax.xml.parsers.SAXParserFactory;
  import mf.org.apache.xml.resolver.helpers.FileURL;
  import mf.org.apache.xml.resolver.helpers.PublicId;
  import mf.org.apache.xml.resolver.readers.CatalogReader;
  import mf.org.apache.xml.resolver.readers.OASISXMLCatalogReader;
  import mf.org.apache.xml.resolver.readers.SAXCatalogReader;
  import mf.org.apache.xml.resolver.readers.TR9401CatalogReader;
  
  /**
   * Represents OASIS Open Catalog files.
   * <p/>
   * <p>This class implements the semantics of OASIS Open Catalog files
   * (defined by
   * <a href="http://www.oasis-open.org/html/a401.htm">OASIS Technical
   * Resolution 9401:1997 (Amendment 2 to TR 9401)</a>).</p>
   * <p/>
   * <p>The primary purpose of the Catalog is to associate resources in the
   * document with local system identifiers. Some entities
   * (document types, XML entities, and notations) have names and all of them
   * can have either public or system identifiers or both. (In XML, only a
   * notation can have a public identifier without a system identifier, but
   * the methods implemented in this class obey the Catalog semantics
   * from the SGML
   * days when system identifiers were optional.)</p>
   * <p/>
   * <p>The system identifiers returned by the resolution methods in this
   * class are valid, i.e. usable by, and in fact constructed by, the
   * <tt>java.net.URL</tt> class. Unfortunately, this class seems to behave in
   * somewhat non-standard ways and the system identifiers returned may
   * not be directly usable in a browser or filesystem context.
   * <p/>
   * <p>This class recognizes all of the Catalog entries defined in
   * TR9401:1997:</p>
   * <p/>
   * <ul>
   * <li><b>BASE</b>
   * changes the base URI for resolving relative system identifiers. The
   * initial base URI is the URI of the location of the catalog (which is,
   * in turn, relative to the location of the current working directory
   * at startup, as returned by the <tt>user.dir</tt> system property).</li>
   * <li><b>CATALOG</b>
   * processes other catalog files. An included catalog occurs logically
   * at the end of the including catalog.</li>
   * <li><b>DELEGATE_PUBLIC</b>
   * specifies alternate catalogs for some public identifiers. The delegated
   * catalogs are not loaded until they are needed, but they are cached
   * once loaded.</li>
   * <li><b>DELEGATE_SYSTEM</b>
   * specifies alternate catalogs for some system identifiers. The delegated
   * catalogs are not loaded until they are needed, but they are cached
   * once loaded.</li>
   * <li><b>DELEGATE_URI</b>
   * specifies alternate catalogs for some URIs. The delegated
   * catalogs are not loaded until they are needed, but they are cached
   * once loaded.</li>
   * <li><b>REWRITE_SYSTEM</b>
   * specifies alternate prefix for a system identifier.</li>
   * <li><b>REWRITE_URI</b>
   * specifies alternate prefix for a URI.</li>
   * <li><b>SYSTEM_SUFFIX</b>
   * maps any system identifier that ends with a particular suffix to another
   * system identifier.</li>
   * <li><b>URI_SUFFIX</b>
   * maps any URI that ends with a particular suffix to another URI.</li>
   * <li><b>DOCTYPE</b>
   * associates the names of root elements with URIs. (In other words, an XML
   * processor might infer the doctype of an XML document that does not include
   * a doctype declaration by looking for the DOCTYPE entry in the
   * catalog which matches the name of the root element of the document.)</li>
   * <li><b>DOCUMENT</b>
   * provides a default document.</li>
   * <li><b>DTDDECL</b>
   * recognized and silently ignored. Not relevant for XML.</li>
   * <li><b>ENTITY</b>
   * associates entity names with URIs.</li>
   * <li><b>LINKTYPE</b>
   * recognized and silently ignored. Not relevant for XML.</li>
   * <li><b>NOTATION</b>
   * associates notation names with URIs.</li>
   * <li><b>OVERRIDE</b>
   * changes the override behavior. Initial behavior is set by the
   * system property <tt>xml.catalog.override</tt>. The default initial
   * behavior is 'YES', that is, entries in the catalog override
   * system identifiers specified in the document.</li>
   * <li><b>PUBLIC</b>
   * maps a public identifier to a system identifier.</li>
   * <li><b>SGMLDECL</b>
   * recognized and silently ignored. Not relevant for XML.</li>
   * <li><b>SYSTEM</b>
   * maps a system identifier to another system identifier.</li>
   * <li><b>URI</b>
   * maps a URI to another URI.</li>
   * </ul>
   * <p/>
   * <p>Note that BASE entries are treated as described by RFC2396. In
   * particular, this has the counter-intuitive property that after a BASE
   * entry identifing "http://example.com/a/b/c" as the base URI,
   * the relative URI "foo" is resolved to the absolute URI
   * "http://example.com/a/b/foo". You must provide the trailing slash if
   * you do not want the final component of the path to be discarded as a
   * filename would in a URI for a resource: "http://example.com/a/b/c/".
   * </p>
   * <p/>
   * <p>Note that subordinate catalogs (all catalogs except the first,
   * including CATALOG and DELEGATE* catalogs) are only loaded if and when
   * they are required.</p>
   * <p/>
   * <p>This class relies on classes which implement the CatalogReader
   * interface to actually load catalog files. This allows the catalog
   * semantics to be implemented for TR9401 text-based catalogs, XML
   * catalogs, or any number of other storage formats.</p>
   * <p/>
   * <p>Additional catalogs may also be loaded with the
   * {@link #parseCatalog} method.</p>
   * </dd>
   * </dl>
   * <p/>
   * <p><b>Change Log:</b></p>
   * <dl>
   * <dt>2.0</dt>
   * <dd><p>Rewrite to use CatalogReaders.</p></dd>
   * <dt>1.1</dt>
   * <dd><p>Allow quoted components in <tt>xml.catalog.files</tt>
   * so that URLs containing colons can be used on Unix.
   * The string passed to <tt>xml.catalog.files</tt> can now have the form:</p>
   * <pre>
   * unquoted-path-with-no-sep-chars:"double-quoted path with or without sep chars":'single-quoted path with or without sep chars'
   * </pre>
   * <p>(Where ":" is the separater character in this example.)</p>
   * <p>If an unquoted path contains an embedded double or single quote
   * character, no special processig is performed on that character. No
   * path can contain separater characters, double, and single quotes
   * simultaneously.</p>
   * <p>Fix bug in calculation of BASE entries: if
   * a catalog contains multiple BASE entries, each is relative to the preceding
   * base, not the default base URI of the catalog.</p>
   * </dd>
   * <dt>1.0.1</dt>
   * <dd><p>Fixed a bug in the calculation of the list of subordinate catalogs.
   * This bug caused an infinite loop where parsing would alternately process
   * two catalogs indefinitely.</p>
   * </dd>
   * </dl>
   *
   * @author Norman Walsh
   *         <a href="mailto:Norman.Walsh@Sun.COM">Norman.Walsh@Sun.COM</a>
   * @version 1.0
   *          <p/>
   *          <p>Derived from public domain code originally published by Arbortext,
   *          Inc.</p>
   * @see CatalogReader
   * @see CatalogEntry
   */
  public class Catalog {
      /**
       * The BASE Catalog Entry type.
       */
      public static final int BASE = CatalogEntry.addEntryType("BASE", 1);
  
      /**
       * The CATALOG Catalog Entry type.
       */
      public static final int CATALOG = CatalogEntry.addEntryType("CATALOG", 1);
  
      /**
       * The DOCUMENT Catalog Entry type.
       */
      public static final int DOCUMENT = CatalogEntry.addEntryType("DOCUMENT", 1);
  
      /**
       * The OVERRIDE Catalog Entry type.
       */
      public static final int OVERRIDE = CatalogEntry.addEntryType("OVERRIDE", 1);
  
      /**
       * The SGMLDECL Catalog Entry type.
       */
      public static final int SGMLDECL = CatalogEntry.addEntryType("SGMLDECL", 1);
  
      /**
       * The DELEGATE_PUBLIC Catalog Entry type.
       */
      public static final int DELEGATE_PUBLIC = CatalogEntry.addEntryType("DELEGATE_PUBLIC", 2);
  
      /**
       * The DELEGATE_SYSTEM Catalog Entry type.
       */
      public static final int DELEGATE_SYSTEM = CatalogEntry.addEntryType("DELEGATE_SYSTEM", 2);
  
      /**
       * The DELEGATE_URI Catalog Entry type.
       */
      public static final int DELEGATE_URI = CatalogEntry.addEntryType("DELEGATE_URI", 2);
  
      /**
       * The DOCTYPE Catalog Entry type.
       */
      public static final int DOCTYPE = CatalogEntry.addEntryType("DOCTYPE", 2);
  
      /**
       * The DTDDECL Catalog Entry type.
       */
      public static final int DTDDECL = CatalogEntry.addEntryType("DTDDECL", 2);
  
      /**
       * The ENTITY Catalog Entry type.
       */
      public static final int ENTITY = CatalogEntry.addEntryType("ENTITY", 2);
  
      /**
       * The LINKTYPE Catalog Entry type.
       */
      public static final int LINKTYPE = CatalogEntry.addEntryType("LINKTYPE", 2);
  
      /**
       * The NOTATION Catalog Entry type.
       */
      public static final int NOTATION = CatalogEntry.addEntryType("NOTATION", 2);
  
      /**
       * The PUBLIC Catalog Entry type.
       */
      public static final int PUBLIC = CatalogEntry.addEntryType("PUBLIC", 2);
  
      /**
       * The SYSTEM Catalog Entry type.
       */
      public static final int SYSTEM = CatalogEntry.addEntryType("SYSTEM", 2);
  
      /**
       * The URI Catalog Entry type.
       */
      public static final int URI = CatalogEntry.addEntryType("URI", 2);
  
      /**
       * The REWRITE_SYSTEM Catalog Entry type.
       */
      public static final int REWRITE_SYSTEM = CatalogEntry.addEntryType("REWRITE_SYSTEM", 2);
  
      /**
       * The REWRITE_URI Catalog Entry type.
       */
      public static final int REWRITE_URI = CatalogEntry.addEntryType("REWRITE_URI", 2);
      /**
       * The SYSTEM_SUFFIX Catalog Entry type.
       */
      public static final int SYSTEM_SUFFIX = CatalogEntry.addEntryType("SYSTEM_SUFFIX", 2);
      /**
       * The URI_SUFFIX Catalog Entry type.
       */
      public static final int URI_SUFFIX = CatalogEntry.addEntryType("URI_SUFFIX", 2);
  
      /**
       * The base URI for relative system identifiers in the catalog.
       * This may be changed by BASE entries in the catalog.
       */
      protected URL base;
  
      /**
       * The base URI of the Catalog file currently being parsed.
       */
      protected URL catalogCwd;
  
      /**
       * The catalog entries currently known to the system.
       */
      protected Vector catalogEntries = new Vector();
  
      /**
       * The default initial override setting.
       */
      protected boolean default_override = true;
  
      /**
       * The catalog manager in use for this instance.
       */
      protected CatalogManager catalogManager = CatalogManager.getStaticManager();
  
      /**
       * A vector of catalog files to be loaded.
       * <p/>
       * <p>This list is initially established by
       * <code>loadSystemCatalogs</code> when
       * it parses the system catalog list, but CATALOG entries may
       * contribute to it during the course of parsing.</p>
       *
       * @see #loadSystemCatalogs
       * @see #localCatalogFiles
       */
      protected Vector catalogFiles = new Vector();
  
      /**
       * A vector of catalog files constructed during processing of
       * CATALOG entries in the current catalog.
       * <p/>
       * <p>This two-level system is actually necessary to correctly implement
       * the semantics of the CATALOG entry. If one catalog file includes
       * another with a CATALOG entry, the included catalog logically
       * occurs <i>at the end</i> of the including catalog, and after any
       * preceding CATALOG entries. In other words, the CATALOG entry
       * cannot insert anything into the middle of a catalog file.</p>
       * <p/>
       * <p>When processing reaches the end of each catalog files, any
       * elements on this vector are added to the front of the
       * <code>catalogFiles</code> vector.</p>
       *
       * @see #catalogFiles
       */
      protected Vector localCatalogFiles = new Vector();
  
      /**
       * A vector of Catalogs.
       * <p/>
       * <p>The semantics of Catalog resolution are such that each
       * catalog is effectively a list of Catalogs (in other words,
       * a recursive list of Catalog instances).</p>
       * <p/>
       * <p>Catalogs that are processed as the result of CATALOG or
       * DELEGATE* entries are subordinate to the catalog that contained
       * them, but they may in turn have subordinate catalogs.</p>
       * <p/>
       * <p>Catalogs are only loaded when they are needed, so this vector
       * initially contains a list of Catalog filenames (URLs). If, during
       * processing, one of these catalogs has to be loaded, the resulting
       * Catalog object is placed in the vector, effectively caching it
       * for the next query.</p>
       */
      protected Vector catalogs = new Vector();
  
      /**
       * A vector of DELEGATE* Catalog entries constructed during
       * processing of the Catalog.
       * <p/>
       * <p>This two-level system has two purposes; first, it allows
       * us to sort the DELEGATE* entries by the length of the partial
       * public identifier so that a linear search encounters them in
       * the correct order and second, it puts them all at the end of
       * the Catalog.</p>
       * <p/>
       * <p>When processing reaches the end of each catalog file, any
       * elements on this vector are added to the end of the
       * <code>catalogEntries</code> vector. This assures that matching
       * PUBLIC keywords are encountered before DELEGATE* entries.</p>
       */
      protected Vector localDelegate = new Vector();
  
      /**
       * A hash of CatalogReaders.
       * <p/>
       * <p>This hash maps MIME types to elements in the readerArr
       * vector. This allows the Catalog to quickly locate the reader
       * for a particular MIME type.</p>
       */
      protected Hashtable readerMap = new Hashtable();
  
      /**
       * A vector of CatalogReaders.
       * <p/>
       * <p>This vector contains all of the readers in the order that they
       * were added. In the event that a catalog is read from a file, where
       * the MIME type is unknown, each reader is attempted in turn until
       * one succeeds.</p>
       */
      protected Vector readerArr = new Vector();
  
      /**
       * Constructs an empty Catalog.
       * <p/>
       * <p>The constructor interrogates the relevant system properties
       * using the default (static) CatalogManager
       * and initializes the catalog data structures.</p>
       */
      public Catalog() {
          // nop;
      }
  
      /**
       * Constructs an empty Catalog with a specific CatalogManager.
       * <p/>
       * <p>The constructor interrogates the relevant system properties
       * using the specified Catalog Manager
       * and initializes the catalog data structures.</p>
       */
      public Catalog(CatalogManager manager) {
          catalogManager = manager;
      }
  
      /**
       * Return the CatalogManager used by this catalog.
       */
      public CatalogManager getCatalogManager() {
          return catalogManager;
      }
  
      /**
       * Establish the CatalogManager used by this catalog.
       */
      public void setCatalogManager(CatalogManager manager) {
          catalogManager = manager;
      }
  
      /**
       * Setup readers.
       */
      public void setupReaders() {
          SAXParserFactory spf = SAXParserFactory.newInstance();
          spf.setNamespaceAware(true);
          spf.setValidating(false);
  
          SAXCatalogReader saxReader = new SAXCatalogReader(spf);
  
          saxReader.setCatalogParser(null, "XCatalog",
                  "org.apache.xml.resolver.readers.XCatalogReader");
  
          saxReader.setCatalogParser(OASISXMLCatalogReader.namespaceName,
                  "catalog",
                  "org.apache.xml.resolver.readers.OASISXMLCatalogReader");
  
          addReader("application/xml", saxReader);
  
          TR9401CatalogReader textReader = new TR9401CatalogReader();
          addReader("text/plain", textReader);
      }
  
      /**
       * Add a new CatalogReader to the Catalog.
       * <p/>
       * <p>This method allows you to add a new CatalogReader to the
       * catalog. The reader will be associated with the specified mimeType.
       * You can only have one reader per mimeType.</p>
       * <p/>
       * <p>In the absence of a mimeType (e.g., when reading a catalog
       * directly from a file on the local system), the readers are attempted
       * in the order that you add them to the Catalog.</p>
       * <p/>
       * <p>Note that subordinate catalogs (created by CATALOG or
       * DELEGATE* entries) get a copy of the set of readers present in
       * the primary catalog when they are created. Readers added subsequently
       * will not be available. For this reason, it is best to add all
       * of the readers before the first call to parse a catalog.</p>
       *
       * @param mimeType The MIME type associated with this reader.
       * @param reader   The CatalogReader to use.
       */
      public void addReader(String mimeType, CatalogReader reader) {
          if (readerMap.containsKey(mimeType)) {
              Integer pos = (Integer) readerMap.get(mimeType);
              readerArr.set(pos.intValue(), reader);
          } else {
              readerArr.add(reader);
              Integer pos = new Integer(readerArr.size() - 1);
              readerMap.put(mimeType, pos);
          }
      }
  
      /**
       * Copies the reader list from the current Catalog to a new Catalog.
       * <p/>
       * <p>This method is used internally when constructing a new catalog.
       * It copies the current reader associations over to the new catalog.
       * </p>
       *
       * @param newCatalog The new Catalog.
       */
      protected void copyReaders(Catalog newCatalog) {
          // Have to copy the readers in the right order...convert hash to arr
          Vector mapArr = new Vector(readerMap.size());
  
          // Pad the mapArr out to the right length
          for (int count = 0; count < readerMap.size(); count++) {
              mapArr.add(null);
          }
  
          Enumeration en = readerMap.keys();
          while (en.hasMoreElements()) {
              String mimeType = (String) en.nextElement();
              Integer pos = (Integer) readerMap.get(mimeType);
              mapArr.set(pos.intValue(), mimeType);
          }
  
          for (int count = 0; count < mapArr.size(); count++) {
              String mimeType = (String) mapArr.get(count);
              Integer pos = (Integer) readerMap.get(mimeType);
              newCatalog.addReader(mimeType,
                      (CatalogReader)
                              readerArr.get(pos.intValue()));
          }
      }
  
      /**
       * Create a new Catalog object.
       * <p/>
       * <p>This method constructs a new instance of the running Catalog
       * class (which might be a subtype of org.apache.xml.resolver.Catalog).
       * All new catalogs are managed by the same CatalogManager.
       * </p>
       * <p/>
       * <p>N.B. All Catalog subtypes should call newCatalog() to construct
       * a new Catalog. Do not simply use "new Subclass()" since that will
       * confuse future subclasses.</p>
       */
      protected Catalog newCatalog() {
          String catalogClass = this.getClass().getName();
  
          try {
              Catalog c = (Catalog) (Class.forName(catalogClass).newInstance());
              c.setCatalogManager(catalogManager);
              copyReaders(c);
              return c;
          } catch (ClassNotFoundException cnfe) {
              catalogManager.debug.message(1, "Class Not Found Exception: " + catalogClass);
          } catch (IllegalAccessException iae) {
              catalogManager.debug.message(1, "Illegal Access Exception: " + catalogClass);
          } catch (InstantiationException ie) {
              catalogManager.debug.message(1, "Instantiation Exception: " + catalogClass);
          } catch (ClassCastException cce) {
              catalogManager.debug.message(1, "Class Cast Exception: " + catalogClass);
          } catch (Exception e) {
              catalogManager.debug.message(1, "Other Exception: " + catalogClass);
          }
  
          Catalog c = new Catalog();
          c.setCatalogManager(catalogManager);
          copyReaders(c);
          return c;
      }
  
      /**
       * Returns the current base URI.
       */
      public String getCurrentBase() {
          return base.toString();
      }
  
      /**
       * Returns the default override setting associated with this
       * catalog.
       * <p/>
       * <p>All catalog files loaded by this catalog will have the
       * initial override setting specified by this default.</p>
       */
      public String getDefaultOverride() {
          if (default_override) {
              return "yes";
          } else {
              return "no";
          }
      }
  
      /**
       * Load the system catalog files.
       * <p/>
       * <p>The method adds all of the
       * catalogs specified in the <tt>xml.catalog.files</tt> property
       * to the Catalog list.</p>
       *
       * @throws MalformedURLException One of the system catalogs is
       *                               identified with a filename that is not a valid URL.
       * @throws IOException           One of the system catalogs cannot be read.
       */
      public void loadSystemCatalogs()
              throws IOException {
  
          Vector catalogs = catalogManager.getCatalogFiles();
          if (catalogs != null) {
              for (int count = 0; count < catalogs.size(); count++) {
                  catalogFiles.addElement(catalogs.elementAt(count));
              }
          }
  
          if (catalogFiles.size() > 0) {
              // This is a little odd. The parseCatalog() method expects
              // a filename, but it adds that name to the end of the
              // catalogFiles vector, and then processes that vector.
              // This allows the system to handle CATALOG entries
              // correctly.
              //
              // In this init case, we take the last element off the
              // catalogFiles vector and pass it to parseCatalog. This
              // will "do the right thing" in the init case, and allow
              // parseCatalog() to do the right thing in the non-init
              // case. Honest.
              //
              String catfile = (String) catalogFiles.lastElement();
              catalogFiles.removeElement(catfile);
              parseCatalog(catfile);
          }
      }
  
      /**
       * Parse a catalog file, augmenting internal data structures.
       *
       * @param fileName The filename of the catalog file to process
       * @throws MalformedURLException The fileName cannot be turned into
       *                               a valid URL.
       * @throws IOException           Error reading catalog file.
       */
      public synchronized void parseCatalog(String fileName)
              throws IOException {
  
          default_override = catalogManager.getPreferPublic();
          catalogManager.debug.message(4, "Parse catalog: " + fileName);
  
          // Put the file into the list of catalogs to process...
          // In all cases except the case when initCatalog() is the
          // caller, this will be the only catalog initially in the list...
          catalogFiles.addElement(fileName);
  
          // Now process all the pending catalogs...
          parsePendingCatalogs();
      }
  
      /**
       * Parse a catalog file, augmenting internal data structures.
       * <p/>
       * <p>Catalogs retrieved over the net may have an associated MIME type.
       * The MIME type can be used to select an appropriate reader.</p>
       *
       * @param mimeType The MIME type of the catalog file.
       * @param is       The InputStream from which the catalog should be read
       * @throws CatalogException Failed to load catalog
       *                          mimeType.
       * @throws IOException      Error reading catalog file.
       */
      public synchronized void parseCatalog(String mimeType, InputStream is)
              throws IOException, CatalogException {
  
          default_override = catalogManager.getPreferPublic();
          catalogManager.debug.message(4, "Parse " + mimeType + " catalog on input stream");
  
          CatalogReader reader = null;
  
          if (readerMap.containsKey(mimeType)) {
              int arrayPos = ((Integer) readerMap.get(mimeType)).intValue();
              reader = (CatalogReader) readerArr.get(arrayPos);
          }
  
          if (reader == null) {
              String msg = "No CatalogReader for MIME type: " + mimeType;
              catalogManager.debug.message(2, msg);
              throw new CatalogException(CatalogException.UNPARSEABLE, msg);
          }
  
          reader.readCatalog(this, is);
  
          // Now process all the pending catalogs...
          parsePendingCatalogs();
      }
  
      /**
       * Parse a catalog document, augmenting internal data structures.
       * <p/>
       * <p>This method supports catalog files stored in jar files: e.g.,
       * jar:file:///path/to/filename.jar!/path/to/catalog.xml". That URI
       * doesn't survive transmogrification through the URI processing that
       * the parseCatalog(String) performs and passing it as an input stream
       * doesn't set the base URI appropriately.</p>
       * <p/>
       * <p>Written by Stefan Wachter (2002-09-26)</p>
       *
       * @param aUrl The URL of the catalog document to process
       * @throws IOException Error reading catalog file.
       */
      public synchronized void parseCatalog(URL aUrl) throws IOException {
          catalogCwd = aUrl;
          base = aUrl;
  
          default_override = catalogManager.getPreferPublic();
          catalogManager.debug.message(4, "Parse catalog: " + aUrl.toString());
  
          DataInputStream inStream = null;
          boolean parsed = false;
  
          for (int count = 0; !parsed && count < readerArr.size(); count++) {
              CatalogReader reader = (CatalogReader) readerArr.get(count);
  
              try {
                  inStream = new DataInputStream(aUrl.openStream());
              } catch (FileNotFoundException fnfe) {
                  // No catalog; give up!
                  break;
              }
  
              try {
                  reader.readCatalog(this, inStream);
                  parsed = true;
              } catch (CatalogException ce) {
                  if (ce.getExceptionType() == CatalogException.PARSE_FAILED) {
                      // give up!
                      break;
                  } else {
                      // try again!
                  }
              }
  
              try {
                  inStream.close();
              } catch (IOException e) {
                  //nop
              }
          }
  
          if (parsed) parsePendingCatalogs();
      }
  
      /**
       * Parse all of the pending catalogs.
       * <p/>
       * <p>Catalogs may refer to other catalogs, this method parses
       * all of the currently pending catalog files.</p>
       */
      protected synchronized void parsePendingCatalogs()
              throws IOException {
  
          if (!localCatalogFiles.isEmpty()) {
              // Move all the localCatalogFiles into the front of
              // the catalogFiles queue
              Vector newQueue = new Vector();
              Enumeration q = localCatalogFiles.elements();
              while (q.hasMoreElements()) {
                  newQueue.addElement(q.nextElement());
              }
  
              // Put the rest of the catalogs on the end of the new list
              for (int curCat = 0; curCat < catalogFiles.size(); curCat++) {
                  String catfile = (String) catalogFiles.elementAt(curCat);
                  newQueue.addElement(catfile);
              }
  
              catalogFiles = newQueue;
              localCatalogFiles.clear();
          }
  
          // Suppose there are no catalog files to process, but the
          // single catalog already parsed included some delegate
          // entries? Make sure they don't get lost.
          if (catalogFiles.isEmpty() && !localDelegate.isEmpty()) {
              Enumeration e = localDelegate.elements();
              while (e.hasMoreElements()) {
                  catalogEntries.addElement(e.nextElement());
              }
              localDelegate.clear();
          }
  
          // Now process all the files on the catalogFiles vector. This
          // vector can grow during processing if CATALOG entries are
          // encountered in the catalog
          while (!catalogFiles.isEmpty()) {
              String catfile = (String) catalogFiles.elementAt(0);
              try {
                  catalogFiles.remove(0);
              } catch (ArrayIndexOutOfBoundsException e) {
                  // can't happen
              }
  
              if (catalogEntries.size() == 0 && catalogs.size() == 0) {
                  // We haven't parsed any catalogs yet, let this
                  // catalog be the first...
                  try {
                      parseCatalogFile(catfile);
                  } catch (CatalogException ce) {
                      System.out.println("FIXME: " + ce.toString());
                  }
              } else {
                  // This is a subordinate catalog. We save its name,
                  // but don't bother to load it unless it's necessary.
                  catalogs.addElement(catfile);
              }
  
              if (!localCatalogFiles.isEmpty()) {
                  // Move all the localCatalogFiles into the front of
                  // the catalogFiles queue
                  Vector newQueue = new Vector();
                  Enumeration q = localCatalogFiles.elements();
                  while (q.hasMoreElements()) {
                      newQueue.addElement(q.nextElement());
                  }
  
                  // Put the rest of the catalogs on the end of the new list
                  for (int curCat = 0; curCat < catalogFiles.size(); curCat++) {
                      catfile = (String) catalogFiles.elementAt(curCat);
                      newQueue.addElement(catfile);
                  }
  
                  catalogFiles = newQueue;
                  localCatalogFiles.clear();
              }
  
              if (!localDelegate.isEmpty()) {
                  Enumeration e = localDelegate.elements();
                  while (e.hasMoreElements()) {
                      catalogEntries.addElement(e.nextElement());
                  }
                  localDelegate.clear();
              }
          }
  
          // We've parsed them all, reinit the vector...
          catalogFiles.clear();
      }
  
      /**
       * Parse a single catalog file, augmenting internal data structures.
       *
       * @param fileName The filename of the catalog file to process
       * @throws MalformedURLException The fileName cannot be turned into
       *                               a valid URL.
       * @throws IOException           Error reading catalog file.
       */
      protected synchronized void parseCatalogFile(String fileName)
              throws IOException, CatalogException {
  
          CatalogEntry entry;
  
          // The base-base is the cwd. If the catalog file is specified
          // with a relative path, this assures that it gets resolved
          // properly...
          try {
              // tack on a basename because URLs point to files not dirs
              catalogCwd = FileURL.makeURL("basename");
          } catch (MalformedURLException e) {
              String userdir = System.getProperty("user.dir");
              userdir = userdir.replace('\\', '/');
              catalogManager.debug.message(1, "Malformed URL on cwd", userdir);
              catalogCwd = null;
          }
  
          // The initial base URI is the location of the catalog file
          try {
              base = new URL(catalogCwd, fixSlashes(fileName));
          } catch (MalformedURLException e) {
              try {
                  base = new URL("file:" + fixSlashes(fileName));
              } catch (MalformedURLException e2) {
                  catalogManager.debug.message(1, "Malformed URL on catalog filename",
                          fixSlashes(fileName));
                  base = null;
              }
          }
  
          catalogManager.debug.message(2, "Loading catalog", fileName);
          catalogManager.debug.message(4, "Default BASE", base.toString());
  
          fileName = base.toString();
  
          DataInputStream inStream = null;
          boolean parsed = false;
          boolean notFound = false;
  
          for (int count = 0; !parsed && count < readerArr.size(); count++) {
              CatalogReader reader = (CatalogReader) readerArr.get(count);
  
              try {
                  notFound = false;
                  inStream = new DataInputStream(base.openStream());
              } catch (FileNotFoundException fnfe) {
                  // No catalog; give up!
                  notFound = true;
                  break;
              }
  
              try {
                  reader.readCatalog(this, inStream);
                  parsed = true;
              } catch (CatalogException ce) {
                  if (ce.getExceptionType() == CatalogException.PARSE_FAILED) {
                      // give up!
                      break;
                  } else {
                      // try again!
                  }
              }
  
              try {
                  inStream.close();
              } catch (IOException e) {
                  //nop
              }
          }
  
          if (!parsed) {
              if (notFound) {
                  catalogManager.debug.message(3, "Catalog does not exist", fileName);
              } else {
                  catalogManager.debug.message(1, "Failed to parse catalog", fileName);
              }
          }
      }
  
      /**
       * Cleanup and process a Catalog entry.
       * <p/>
       * <p>This method processes each Catalog entry, changing mapped
       * relative system identifiers into absolute ones (based on the current
       * base URI), and maintaining other information about the current
       * catalog.</p>
       *
       * @param entry The CatalogEntry to process.
       */
      public void addEntry(CatalogEntry entry) {
          int type = entry.getEntryType();
  
          if (type == BASE) {
              String value = entry.getEntryArg(0);
              URL newbase = null;
  
              if (base == null) {
                  catalogManager.debug.message(5, "BASE CUR", "null");
              } else {
                  catalogManager.debug.message(5, "BASE CUR", base.toString());
              }
              catalogManager.debug.message(4, "BASE STR", value);
  
              try {
                  value = fixSlashes(value);
                  newbase = new URL(base, value);
              } catch (MalformedURLException e) {
                  try {
                      newbase = new URL("file:" + value);
                  } catch (MalformedURLException e2) {
                      catalogManager.debug.message(1, "Malformed URL on base", value);
                      newbase = null;
                  }
              }
  
              if (newbase != null) {
                  base = newbase;
              }
  
              catalogManager.debug.message(5, "BASE NEW", base.toString());
          } else if (type == CATALOG) {
              String fsi = makeAbsolute(entry.getEntryArg(0));
  
              catalogManager.debug.message(4, "CATALOG", fsi);
  
              localCatalogFiles.addElement(fsi);
          } else if (type == PUBLIC) {
              String publicid = PublicId.normalize(entry.getEntryArg(0));
              String systemid = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, publicid);
              entry.setEntryArg(1, systemid);
  
              catalogManager.debug.message(4, "PUBLIC", publicid, systemid);
  
              catalogEntries.addElement(entry);
          } else if (type == SYSTEM) {
              String systemid = normalizeURI(entry.getEntryArg(0));
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "SYSTEM", systemid, fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == URI) {
              String uri = normalizeURI(entry.getEntryArg(0));
              String altURI = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(1, altURI);
  
              catalogManager.debug.message(4, "URI", uri, altURI);
  
              catalogEntries.addElement(entry);
          } else if (type == DOCUMENT) {
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(0)));
              entry.setEntryArg(0, fsi);
  
              catalogManager.debug.message(4, "DOCUMENT", fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == OVERRIDE) {
              catalogManager.debug.message(4, "OVERRIDE", entry.getEntryArg(0));
  
              catalogEntries.addElement(entry);
          } else if (type == SGMLDECL) {
              // meaningless in XML
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(0)));
              entry.setEntryArg(0, fsi);
  
              catalogManager.debug.message(4, "SGMLDECL", fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == DELEGATE_PUBLIC) {
              String ppi = PublicId.normalize(entry.getEntryArg(0));
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, ppi);
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "DELEGATE_PUBLIC", ppi, fsi);
  
              addDelegate(entry);
          } else if (type == DELEGATE_SYSTEM) {
              String psi = normalizeURI(entry.getEntryArg(0));
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, psi);
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "DELEGATE_SYSTEM", psi, fsi);
  
              addDelegate(entry);
          } else if (type == DELEGATE_URI) {
              String pui = normalizeURI(entry.getEntryArg(0));
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, pui);
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "DELEGATE_URI", pui, fsi);
  
              addDelegate(entry);
          } else if (type == REWRITE_SYSTEM) {
              String psi = normalizeURI(entry.getEntryArg(0));
              String rpx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, psi);
              entry.setEntryArg(1, rpx);
  
              catalogManager.debug.message(4, "REWRITE_SYSTEM", psi, rpx);
  
              catalogEntries.addElement(entry);
          } else if (type == REWRITE_URI) {
              String pui = normalizeURI(entry.getEntryArg(0));
              String upx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, pui);
              entry.setEntryArg(1, upx);
  
              catalogManager.debug.message(4, "REWRITE_URI", pui, upx);
  
              catalogEntries.addElement(entry);
          } else if (type == SYSTEM_SUFFIX) {
              String pui = normalizeURI(entry.getEntryArg(0));
              String upx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, pui);
              entry.setEntryArg(1, upx);
  
              catalogManager.debug.message(4, "SYSTEM_SUFFIX", pui, upx);
  
              catalogEntries.addElement(entry);
          } else if (type == URI_SUFFIX) {
              String pui = normalizeURI(entry.getEntryArg(0));
              String upx = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
  
              entry.setEntryArg(0, pui);
              entry.setEntryArg(1, upx);
  
              catalogManager.debug.message(4, "URI_SUFFIX", pui, upx);
  
              catalogEntries.addElement(entry);
          } else if (type == DOCTYPE) {
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "DOCTYPE", entry.getEntryArg(0), fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == DTDDECL) {
              // meaningless in XML
              String fpi = PublicId.normalize(entry.getEntryArg(0));
              entry.setEntryArg(0, fpi);
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "DTDDECL", fpi, fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == ENTITY) {
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "ENTITY", entry.getEntryArg(0), fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == LINKTYPE) {
              // meaningless in XML
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "LINKTYPE", entry.getEntryArg(0), fsi);
  
              catalogEntries.addElement(entry);
          } else if (type == NOTATION) {
              String fsi = makeAbsolute(normalizeURI(entry.getEntryArg(1)));
              entry.setEntryArg(1, fsi);
  
              catalogManager.debug.message(4, "NOTATION", entry.getEntryArg(0), fsi);
  
              catalogEntries.addElement(entry);
          } else {
              catalogEntries.addElement(entry);
          }
      }
  
      /**
       * Handle unknown CatalogEntry types.
       * <p/>
       * <p>This method exists to allow subclasses to deal with unknown
       * entry types.</p>
       */
      public void unknownEntry(Vector strings) {
          if (strings != null && strings.size() > 0) {
              String keyword = (String) strings.elementAt(0);
              catalogManager.debug.message(2, "Unrecognized token parsing catalog", keyword);
          }
      }
  
      /**
       * Parse all subordinate catalogs.
       * <p/>
       * <p>This method recursively parses all of the subordinate catalogs.
       * If this method does not throw an exception, you can be confident that
       * no subsequent call to any resolve*() method will either, with two
       * possible exceptions:</p>
       * <p/>
       * <ol>
       * <li><p>Delegated catalogs are re-parsed each time they are needed
       * (because a variable list of them may be needed in each case,
       * depending on the length of the matching partial public identifier).</p>
       * <p>But they are parsed by this method, so as long as they don't
       * change or disappear while the program is running, they shouldn't
       * generate errors later if they don't generate errors now.</p>
       * <li><p>If you add new catalogs with <code>parseCatalog</code>, they
       * won't be loaded until they are needed or until you call
       * <code>parseAllCatalogs</code> again.</p>
       * </ol>
       * <p/>
       * <p>On the other hand, if you don't call this method, you may
       * successfully parse documents without having to load all possible
       * catalogs.</p>
       *
       * @throws MalformedURLException The filename (URL) for a
       *                               subordinate or delegated catalog is not a valid URL.
       * @throws IOException           Error reading some subordinate or delegated
       *                               catalog file.
       */
      public void parseAllCatalogs()
              throws IOException {
  
          // Parse all the subordinate catalogs
          for (int catPos = 0; catPos < catalogs.size(); catPos++) {
              Catalog c = null;
  
              try {
                  c = (Catalog) catalogs.elementAt(catPos);
              } catch (ClassCastException e) {
                  String catfile = (String) catalogs.elementAt(catPos);
                  c = newCatalog();
  
                  c.parseCatalog(catfile);
                  catalogs.setElementAt(c, catPos);
                  c.parseAllCatalogs();
              }
          }
  
          // Parse all the DELEGATE catalogs
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == DELEGATE_PUBLIC
                      || e.getEntryType() == DELEGATE_SYSTEM
                      || e.getEntryType() == DELEGATE_URI) {
                  Catalog dcat = newCatalog();
                  dcat.parseCatalog(e.getEntryArg(1));
              }
          }
      }
  
  
      /**
       * Return the applicable DOCTYPE system identifier.
       *
       * @param entityName The name of the entity (element) for which
       *                   a doctype is required.
       * @param publicId   The nominal public identifier for the doctype
       *                   (as provided in the source document).
       * @param systemId   The nominal system identifier for the doctype
       *                   (as provided in the source document).
       * @return The system identifier to use for the doctype.
       * @throws MalformedURLException The formal system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolveDoctype(String entityName,
                                   String publicId,
                                   String systemId)
              throws IOException {
          String resolved = null;
  
          catalogManager.debug.message(3, "resolveDoctype("
                  + entityName + "," + publicId + "," + systemId + ")");
  
          systemId = normalizeURI(systemId);
  
          if (publicId != null && publicId.startsWith("urn:publicid:")) {
              publicId = PublicId.decodeURN(publicId);
          }
  
          if (systemId != null && systemId.startsWith("urn:publicid:")) {
              systemId = PublicId.decodeURN(systemId);
              if (publicId != null && !publicId.equals(systemId)) {
                  catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
                  systemId = null;
              } else {
                  publicId = systemId;
                  systemId = null;
              }
          }
  
          if (systemId != null) {
              // If there's a SYSTEM entry in this catalog, use it
              resolved = resolveLocalSystem(systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          if (publicId != null) {
              // If there's a PUBLIC entry in this catalog, use it
              resolved = resolveLocalPublic(DOCTYPE,
                      entityName,
                      publicId,
                      systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // If there's a DOCTYPE entry in this catalog, use it
          boolean over = default_override;
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == OVERRIDE) {
                  over = e.getEntryArg(0).equalsIgnoreCase("YES");
                  continue;
              }
  
              if (e.getEntryType() == DOCTYPE
                      && e.getEntryArg(0).equals(entityName)) {
                  if (over || systemId == null) {
                      return e.getEntryArg(1);
                  }
              }
          }
  
          // Otherwise, look in the subordinate catalogs
          return resolveSubordinateCatalogs(DOCTYPE,
                  entityName,
                  publicId,
                  systemId);
      }
  
      /**
       * Return the applicable DOCUMENT entry.
       *
       * @return The system identifier to use for the doctype.
       * @throws MalformedURLException The formal system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolveDocument()
              throws IOException {
          // If there's a DOCUMENT entry, return it
  
          catalogManager.debug.message(3, "resolveDocument");
  
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == DOCUMENT) {
                  return e.getEntryArg(0);
              }
          }
  
          return resolveSubordinateCatalogs(DOCUMENT,
                  null, null, null);
      }
  
      /**
       * Return the applicable ENTITY system identifier.
       *
       * @param entityName The name of the entity for which
       *                   a system identifier is required.
       * @param publicId   The nominal public identifier for the entity
       *                   (as provided in the source document).
       * @param systemId   The nominal system identifier for the entity
       *                   (as provided in the source document).
       * @return The system identifier to use for the entity.
       * @throws MalformedURLException The formal system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolveEntity(String entityName,
                                  String publicId,
                                  String systemId)
              throws IOException {
          String resolved = null;
  
          catalogManager.debug.message(3, "resolveEntity("
                  + entityName + "," + publicId + "," + systemId + ")");
  
          systemId = normalizeURI(systemId);
  
          if (publicId != null && publicId.startsWith("urn:publicid:")) {
              publicId = PublicId.decodeURN(publicId);
          }
  
          if (systemId != null && systemId.startsWith("urn:publicid:")) {
              systemId = PublicId.decodeURN(systemId);
              if (publicId != null && !publicId.equals(systemId)) {
                  catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
                  systemId = null;
              } else {
                  publicId = systemId;
                  systemId = null;
              }
          }
  
          if (systemId != null) {
              // If there's a SYSTEM entry in this catalog, use it
              resolved = resolveLocalSystem(systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          if (publicId != null) {
              // If there's a PUBLIC entry in this catalog, use it
              resolved = resolveLocalPublic(ENTITY,
                      entityName,
                      publicId,
                      systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // If there's a ENTITY entry in this catalog, use it
          boolean over = default_override;
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == OVERRIDE) {
                  over = e.getEntryArg(0).equalsIgnoreCase("YES");
                  continue;
              }
  
              if (e.getEntryType() == ENTITY
                      && e.getEntryArg(0).equals(entityName)) {
                  if (over || systemId == null) {
                      return e.getEntryArg(1);
                  }
              }
          }
  
          // Otherwise, look in the subordinate catalogs
          return resolveSubordinateCatalogs(ENTITY,
                  entityName,
                  publicId,
                  systemId);
      }
  
      /**
       * Return the applicable NOTATION system identifier.
       *
       * @param notationName The name of the notation for which
       *                     a doctype is required.
       * @param publicId     The nominal public identifier for the notation
       *                     (as provided in the source document).
       * @param systemId     The nominal system identifier for the notation
       *                     (as provided in the source document).
       * @return The system identifier to use for the notation.
       * @throws MalformedURLException The formal system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolveNotation(String notationName,
                                    String publicId,
                                    String systemId)
              throws IOException {
          String resolved = null;
  
          catalogManager.debug.message(3, "resolveNotation("
                  + notationName + "," + publicId + "," + systemId + ")");
  
          systemId = normalizeURI(systemId);
  
          if (publicId != null && publicId.startsWith("urn:publicid:")) {
              publicId = PublicId.decodeURN(publicId);
          }
  
          if (systemId != null && systemId.startsWith("urn:publicid:")) {
              systemId = PublicId.decodeURN(systemId);
              if (publicId != null && !publicId.equals(systemId)) {
                  catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
                  systemId = null;
              } else {
                  publicId = systemId;
                  systemId = null;
              }
          }
  
          if (systemId != null) {
              // If there's a SYSTEM entry in this catalog, use it
              resolved = resolveLocalSystem(systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          if (publicId != null) {
              // If there's a PUBLIC entry in this catalog, use it
              resolved = resolveLocalPublic(NOTATION,
                      notationName,
                      publicId,
                      systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // If there's a NOTATION entry in this catalog, use it
          boolean over = default_override;
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == OVERRIDE) {
                  over = e.getEntryArg(0).equalsIgnoreCase("YES");
                  continue;
              }
  
              if (e.getEntryType() == NOTATION
                      && e.getEntryArg(0).equals(notationName)) {
                  if (over || systemId == null) {
                      return e.getEntryArg(1);
                  }
              }
          }
  
          // Otherwise, look in the subordinate catalogs
          return resolveSubordinateCatalogs(NOTATION,
                  notationName,
                  publicId,
                  systemId);
      }
  
      /**
       * Return the applicable PUBLIC or SYSTEM identifier.
       * <p/>
       * <p>This method searches the Catalog and returns the system
       * identifier specified for the given system or
       * public identifiers. If
       * no appropriate PUBLIC or SYSTEM entry is found in the Catalog,
       * null is returned.</p>
       *
       * @param publicId The public identifier to locate in the catalog.
       *                 Public identifiers are normalized before comparison.
       * @param systemId The nominal system identifier for the entity
       *                 in question (as provided in the source document).
       * @return The system identifier to use.
       * Note that the nominal system identifier is not returned if a
       * match is not found in the catalog, instead null is returned
       * to indicate that no match was found.
       * @throws MalformedURLException The formal system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolvePublic(String publicId, String systemId)
              throws IOException {
  
          catalogManager.debug.message(3, "resolvePublic(" + publicId + "," + systemId + ")");
  
          systemId = normalizeURI(systemId);
  
          if (publicId != null && publicId.startsWith("urn:publicid:")) {
              publicId = PublicId.decodeURN(publicId);
          }
  
          if (systemId != null && systemId.startsWith("urn:publicid:")) {
              systemId = PublicId.decodeURN(systemId);
              if (publicId != null && !publicId.equals(systemId)) {
                  catalogManager.debug.message(1, "urn:publicid: system identifier differs from public identifier; using public identifier");
                  systemId = null;
              } else {
                  publicId = systemId;
                  systemId = null;
              }
          }
  
          // If there's a SYSTEM entry in this catalog, use it
          if (systemId != null) {
              String resolved = resolveLocalSystem(systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // If there's a PUBLIC entry in this catalog, use it
          String resolved = resolveLocalPublic(PUBLIC,
                  null,
                  publicId,
                  systemId);
          if (resolved != null) {
              return resolved;
          }
  
          // Otherwise, look in the subordinate catalogs
          return resolveSubordinateCatalogs(PUBLIC,
                  null,
                  publicId,
                  systemId);
      }
  
      /**
       * Return the applicable PUBLIC or SYSTEM identifier.
       * <p/>
       * <p>This method searches the Catalog and returns the system
       * identifier specified for the given system or public identifiers.
       * If no appropriate PUBLIC or SYSTEM entry is found in the Catalog,
       * delegated Catalogs are interrogated.</p>
       * <p/>
       * <p>There are four possible cases:</p>
       * <p/>
       * <ul>
       * <li>If the system identifier provided matches a SYSTEM entry
       * in the current catalog, the SYSTEM entry is returned.
       * <li>If the system identifier is not null, the PUBLIC entries
       * that were encountered when OVERRIDE YES was in effect are
       * interrogated and the first matching entry is returned.</li>
       * <li>If the system identifier is null, then all of the PUBLIC
       * entries are interrogated and the first matching entry
       * is returned. This may not be the same as the preceding case, if
       * some PUBLIC entries are encountered when OVERRIDE NO is in effect. In
       * XML, the only place where a public identifier may occur without
       * a system identifier is in a notation declaration.</li>
       * <li>Finally, if the public identifier matches one of the partial
       * public identifiers specified in a DELEGATE* entry in
       * the Catalog, the delegated catalog is interrogated. The first
       * time that the delegated catalog is required, it will be
       * retrieved and parsed. It is subsequently cached.
       * </li>
       * </ul>
       *
       * @param entityType The CatalogEntry type for which this query is
       *                   being conducted. This is necessary in order to do the approprate
       *                   query on a delegated catalog.
       * @param entityName The name of the entity being searched for, if
       *                   appropriate.
       * @param publicId   The public identifier of the entity in question.
       * @param systemId   The nominal system identifier for the entity
       *                   in question (as provided in the source document).
       * @return The system identifier to use.
       * Note that the nominal system identifier is not returned if a
       * match is not found in the catalog, instead null is returned
       * to indicate that no match was found.
       * @throws MalformedURLException The formal system identifier of a
       *                               delegated catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading delegated catalog file.
       */
      protected synchronized String resolveLocalPublic(int entityType,
                                                       String entityName,
                                                       String publicId,
                                                       String systemId)
              throws IOException {
  
          // Always normalize the public identifier before attempting a match
          publicId = PublicId.normalize(publicId);
  
          // If there's a SYSTEM entry in this catalog, use it
          if (systemId != null) {
              String resolved = resolveLocalSystem(systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // If there's a PUBLIC entry in this catalog, use it
          boolean over = default_override;
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == OVERRIDE) {
                  over = e.getEntryArg(0).equalsIgnoreCase("YES");
                  continue;
              }
  
              if (e.getEntryType() == PUBLIC
                      && e.getEntryArg(0).equals(publicId)) {
                  if (over || systemId == null) {
                      return e.getEntryArg(1);
                  }
              }
          }
  
          // If there's a DELEGATE_PUBLIC entry in this catalog, use it
          over = default_override;
          en = catalogEntries.elements();
          Vector delCats = new Vector();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == OVERRIDE) {
                  over = e.getEntryArg(0).equalsIgnoreCase("YES");
                  continue;
              }
  
              if (e.getEntryType() == DELEGATE_PUBLIC
                      && (over || systemId == null)) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= publicId.length()
                          && p.equals(publicId.substring(0, p.length()))) {
                      // delegate this match to the other catalog
  
                      delCats.addElement(e.getEntryArg(1));
                  }
              }
          }
  
          if (delCats.size() > 0) {
              Enumeration enCats = delCats.elements();
  
              if (catalogManager.debug.getDebug() > 1) {
                  catalogManager.debug.message(2, "Switching to delegated catalog(s):");
                  while (enCats.hasMoreElements()) {
                      String delegatedCatalog = (String) enCats.nextElement();
                      catalogManager.debug.message(2, "\t" + delegatedCatalog);
                  }
              }
  
              Catalog dcat = newCatalog();
  
              enCats = delCats.elements();
              while (enCats.hasMoreElements()) {
                  String delegatedCatalog = (String) enCats.nextElement();
                  dcat.parseCatalog(delegatedCatalog);
              }
  
              return dcat.resolvePublic(publicId, null);
          }
  
          // Nada!
          return null;
      }
  
      /**
       * Return the applicable SYSTEM system identifier.
       * <p/>
       * <p>If a SYSTEM entry exists in the Catalog
       * for the system ID specified, return the mapped value.</p>
       * <p/>
       * <p>On Windows-based operating systems, the comparison between
       * the system identifier provided and the SYSTEM entries in the
       * Catalog is case-insensitive.</p>
       *
       * @param systemId The system ID to locate in the catalog.
       * @return The resolved system identifier.
       * @throws MalformedURLException The formal system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolveSystem(String systemId)
              throws IOException {
  
          catalogManager.debug.message(3, "resolveSystem(" + systemId + ")");
  
          systemId = normalizeURI(systemId);
  
          if (systemId != null && systemId.startsWith("urn:publicid:")) {
              systemId = PublicId.decodeURN(systemId);
              return resolvePublic(systemId, null);
          }
  
          // If there's a SYSTEM entry in this catalog, use it
          if (systemId != null) {
              String resolved = resolveLocalSystem(systemId);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // Otherwise, look in the subordinate catalogs
          return resolveSubordinateCatalogs(SYSTEM,
                  null,
                  null,
                  systemId);
      }
  
      /**
       * Return the applicable SYSTEM system identifier in this
       * catalog.
       * <p/>
       * <p>If a SYSTEM entry exists in the catalog file
       * for the system ID specified, return the mapped value.</p>
       *
       * @param systemId The system ID to locate in the catalog
       * @return The mapped system identifier or null
       */
      protected String resolveLocalSystem(String systemId)
              throws IOException {
  
          String osname = System.getProperty("os.name");
          boolean windows = (osname.indexOf("Windows") >= 0);
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == SYSTEM
                      && (e.getEntryArg(0).equals(systemId)
                      || (windows
                      && e.getEntryArg(0).equalsIgnoreCase(systemId)))) {
                  return e.getEntryArg(1);
              }
          }
  
          // If there's a REWRITE_SYSTEM entry in this catalog, use it
          en = catalogEntries.elements();
          String startString = null;
          String prefix = null;
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
  
              if (e.getEntryType() == REWRITE_SYSTEM) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= systemId.length()
                          && p.equals(systemId.substring(0, p.length()))) {
                      // Is this the longest prefix?
                      if (startString == null
                              || p.length() > startString.length()) {
                          startString = p;
                          prefix = e.getEntryArg(1);
                      }
                  }
              }
          }
  
          if (prefix != null) {
              // return the systemId with the new prefix
              return prefix + systemId.substring(startString.length());
          }
  
          // If there's a SYSTEM_SUFFIX entry in this catalog, use it
          en = catalogEntries.elements();
          String suffixString = null;
          String suffixURI = null;
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
  
              if (e.getEntryType() == SYSTEM_SUFFIX) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= systemId.length()
                          && systemId.endsWith(p)) {
                      // Is this the longest prefix?
                      if (suffixString == null
                              || p.length() > suffixString.length()) {
                          suffixString = p;
                          suffixURI = e.getEntryArg(1);
                      }
                  }
              }
          }
  
          if (suffixURI != null) {
              // return the systemId for the suffix
              return suffixURI;
          }
  
          // If there's a DELEGATE_SYSTEM entry in this catalog, use it
          en = catalogEntries.elements();
          Vector delCats = new Vector();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
  
              if (e.getEntryType() == DELEGATE_SYSTEM) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= systemId.length()
                          && p.equals(systemId.substring(0, p.length()))) {
                      // delegate this match to the other catalog
  
                      delCats.addElement(e.getEntryArg(1));
                  }
              }
          }
  
          if (delCats.size() > 0) {
              Enumeration enCats = delCats.elements();
  
              if (catalogManager.debug.getDebug() > 1) {
                  catalogManager.debug.message(2, "Switching to delegated catalog(s):");
                  while (enCats.hasMoreElements()) {
                      String delegatedCatalog = (String) enCats.nextElement();
                      catalogManager.debug.message(2, "\t" + delegatedCatalog);
                  }
              }
  
              Catalog dcat = newCatalog();
  
              enCats = delCats.elements();
              while (enCats.hasMoreElements()) {
                  String delegatedCatalog = (String) enCats.nextElement();
                  dcat.parseCatalog(delegatedCatalog);
              }
  
              return dcat.resolveSystem(systemId);
          }
  
          return null;
      }
  
      /**
       * Return the applicable URI.
       * <p/>
       * <p>If a URI entry exists in the Catalog
       * for the URI specified, return the mapped value.</p>
       * <p/>
       * <p>URI comparison is case sensitive.</p>
       *
       * @param uri The URI to locate in the catalog.
       * @return The resolved URI.
       * @throws MalformedURLException The system identifier of a
       *                               subordinate catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading subordinate catalog file.
       */
      public String resolveURI(String uri)
              throws IOException {
  
          catalogManager.debug.message(3, "resolveURI(" + uri + ")");
  
          uri = normalizeURI(uri);
  
          if (uri != null && uri.startsWith("urn:publicid:")) {
              uri = PublicId.decodeURN(uri);
              return resolvePublic(uri, null);
          }
  
          // If there's a URI entry in this catalog, use it
          if (uri != null) {
              String resolved = resolveLocalURI(uri);
              if (resolved != null) {
                  return resolved;
              }
          }
  
          // Otherwise, look in the subordinate catalogs
          return resolveSubordinateCatalogs(URI,
                  null,
                  null,
                  uri);
      }
  
      /**
       * Return the applicable URI in this catalog.
       * <p/>
       * <p>If a URI entry exists in the catalog file
       * for the URI specified, return the mapped value.</p>
       *
       * @param uri The URI to locate in the catalog
       * @return The mapped URI or null
       */
      protected String resolveLocalURI(String uri)
              throws IOException {
          Enumeration en = catalogEntries.elements();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
              if (e.getEntryType() == URI
                      && (e.getEntryArg(0).equals(uri))) {
                  return e.getEntryArg(1);
              }
          }
  
          // If there's a REWRITE_URI entry in this catalog, use it
          en = catalogEntries.elements();
          String startString = null;
          String prefix = null;
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
  
              if (e.getEntryType() == REWRITE_URI) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= uri.length()
                          && p.equals(uri.substring(0, p.length()))) {
                      // Is this the longest prefix?
                      if (startString == null
                              || p.length() > startString.length()) {
                          startString = p;
                          prefix = e.getEntryArg(1);
                      }
                  }
              }
          }
  
          if (prefix != null) {
              // return the uri with the new prefix
              return prefix + uri.substring(startString.length());
          }
  
          // If there's a URI_SUFFIX entry in this catalog, use it
          en = catalogEntries.elements();
          String suffixString = null;
          String suffixURI = null;
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
  
              if (e.getEntryType() == URI_SUFFIX) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= uri.length()
                          && uri.endsWith(p)) {
                      // Is this the longest prefix?
                      if (suffixString == null
                              || p.length() > suffixString.length()) {
                          suffixString = p;
                          suffixURI = e.getEntryArg(1);
                      }
                  }
              }
          }
  
          if (suffixURI != null) {
              // return the uri for the suffix
              return suffixURI;
          }
  
          // If there's a DELEGATE_URI entry in this catalog, use it
          en = catalogEntries.elements();
          Vector delCats = new Vector();
          while (en.hasMoreElements()) {
              CatalogEntry e = (CatalogEntry) en.nextElement();
  
              if (e.getEntryType() == DELEGATE_URI) {
                  String p = e.getEntryArg(0);
                  if (p.length() <= uri.length()
                          && p.equals(uri.substring(0, p.length()))) {
                      // delegate this match to the other catalog
  
                      delCats.addElement(e.getEntryArg(1));
                  }
              }
          }
  
          if (delCats.size() > 0) {
              Enumeration enCats = delCats.elements();
  
              if (catalogManager.debug.getDebug() > 1) {
                  catalogManager.debug.message(2, "Switching to delegated catalog(s):");
                  while (enCats.hasMoreElements()) {
                      String delegatedCatalog = (String) enCats.nextElement();
                      catalogManager.debug.message(2, "\t" + delegatedCatalog);
                  }
              }
  
              Catalog dcat = newCatalog();
  
              enCats = delCats.elements();
              while (enCats.hasMoreElements()) {
                  String delegatedCatalog = (String) enCats.nextElement();
                  dcat.parseCatalog(delegatedCatalog);
              }
  
              return dcat.resolveURI(uri);
          }
  
          return null;
      }
  
      /**
       * Search the subordinate catalogs, in order, looking for a match.
       * <p/>
       * <p>This method searches the Catalog and returns the system
       * identifier specified for the given entity type with the given
       * name, public, and system identifiers. In some contexts, these
       * may be null.</p>
       *
       * @param entityType The CatalogEntry type for which this query is
       *                   being conducted. This is necessary in order to do the approprate
       *                   query on a subordinate catalog.
       * @param entityName The name of the entity being searched for, if
       *                   appropriate.
       * @param publicId   The public identifier of the entity in question
       *                   (as provided in the source document).
       * @param systemId   The nominal system identifier for the entity
       *                   in question (as provided in the source document). This parameter is
       *                   overloaded for the URI entry type.
       * @return The system identifier to use.
       * Note that the nominal system identifier is not returned if a
       * match is not found in the catalog, instead null is returned
       * to indicate that no match was found.
       * @throws MalformedURLException The formal system identifier of a
       *                               delegated catalog cannot be turned into a valid URL.
       * @throws IOException           Error reading delegated catalog file.
       */
      protected synchronized String resolveSubordinateCatalogs(int entityType,
                                                               String entityName,
                                                               String publicId,
                                                               String systemId)
              throws IOException {
  
          for (int catPos = 0; catPos < catalogs.size(); catPos++) {
              Catalog c = null;
  
              try {
                  c = (Catalog) catalogs.elementAt(catPos);
              } catch (ClassCastException e) {
                  String catfile = (String) catalogs.elementAt(catPos);
                  c = newCatalog();
  
                  try {
                      c.parseCatalog(catfile);
                  } catch (MalformedURLException mue) {
                      catalogManager.debug.message(1, "Malformed Catalog URL", catfile);
                  } catch (FileNotFoundException fnfe) {
                      catalogManager.debug.message(1, "Failed to load catalog, file not found",
                              catfile);
                  } catch (IOException ioe) {
                      catalogManager.debug.message(1, "Failed to load catalog, I/O error", catfile);
                  }
  
                  catalogs.setElementAt(c, catPos);
              }
  
              String resolved = null;
  
              // Ok, now what are we supposed to call here?
              if (entityType == DOCTYPE) {
                  resolved = c.resolveDoctype(entityName,
                          publicId,
                          systemId);
              } else if (entityType == DOCUMENT) {
                  resolved = c.resolveDocument();
              } else if (entityType == ENTITY) {
                  resolved = c.resolveEntity(entityName,
                          publicId,
                          systemId);
              } else if (entityType == NOTATION) {
                  resolved = c.resolveNotation(entityName,
                          publicId,
                          systemId);
              } else if (entityType == PUBLIC) {
                  resolved = c.resolvePublic(publicId, systemId);
              } else if (entityType == SYSTEM) {
                  resolved = c.resolveSystem(systemId);
              } else if (entityType == URI) {
                  resolved = c.resolveURI(systemId);
              }
  
              if (resolved != null) {
                  return resolved;
              }
          }
  
          return null;
      }
  
      // -----------------------------------------------------------------
  
      /**
       * Replace backslashes with forward slashes. (URLs always use
       * forward slashes.)
       *
       * @param sysid The input system identifier.
       * @return The same system identifier with backslashes turned into
       * forward slashes.
       */
      protected String fixSlashes(String sysid) {
          return sysid.replace('\\', '/');
      }
  
      /**
       * Construct an absolute URI from a relative one, using the current
       * base URI.
       *
       * @param sysid The (possibly relative) system identifier
       * @return The system identifier made absolute with respect to the
       * current {@link #base}.
       */
      protected String makeAbsolute(String sysid) {
          URL local = null;
  
          sysid = fixSlashes(sysid);
  
          try {
              local = new URL(base, sysid);
          } catch (MalformedURLException e) {
              catalogManager.debug.message(1, "Malformed URL on system identifier", sysid);
          }
  
          if (local != null) {
              return local.toString();
          } else {
              return sysid;
          }
      }
  
      /**
       * Perform character normalization on a URI reference.
       *
       * @param uriref The URI reference
       * @return The normalized URI reference.
       */
      protected String normalizeURI(String uriref) {
          String newRef = "";
          byte[] bytes;
  
          if (uriref == null) {
              return null;
          }
  
          try {
              bytes = uriref.getBytes("UTF-8");
          } catch (UnsupportedEncodingException uee) {
              // this can't happen
              catalogManager.debug.message(1, "UTF-8 is an unsupported encoding!?");
              return uriref;
          }
  
          for (int count = 0; count < bytes.length; count++) {
              int ch = bytes[count] & 0xFF;
  
              if ((ch <= 0x20)    // ctrl
                      || (ch > 0x7F)  // high ascii
                      || (ch == 0x22) // "
                      || (ch == 0x3C) // <
                      || (ch == 0x3E) // >
                      || (ch == 0x5C) // \
                      || (ch == 0x5E) // ^
                      || (ch == 0x60) // `
                      || (ch == 0x7B) // {
                      || (ch == 0x7C) // |
                      || (ch == 0x7D) // }
                      || (ch == 0x7F)) {
                  newRef += encodedByte(ch);
              } else {
                  newRef += (char) bytes[count];
              }
          }
  
          return newRef;
      }
  
      /**
       * Perform %-encoding on a single byte.
       *
       * @param b The 8-bit integer that represents th byte. (Bytes are signed
       *          but encoding needs to look at the bytes unsigned.)
       * @return The %-encoded string for the byte in question.
       */
      protected String encodedByte(int b) {
          String hex = Integer.toHexString(b).toUpperCase();
          if (hex.length() < 2) {
              return "%0" + hex;
          } else {
              return "%" + hex;
          }
      }
  
      // -----------------------------------------------------------------
  
      /**
       * Add to the current list of delegated catalogs.
       * <p/>
       * <p>This method always constructs the {@link #localDelegate}
       * vector so that it is ordered by length of partial
       * public identifier.</p>
       *
       * @param entry The DELEGATE catalog entry
       */
      protected void addDelegate(CatalogEntry entry) {
          int pos = 0;
          String partial = entry.getEntryArg(0);
  
          Enumeration local = localDelegate.elements();
          while (local.hasMoreElements()) {
              CatalogEntry dpe = (CatalogEntry) local.nextElement();
              String dp = dpe.getEntryArg(0);
              if (dp.equals(partial)) {
                  // we already have this prefix
                  return;
              }
              if (dp.length() > partial.length()) {
                  pos++;
              }
              if (dp.length() < partial.length()) {
                  break;
              }
          }
  
          // now insert partial into the vector at [pos]
          if (localDelegate.size() == 0) {
              localDelegate.addElement(entry);
          } else {
              localDelegate.insertElementAt(entry, pos);
          }
      }
  }
  
  