Index: src/com/arbortext/catalog/InvalidCatalogEntryException.java
===================================================================
--- src/com/arbortext/catalog/InvalidCatalogEntryException.java	(nonexistent)
+++ src/com/arbortext/catalog/InvalidCatalogEntryException.java	(revision 71)
@@ -0,0 +1,21 @@
+// InvalidCatalogEntryException.java - Bad Catalog entry type
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal bad Catalog entry</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to create
+ * a CatalogEntry instance with the wrong number of arguments or
+ * an obviously erroneous argument.</p>
+ */
+public class InvalidCatalogEntryException extends Exception {
+    public InvalidCatalogEntryException() { super(); }
+}
+

Property changes on: src/com/arbortext/catalog/InvalidCatalogEntryException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/UnknownCatalogFormatException.java
===================================================================
--- src/com/arbortext/catalog/UnknownCatalogFormatException.java	(nonexistent)
+++ src/com/arbortext/catalog/UnknownCatalogFormatException.java	(revision 71)
@@ -0,0 +1,19 @@
+// UnknownCatalogFormatException.java - Unknown XML Catalog format
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal unknown XML Catalog format.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an XML Catalog is loaded and the
+ * root element of the catalog file is unrecognized.</p>
+ */
+public class UnknownCatalogFormatException extends Exception {
+    public UnknownCatalogFormatException() { super(); }
+}

Property changes on: src/com/arbortext/catalog/UnknownCatalogFormatException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/InvalidCatalogEntryTypeException.java
===================================================================
--- src/com/arbortext/catalog/InvalidCatalogEntryTypeException.java	(nonexistent)
+++ src/com/arbortext/catalog/InvalidCatalogEntryTypeException.java	(revision 71)
@@ -0,0 +1,19 @@
+// InvalidCatalogEntryException.java - Bad Catalog entry type
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal bad Catalog entry type</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to create
+ * a CatalogEntry instance using an invalid entry type.</p>
+ */
+public class InvalidCatalogEntryTypeException extends Exception {
+    public InvalidCatalogEntryTypeException() { super(); }
+}

Property changes on: src/com/arbortext/catalog/InvalidCatalogEntryTypeException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/CatalogEntityResolver.java
===================================================================
--- src/com/arbortext/catalog/CatalogEntityResolver.java	(nonexistent)
+++ src/com/arbortext/catalog/CatalogEntityResolver.java	(revision 71)
@@ -0,0 +1,317 @@
+// CatalogEntityResolver.java - SAX entityResolver using OASIS Catalogs
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Integer;
+import java.net.URL;
+import java.net.MalformedURLException;
+
+import com.arbortext.catalog.Catalog;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+
+/**
+ * <p>Implements SAX entityResolver using OASIS Open Catalogs.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class implements the SAX entityResolver interface. It uses
+ * OASIS Open catalog files to provide a facility for mapping public
+ * or system identifiers in source documents to local system identifiers.
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ * </dl>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class CatalogEntityResolver implements EntityResolver {
+    /**
+     * <p>The debug level</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * <p>Indicates that unusable system identifiers should be ignored.</p>
+     */
+    private boolean retryBadSystemIds = false;
+
+    /**
+     * <p>The OASIS Open Catalog used for entity resolution.</p>
+     *
+     * <p>This field is exposed so that the catalog can be updated
+     * after creating the instance of CatalogEntityResolver that will
+     * be used by the parser.</p>
+     */
+    public Catalog catalog = null;
+
+    /**
+     * <p>Constructs a CatalogEntityResolver with an empty catalog.</p>
+     */
+    public CatalogEntityResolver() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+    }
+
+    /**
+     * <p>Set the Catalog that will be used to resolve entities.</p>
+     *
+     * <p>This is a convenience method for setting the
+     * <a href="#catalog"><code>catalog</code></a> field.</p>
+     */
+    public void setCatalog(Catalog cat) {
+	catalog = cat;
+    }
+
+    /**
+     * <p>Parse a Catalog file.</p>
+     *
+     * <p>This is really just a convenience method which calls
+     * <a href="#catalog"><code>catalog.parseCatalog()</code></a>.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	catalog.parseCatalog(fileName);
+    }
+
+    /**
+     * <p>Establish whether or not bad system identifiers should be
+     * ignored.</p>
+     *
+     * <p>The semantics of catalog file lookup are such that if a system
+     * identifier is supplied in the instance document, it is possible
+     * that it will be used in preference to alternative system identifiers
+     * in the catalog.</p>
+     *
+     * <p>If this variable is <code>true</code> and the system identifier
+     * passed to the entity resolver would be returned, the entity resolver
+     * attempts to open it. If it cannot be opened, the resolver
+     * does another catalog search,
+     * ignoring the fact that a system identifier was specified. If this
+     * second search locates a system identifer, it will be returned.</p>
+     *
+     * <p>This setting is initially <code>false</code> meaning that
+     * system identifiers
+     * in the document will be used in preference to some entries in
+     * the catalog.</p>
+     *
+     * @param retry If true, the resolver will retry Catalog lookups when
+     * the supplied system identifer cannot be opened.
+     */
+    public void setRetry(boolean retry) {
+	retryBadSystemIds = retry;
+    }
+
+    /**
+     * <p>Implements the <code>resolveEntity</code> method
+     * for the SAX interface.</p>
+     *
+     * <p>Presented with an optional public identifier and a system
+     * identifier, this function attempts to locate a mapping in the
+     * catalogs.</p>
+     *
+     * <p>If such a mapping is found, the resolver attempts to open
+     * the mapped value as an InputSource and return it. Exceptions are
+     * ignored and null is returned if the mapped value cannot be opened
+     * as an input source.</p>
+     *
+     * If no mapping is found (or an error occurs attempting to open
+     * the mapped value as an input source), null is returned and the system
+     * will use the specified system identifier as if no entityResolver
+     * was specified.</p>
+     *
+     * @param publicId  The public identifier for the entity in question.
+     * This may be null.
+     *
+     * @param systemId  The system identifier for the entity in question.
+     * XML requires a system identifier on all external entities, so this
+     * value is always specified.
+     *
+     * @return An InputSource for the mapped identifier, or null.
+     */
+    public InputSource resolveEntity (String publicId, String systemId) {
+	String resolved = null;
+
+	if (systemId != null) {
+	    try {
+		resolved = catalog.resolveSystem(systemId);
+	    } catch (MalformedURLException me) {
+		debug(1, "Malformed URL exception trying to resolve",
+		      publicId);
+		resolved = null;
+	    } catch (IOException ie) {
+		debug(1, "I/O exception trying to resolve", publicId);
+		resolved = null;
+	    }
+	}
+
+	if (resolved == null) {
+	    if (publicId != null) {
+		try {
+		    resolved = catalog.resolvePublic(publicId, systemId);
+		} catch (MalformedURLException me) {
+		    debug(1, "Malformed URL exception trying to resolve",
+			  publicId);
+		} catch (IOException ie) {
+		    debug(1, "I/O exception trying to resolve", publicId);
+		}
+	    }
+
+	    if (resolved != null) {
+		debug(2, "Resolved", publicId, resolved);
+	    }
+	} else {
+	    debug(2, "Resolved", systemId, resolved);
+	}
+
+	if (resolved == null && retryBadSystemIds
+	    && publicId != null && systemId != null) {
+	    URL systemURL = null;
+	    try {
+		systemURL = new URL(systemId);
+	    } catch (MalformedURLException e) {
+		try {
+		    systemURL = new URL("file:///" + systemId);
+		} catch (MalformedURLException e2) {
+		    systemURL = null;
+		}
+	    }
+
+	    if (systemURL != null) {
+		try {
+		    InputStream iStream = systemURL.openStream();
+
+		    // The systemId can be opened, so that's the one that
+		    // we'll use. There's no point making the caller open
+		    // it again though...
+
+		    InputSource iSource = new InputSource(systemId);
+		    iSource.setPublicId(publicId);
+		    iSource.setByteStream(iStream);
+		    return iSource;
+		} catch (Exception e) {
+		    // nop
+		}
+	    }
+
+	    // we got here, so it must be that the systemId cannot be
+	    // opened and the caller wants us to retry...
+	    debug(2, "Failed to open", systemId);
+	    debug(2, "\tAttempting catalog lookup without system identifier.");
+	    return resolveEntity(publicId, null);
+	}
+
+	if (resolved != null) {
+	    try {
+		InputSource iSource = new InputSource(resolved);
+		iSource.setPublicId(publicId);
+
+		// Ideally this method would not attempt to open the
+		// InputStream, but there is a bug (in Xerces, at least)
+		// that causes the parser to mistakenly open the wrong
+		// system identifier if the returned InputSource does
+		// not have a byteStream.
+		//
+		// It could be argued that we still shouldn't do this here,
+		// but since the purpose of calling the entityResolver is
+		// almost certainly to open the input stream, it seems to
+		// do little harm.
+		//
+		URL url = new URL(resolved);
+		InputStream iStream = url.openStream();
+		iSource.setByteStream(iStream);
+
+		return iSource;
+	    } catch (Exception e) {
+		debug(1, "Failed to create InputSource", resolved);
+		return null;
+	    }
+	}
+	return null;
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     */
+    private void debug(int level, String message) {
+	if (debug >= level) {
+	    System.out.println(message);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec An argument to the message.
+     */
+    private void debug(int level, String message, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec1 An argument to the message.
+     * @param spec1 Another argument to the message.
+     */
+    private void debug(int level, String message, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+}

Property changes on: src/com/arbortext/catalog/CatalogEntityResolver.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/NoXMLParserException.java
===================================================================
--- src/com/arbortext/catalog/NoXMLParserException.java	(nonexistent)
+++ src/com/arbortext/catalog/NoXMLParserException.java	(revision 71)
@@ -0,0 +1,22 @@
+// NoXMLParserException.java - Attempt to parse XML Catalog with no Parser
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal attempt to parse an XML Catalog without a Parser class.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to load an XML
+ * Catalog, but no Parser class has been provided.</p>
+ *
+ * @see XMLCatalogReader
+ * @see XMLCatalogReader#setParserClass
+ */
+public class NoXMLParserException extends Exception {
+    public NoXMLParserException() { super(); }
+}

Property changes on: src/com/arbortext/catalog/NoXMLParserException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/Catalog.java
===================================================================
--- src/com/arbortext/catalog/Catalog.java	(nonexistent)
+++ src/com/arbortext/catalog/Catalog.java	(revision 71)
@@ -0,0 +1,1406 @@
+// Catalog.java - Represents OASIS Open Catalog files.
+
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.lang.Integer;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Vector;
+import java.net.URL;
+import java.net.MalformedURLException;
+import com.arbortext.catalog.CatalogReader;
+import com.arbortext.catalog.XMLCatalogReader;
+import com.arbortext.catalog.NotXMLCatalogException;
+import com.arbortext.catalog.NoXMLParserException;
+import org.xml.sax.SAXException;
+
+/**
+ * <p>Represents OASIS Open Catalog files.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class loads one or more 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>)
+ * and provides
+ * methods for implementing the Catalog semantics.</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>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>This class processes the following Catalog entries:</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</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>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>
+ * </ul>
+ *
+ * <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>If provided with an SAX Parser class, this object can also load
+ * XML Catalogs. For the details about which XML Catalog formats are
+ * recognized, see {@link XMLCatalogReader}.
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ *
+ * <dt><b>xml.catalog.override</b></dt>
+ * <dd><p>Specifies the default override behavior. If override is true ("true",
+ * "yes", "1"), system identifiers in the catalog file are used in preference
+ * to system identifiers in the document. In other words, a value of false
+ * essentially disables catalog processing since almost all external
+ * entities are required to have a system identifier in XML.
+ * A value of true is assumed if the property is not set.</p></dd>
+ *
+ * <dt><b>xml.catalog.files</b></dt>
+ * <dd><p>Identifies the list of catalog <i>files</i> to parse initially.
+ * (Additional catalog files may be parsed if the CATALOG entry
+ * is used.) Components of the list should be separated by the system
+ * property "<code>path.separator</code>" character
+ * (typically ";" on DOS/Windows systems, ":" on Unix systems).</p>
+ *
+ * <p>Additional catalogs may also be loaded with the
+ * {@link #parseCatalog} method.</p>
+ * </dd>
+ * </dl>
+ *
+ * <p><b>Change Log:</b></p>
+ * <dl>
+ * <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>
+ *
+ * @see CatalogReader
+ * @see XMLCatalogReader
+ * @see CatalogEntry
+ *
+ * @author Abortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0.1
+ */
+public class Catalog {
+    /**
+     * The base URI for relative system identifiers in the catalog.
+     * This may be changed by BASE entries in the catalog.
+     */
+    private URL base;
+
+    /**
+     * The base URI of the Catalog file currently being parsed.
+     */
+    private URL catalogCwd;
+
+    /** The catalog entries currently known to the system. */
+    private Vector catalogEntries = new Vector();
+
+    /** The default initial override setting. */
+    private boolean default_override = true;
+
+    /**
+     * <p>The debug level.</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, more messages
+     * <li>3, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * <p>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
+     */
+    private Vector catalogFiles = new Vector();
+
+    /**
+     * <p>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>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
+     */
+    private Vector localCatalogFiles = new Vector();
+
+    /**
+     * <p>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>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>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>
+     */
+    private Vector catalogs = new Vector();
+
+    /**
+     * <p>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>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>
+     */
+    private Vector localDelegate = new Vector();
+
+    /**
+     * <p>The name of the parser class to load when parsing XML Catalogs.</p>
+     *
+     * <p>If a parser class is provided,
+     * subsequent attempts to parse Catalog files will begin
+     * by attemptiing an XML parse of the catalog file using a parser
+     * of this class.
+     * If the XML parse fails, the "default" text parse will be done
+     * instead.</p>
+     */
+    private String parserClass = null;
+
+    /**
+     * <p>Constructs an empty Catalog.</p>
+     *
+     * <p>The constructor interrogates the relevant system properties
+     * and initializes the catalog data structures.</p>
+     */
+    public Catalog() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+
+	property = System.getProperty("xml.catalog.override");
+
+	if (property != null) {
+	    default_override = (property.equalsIgnoreCase("true")
+				|| property.equalsIgnoreCase("yes")
+				|| property.equalsIgnoreCase("1"));
+	}
+    }
+
+    /**
+     * <p>Sets the parser class, enabling XML Catalog parsing.</p>
+     *
+     * <p>Sets the parser class that will be used for loading XML Catalogs.
+     * If this method is not called, all catalogs will be parsed as
+     * plain text (and assumed to conform to the
+     * <a href="http://www.oasis-open.org/html/a401.htm">OASIS Catalog
+     * format</a>).</p>
+     *
+     * @param parser The name of a class implementing the SAX Parser
+     * interface to be used for subsequent XML Catalog parsing.
+     */
+    public void setParserClass(String parser) {
+	parserClass = parser;
+    }
+
+    /**
+     * <p>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 MalformedURLException, IOException {
+	String PCS = System.getProperty("path.separator");
+	String catalog_files = System.getProperty("xml.catalog.files");
+
+	while (catalog_files != null) {
+	    int pos = catalog_files.indexOf(PCS);
+	    String catfile = null;
+
+	    if (pos > 0) {
+		catfile = catalog_files.substring(0, pos);
+		catalog_files = catalog_files.substring(pos+1);
+	    } else {
+		catfile = catalog_files;
+		catalog_files = null;
+	    }
+
+	    catalogFiles.addElement(catfile);
+	}
+
+	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);
+	}
+    }
+
+    /**
+     * <p>Parse a catalog file, augmenting internal data structures</p>
+     *
+     * @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 MalformedURLException, IOException {
+
+	// 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 files on the catalogFiles vector. This
+	// vector can grow during processing if CATALOG entries are
+	// encountered in the catalog
+	int curCat = 0;
+	while (curCat < catalogFiles.size()) {
+	    String catfile = (String) catalogFiles.elementAt(curCat++);
+
+	    if (catalogEntries.size() == 0 && catalogs.size() == 0) {
+		// We haven't parsed any catalogs yet, let this
+		// catalog be the first...
+		parseCatalogFile(catfile);
+	    } 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
+		while (curCat < catalogFiles.size()) {
+		    catfile = (String) catalogFiles.elementAt(curCat++);
+		    newQueue.addElement(catfile);
+		}
+
+		localCatalogFiles = new Vector();
+		catalogFiles = newQueue;
+		curCat = 0;
+	    }
+
+	    if (!localDelegate.isEmpty()) {
+		Enumeration e = localDelegate.elements();
+		while (e.hasMoreElements()) {
+		    catalogEntries.addElement(e.nextElement());
+		}
+		localDelegate = new Vector();
+	    }
+	}
+
+	// We've parsed them all, reinit the vector...
+	catalogFiles = new Vector();
+    }
+
+    /**
+     * <p>Parse a single catalog file, augmenting internal data structures</p>
+     *
+     * @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.
+     */
+    private synchronized void parseCatalogFile(String fileName)
+	throws MalformedURLException, IOException {
+
+	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
+	    String userdir = fixSlashes(System.getProperty("user.dir"));
+	    catalogCwd = new URL("file:///" + userdir + "/basename");
+	} catch (MalformedURLException e) {
+	    String userdir = fixSlashes(System.getProperty("user.dir"));
+	    debug(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) {
+		debug(1, "Malformed URL on catalog filename", 
+		      fixSlashes(fileName));
+		base = null;
+	    }
+	}
+
+	debug(1, "Loading catalog", fileName);
+	debug(3, "Default BASE", base.toString());
+
+	fileName = base.toString();
+
+	if (parserClass != null) {
+	    try {
+		XMLCatalogReader catfile = new XMLCatalogReader();
+		catfile.setParserClass(parserClass);
+		catfile.parseCatalog(fileName);
+
+		CatalogEntry ce = null;
+		while ((ce = catfile.nextEntry()) != null) {
+		    addEntry(ce);
+		}
+		return;
+	    } catch (SAXException e1) {
+		// not an XML catalog, continue with text parse
+	    } catch (NoXMLParserException e2) {
+		// not an XML catalog, continue with text parse
+	    } catch (NotXMLCatalogException e2) {
+		// not an XML catalog, continue with text parse
+	    } catch (InstantiationException e3) {
+		debug(1, "Cannot instantiate XML Parser class", parserClass);
+	    } catch (IllegalAccessException e4) {
+		debug(1, "Cannot access XML Parser class", parserClass);
+	    } catch (ClassNotFoundException e5) {
+		debug(1, "Cannot load XML Parser class", parserClass);
+	    } catch (UnknownCatalogFormatException e6) {
+		debug(1, "Unrecognized XML Catalog format.");
+		return;
+	    }
+	}
+
+	CatalogReader catfile = new CatalogReader();
+	catfile.parseCatalog(fileName);
+
+	// Process the contents of the catalog file as a whitespace
+	// delimited set of tokens
+	while ((entry = catfile.nextEntry()) != null) {
+	    addEntry(entry);
+	}
+    }
+
+    /**
+     * <p>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.
+     */
+    private void addEntry(CatalogEntry entry) {
+	switch (entry.entryType()) {
+	case CatalogEntry.BASE: {
+	    String value = entry.formalSystemIdentifier();
+	    URL newbase = null;
+
+	    debug(3, "BASE", value);
+
+	    try {
+		value = fixSlashes(value);
+		newbase = new URL(catalogCwd, value);
+	    } catch (MalformedURLException e) {
+		try {
+		    newbase = new URL("file:///" + value);
+		} catch (MalformedURLException e2) {
+		    debug(1, "Malformed URL on base", value);
+		    newbase = null;
+		}
+	    }
+
+	    if (newbase != null) {
+		base = newbase;
+	    }
+
+	    break;
+	}
+
+	case CatalogEntry.CATALOG: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+
+	    debug(3, "CATALOG", fsi);
+
+	    localCatalogFiles.addElement(fsi);
+	    break;
+	}
+
+	case CatalogEntry.DOCUMENT: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "DOCUMENT", fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.OVERRIDE: {
+	    debug(3, "OVERRIDE", entry.yes_or_no());
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.SGMLDECL: {
+	    // meaningless in XML
+	    break;
+	}
+	case CatalogEntry.DELEGATE: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "DELEGATE", entry.partialPublicId(), fsi);
+
+	    addDelegate(entry);
+	    break;
+	}
+	case CatalogEntry.DOCTYPE: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "DOCTYPE", entry.publicId(), fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.DTDDECL: {
+	    // meaningless in XML
+	    break;
+	}
+	case CatalogEntry.ENTITY: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "ENTITY", entry.entityName(), fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.LINKTYPE: {
+	    // meaningless in XML
+	    break;
+	}
+	case CatalogEntry.NOTATION: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "NOTATION", entry.entityName(), fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.PUBLIC: {
+	    // This entry has to go in the vector because it would
+	    // be relevant in subsequent searches for notations.
+	    String publicid = entry.publicId();
+	    String systemid = makeAbsolute(entry.formalSystemIdentifier());
+
+	    debug(3, "PUBLIC", publicid, systemid);
+
+	    entry.updateFormalSystemIdentifier(systemid);
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.SYSTEM: {
+	    String systemid = entry.systemId();
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+
+	    debug(3, "SYSTEM", systemid, fsi);
+
+	    entry.updateFormalSystemIdentifier(fsi);
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	}
+    }
+
+    /**
+     * <p>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>
+     *
+     * <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>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 MalformedURLException, 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 = new Catalog();
+		c.setParserClass(parserClass);
+		c.debug = debug;
+
+		c.parseCatalog(catfile);
+		catalogs.setElementAt(c, catPos);
+		c.parseAllCatalogs();
+	    }
+	}
+
+	// Parse all the DELEGATE catalogs
+	Enumeration enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.DELEGATE) {
+		Catalog dcat = new Catalog();
+		dcat.setParserClass(parserClass);
+		dcat.debug = debug;
+		dcat.parseCatalog(e.formalSystemIdentifier());
+	    }
+	}
+    }
+
+
+    /**
+     * <p>Return the applicable DOCTYPE system identifier.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	String resolved = 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(CatalogEntry.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.DOCTYPE
+		&& e.entityName().equals(entityName)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.DOCTYPE,
+					  entityName,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>Return the applicable DOCUMENT entry.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	// If there's a DOCUMENT entry, return it
+	Enumeration enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.DOCUMENT) {
+		return e.formalSystemIdentifier();
+	    }
+	}
+
+	return resolveSubordinateCatalogs(CatalogEntry.DOCUMENT,
+					  null, null, null);
+    }
+
+    /**
+     * <p>Return the applicable ENTITY system identifier.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	String resolved = 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(CatalogEntry.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.ENTITY
+		&& e.entityName().equals(entityName)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.ENTITY,
+					  entityName,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>Return the applicable NOTATION system identifier.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	String resolved = 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(CatalogEntry.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.NOTATION
+		&& e.entityName().equals(notationName)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.NOTATION,
+					  notationName,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>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).
+     *
+     * @throws MalformedURLException The formal system identifier of a
+     * subordinate catalog cannot be turned into a valid URL.
+     * @throws IOException Error reading subordinate catalog file.
+     *
+     * @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.
+     */
+    public String resolvePublic(String publicId, String systemId) 
+	throws MalformedURLException, IOException {
+
+	// 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(CatalogEntry.PUBLIC,
+					     null,
+					     publicId,
+					     systemId);
+	if (resolved != null) {
+	    return resolved;
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.PUBLIC,
+					  null,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>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>There are four possible cases:</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).
+     *
+     * @throws MalformedURLException The formal system identifier of a
+     * delegated catalog cannot be turned into a valid URL.
+     * @throws IOException Error reading delegated catalog file.
+     *
+     * @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.
+     */
+    private synchronized String resolveLocalPublic(int entityType,
+				      String entityName,
+				      String publicId,
+				      String systemId)
+	throws MalformedURLException, IOException {
+
+	// Always normalize the public identifier before attempting a match
+	publicId = CatalogReader.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.PUBLIC
+		&& e.publicId().equals(publicId)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// If there's a DELEGATE entry in this catalog, use it
+	over = default_override;
+	enum = catalogEntries.elements();
+	Vector delCats = new Vector();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.DELEGATE
+		&& (over || systemId == null)) {
+		String p = (String) e.partialPublicId();
+		if (p.length() <= publicId.length()
+		    && p.equals(publicId.substring(0, p.length()))) {
+		    // delegate this match to the other catalog
+
+		    delCats.addElement(e.formalSystemIdentifier());
+		}
+	    }
+	}
+
+	if (delCats.size() > 0) {
+	    Enumeration enumCats = delCats.elements();
+
+	    if (debug > 0) {
+		debug(1, "Switching to delegated catalog(s):");
+		while (enumCats.hasMoreElements()) {
+		    String delegatedCatalog = (String) enumCats.nextElement();
+		    debug(1, "\t" + delegatedCatalog);
+		}
+	    }
+
+	    Catalog dcat = new Catalog();
+	    dcat.setParserClass(parserClass);
+	    dcat.debug = debug;
+
+	    enumCats = delCats.elements();
+	    while (enumCats.hasMoreElements()) {
+		String delegatedCatalog = (String) enumCats.nextElement();
+		dcat.parseCatalog(delegatedCatalog);
+	    }
+
+	    return dcat.resolvePublic(publicId, null);
+	}
+
+	// Nada!
+	return null;
+    }
+
+    /**
+     * <p>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>The caller is responsible for doing any necessary
+     * normalization of the system identifier before calling
+     * this method. For example, a relative system identifier in
+     * a document might be converted to an absolute system identifier
+     * before attempting to resolve it.</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 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 resolveSystem(String systemId)
+	throws MalformedURLException, IOException {
+
+	// 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(CatalogEntry.SYSTEM,
+					  null,
+					  null,
+					  systemId);
+    }
+
+    /**
+     * <p>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
+     */
+    private String resolveLocalSystem(String systemId) {
+	String osname = System.getProperty("os.name");
+	boolean windows = (osname.indexOf("Windows") >= 0);
+	Enumeration enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.SYSTEM
+		&& (e.systemId().equals(systemId)
+		    || (windows
+			&& e.systemId().equalsIgnoreCase(systemId)))) {
+		return e.formalSystemIdentifier();
+	    }
+	}
+	return null;
+    }
+
+
+    /**
+     * <p>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).
+     *
+     * @throws MalformedURLException The formal system identifier of a
+     * delegated catalog cannot be turned into a valid URL.
+     * @throws IOException Error reading delegated catalog file.
+     *
+     * @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.
+     */
+    private synchronized String resolveSubordinateCatalogs(int entityType,
+					      String entityName,
+					      String publicId,
+					      String systemId)
+	throws MalformedURLException, 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 = new Catalog();
+		c.setParserClass(parserClass);
+		c.debug = debug;
+
+		try {
+		    c.parseCatalog(catfile);
+		} catch (MalformedURLException mue) {
+		    debug(1, "Malformed Catalog URL", catfile);
+		} catch (FileNotFoundException fnfe) {
+		    debug(1, "Failed to load catalog, file not found",
+			  catfile);
+		} catch (IOException ioe) {
+		    debug(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?
+	    switch (entityType) {
+	    case CatalogEntry.DOCTYPE: {
+		resolved = c.resolveDoctype(entityName,
+					    publicId,
+					    systemId);
+		break;
+	    }
+	    case CatalogEntry.DOCUMENT: {
+		resolved = c.resolveDocument();
+		break;
+	    }
+	    case CatalogEntry.ENTITY: {
+		resolved = c.resolveEntity(entityName,
+					   publicId,
+					   systemId);
+		break;
+	    }
+	    case CatalogEntry.NOTATION: {
+		resolved = c.resolveNotation(entityName,
+					     publicId,
+					     systemId);
+		break;
+	    }
+	    case CatalogEntry.PUBLIC: {
+		resolved = c.resolvePublic(publicId, systemId);
+		break;
+	    }
+	    case CatalogEntry.SYSTEM: {
+		resolved = c.resolveSystem(systemId);
+		break;
+	    }
+	    }
+
+	    if (resolved != null) {
+		return resolved;
+	    }
+	}
+
+	return null;
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Replace backslashes with forward slashes. (URLs always use
+     * forward slashes.)</p>
+     *
+     * @param sysid The input system identifier.
+     * @return The same system identifier with backslashes turned into
+     * forward slashes.
+     */
+    private String fixSlashes (String sysid) {
+	return sysid.replace('\\', '/');
+    }
+
+    /**
+     * <p>Construct an absolute URI from a relative one, using the current
+     * base URI.</p>
+     *
+     * @param sysid The (possibly relative) system identifier
+     * @return The system identifier made absolute with respect to the
+     * current {@link #base}.
+     */
+    private String makeAbsolute(String sysid) {
+	URL local = null;
+
+	sysid = fixSlashes(sysid);
+
+	try {
+	    local = new URL(base, sysid);
+	} catch (MalformedURLException e) {
+	    debug(1, "Malformed URL on system identifier", sysid);
+	}
+
+	if (local != null) {
+	    return local.toString();
+	} else {
+	    return sysid;
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     */
+    private void debug(int level, String message) {
+	if (debug >= level) {
+	    System.out.println(message);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec An argument to the message.
+     */
+    private void debug(int level, String message, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec1 An argument to the message.
+     * @param spec1 Another argument to the message.
+     */
+    private void debug(int level, String message, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>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
+     */
+    private void addDelegate(CatalogEntry entry) {
+	int pos = 0;
+	String partial = entry.partialPublicId();
+
+	Enumeration local = localDelegate.elements();
+	while (local.hasMoreElements()) {
+	    CatalogEntry dpe = (CatalogEntry) local.nextElement();
+	    String dp = dpe.partialPublicId();
+	    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);
+	}
+    }
+}

Property changes on: src/com/arbortext/catalog/Catalog.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/NotXMLCatalogException.java
===================================================================
--- src/com/arbortext/catalog/NotXMLCatalogException.java	(nonexistent)
+++ src/com/arbortext/catalog/NotXMLCatalogException.java	(revision 71)
@@ -0,0 +1,19 @@
+// NotXMLCatalogException.java - Not an XML catalog
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal attempt to parse a non-XML Catalog with an XML Parser.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to load an XML
+ * Catalog and it appears not to be XML.</p>
+ */
+public class NotXMLCatalogException extends Exception {
+    public NotXMLCatalogException() { super(); }
+}

Property changes on: src/com/arbortext/catalog/NotXMLCatalogException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/CatalogEntry.java
===================================================================
--- src/com/arbortext/catalog/CatalogEntry.java	(nonexistent)
+++ src/com/arbortext/catalog/CatalogEntry.java	(revision 71)
@@ -0,0 +1,253 @@
+// CatalogEntry.java - Represent OASIS Catalog entries
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import com.arbortext.catalog.InvalidCatalogEntryTypeException;
+import com.arbortext.catalog.InvalidCatalogEntryException;
+
+/**
+ * <p>Represents an OASIS Open Catalog entry.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>Instances of this class represent individual entries
+ * from an <a href="http://www.oasis-open.org/html/a401.htm">OASIS
+ * Open Catalog</a> file.</p>
+ * <p>While this could have been implemented as a base class with a
+ * separate subclass for each type of catalog entry, it didn't seem
+ * to be worth the extra overhead.</p>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class CatalogEntry {
+    // These have one argument in the catalog file
+
+    /** The entry type for a BASE entry */
+    public static final int BASE = 1;       // BASE fsispec
+    /** The entry type for a CATALOG entry */
+    public static final int CATALOG = 2;    // CATALOG fsispec
+    /** The entry type for a DOCUMENT entry */
+    public static final int DOCUMENT = 3;   // DOCUMENT fsispec
+    /** The entry type for a OVERRIDE entry */
+    public static final int OVERRIDE = 4;   // OVERRIDE (yes|no)
+    /** The entry type for a SGMLDECL entry */
+    public static final int SGMLDECL = 5;   // SGMLDECL fsispec
+
+    // These have two arguments in the catalog file
+    /** The entry type for a DELEGATE entry */
+    public static final int DELEGATE = 6;   // DELEGATE partialpublic fsispec
+    /** The entry type for a DOCTYPE entry */
+    public static final int DOCTYPE = 7;    // DOCTYPE entityname fsispec
+    /** The entry type for a DTDDECL entry */
+    public static final int DTDDECL = 8;    // DTDDECL publicid fsispec
+    /** The entry type for a ENTITY entry */
+    public static final int ENTITY = 9;     // ENTITY entityname fsispec
+    /** The entry type for a LINKTYPE entry */
+    public static final int LINKTYPE = 10;  // LINKTYPE entityname fsispec
+    /** The entry type for a NOTATION entry */
+    public static final int NOTATION = 11;  // NOTATION entityname fsispec
+    /** The entry type for a PUBLIC entry */
+    public static final int PUBLIC = 12;    // PUBLIC publicid fsispec
+    /** The entry type for a SYSTEM entry */
+    public static final int SYSTEM = 13;    // SYSTEM systemid fsispec
+
+    /** The entry type (one of BASE..SYSTEM) */
+    private int entryType = 0;
+
+    /** The first argument in a catalog entry */
+    private String spec1 = "";
+
+    /** The second argument in a catalog entry (usually an fsispec) */
+    private String spec2 = "";
+
+    /**
+     * <p>Construct a catalog entry of the specified type. The two-argument
+     * form of the constructor can be used for BASE, CATALOG, DOCUMENT,
+     * OVERRIDE, and SGMLDECL entries.</p>
+     *
+     * @param type The entry type.
+     * @param spec The argument to the entry, a formal system
+     * identifier in all cases except OVERRIDE when it must be either
+     * "yes" or "no".
+     */
+    public CatalogEntry(int type, String spec)
+	throws InvalidCatalogEntryTypeException,
+	       InvalidCatalogEntryException {
+
+	if (type < BASE || type > SYSTEM) {
+	    throw new InvalidCatalogEntryTypeException();
+	}
+
+	if (type > SGMLDECL) {
+	    throw new InvalidCatalogEntryException();
+	}
+
+	if (type == OVERRIDE
+	    && !(spec.equalsIgnoreCase("YES")
+		 || spec.equalsIgnoreCase("NO"))) {
+	    throw new InvalidCatalogEntryException();
+	}
+
+	entryType = type;
+	spec1 = spec;
+    }
+
+    /**
+     * <p>Construct a catalog entry of the specified type. The three-argument
+     * form of the constructor can be used for DELEGATE, DOCTYPE, DTDDECL,
+     * ENTITY, LINKTYPE, NOTATION, PUBLIC, and SYSTEM entries.</p>
+     *
+     * @param type The entry type.
+     * @param spec1 The first argument to the entry, usually an
+     * entity name or (partial) public identifier.
+     * @param spec2 The second argument to the entry, often a
+     * formal system identifier.
+     */
+    public CatalogEntry(int type, String spec, String fsispec)
+	throws InvalidCatalogEntryTypeException,
+	       InvalidCatalogEntryException {
+
+	if (type < BASE || type > SYSTEM) {
+	    throw new InvalidCatalogEntryTypeException();
+	}
+
+	if (type < DELEGATE) {
+	    throw new InvalidCatalogEntryException();
+	}
+
+	entryType = type;
+	spec1 = spec;
+	spec2 = fsispec;
+    }
+
+    /**
+     * <p>The entry type</p>
+     *
+     * @return The entry type
+     */
+    public int entryType() {
+	return entryType;
+    }
+
+    /**
+     * <p>The formal system identifier of the entry, if appropriate</p>
+     *
+     * @return The FSI for the entry, or null if it has no FSI.
+     */
+    public String formalSystemIdentifier() {
+	if (entryType > SGMLDECL) {
+	    return spec2;
+	} else {
+	    if (entryType != OVERRIDE) {
+		return spec1;
+	    } else {
+		return null;
+	    }
+	}
+    }
+
+    /**
+     * <p>The argument, YES or NO, of an OVERRIDE entry.</p>
+     *
+     * @return The YES or NO setting of an OVERRIDE entry,
+     * null otherwise.
+     */
+    public String yes_or_no() {
+	if (entryType != OVERRIDE) {
+	    return null;
+	} else {
+	    return spec1;
+	}
+    }
+
+    /**
+     * <p>The partial public identifier of a DELEGATE entry.</p>
+     *
+     * @return The partial public identifier of a DELEGATE entry,
+     * null otherwise.
+     */
+    public String partialPublicId() {
+	if (entryType != DELEGATE) {
+	    return null;
+	} else {
+	    return spec1;
+	}
+    }
+
+    /**
+     * <p>The entity name</p>
+     *
+     * @return The entity name of a DOCTYPE, ENTITY, LINKTYPE, or
+     * NOTATION entry,
+     * null otherwise.
+     */
+    public String entityName() {
+	if (entryType == DOCTYPE
+	    || entryType == ENTITY
+	    || entryType == LINKTYPE
+	    || entryType == NOTATION) {
+	    return spec1;
+	} else {
+	    return null;
+	}
+    }
+
+    /**
+     * <p>The public identifier</p>
+     *
+     * @return The public identifier of a DTDDECL or PUBLIC entry,
+     * null otherwise.
+     */
+    public String publicId() {
+	if (entryType == DTDDECL
+	    || entryType == PUBLIC) {
+	    return spec1;
+	} else {
+	    return null;
+	}
+    }
+
+    /**
+     * <p>The system identifier</p>
+     *
+     * @return The system identifier of a SYSTEM entry,
+     * null otherwise.
+     */
+    public String systemId() {
+	if (entryType != SYSTEM) {
+	    return null;
+	} else {
+	    return spec1;
+	}
+    }
+
+    /**
+     * <p>Update the formal system identifier</p>
+     *
+     * <p>The FSI initial specified in an entry may be relative (to
+     * the location of the catalog file or as modified by a BASE entry).
+     * A system processing catalog files
+     * (e.g. {@link com.arbortext.catalog.Catalog}),
+     * must be able to update the FSI in order to change it from a relative
+     * location to an absolute one.</p>
+     *
+     * @param newspec The new FSI
+     */
+    public void updateFormalSystemIdentifier(String newspec) {
+	if (entryType > SGMLDECL) {
+	    spec2 = newspec;
+	} else {
+	    spec1 = newspec;
+	}
+    }
+}

Property changes on: src/com/arbortext/catalog/CatalogEntry.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/CatalogReader.java
===================================================================
--- src/com/arbortext/catalog/CatalogReader.java	(nonexistent)
+++ src/com/arbortext/catalog/CatalogReader.java	(revision 71)
@@ -0,0 +1,432 @@
+// CatalogReader.java - Read OASIS Catalog files
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.lang.Integer;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.net.URL;
+import java.net.MalformedURLException;
+import com.arbortext.catalog.CatalogEntry;
+import com.arbortext.catalog.InvalidCatalogEntryTypeException;
+import com.arbortext.catalog.InvalidCatalogEntryException;
+
+/**
+ * <p>Parses OASIS Open Catalog files.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class reads OASIS Open Catalog files, returning a stream
+ * of tokens.</p>
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ * </dl>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class CatalogReader {
+    // These are class variables so that several methods can access them
+    /** The filename (URL) of the catalog being read */
+    private String catfilename = null;
+
+    /** The input stream used to read the catalog */
+    private DataInputStream catfile = null;
+
+    /**
+     * Lookahead stack. Reading a catalog sometimes requires up to
+     * two characters of lookahead.
+     */
+    private int[] stack = new int[3];
+
+    /** The current position on the lookahead stack */
+    private int top = -1;
+
+    /**
+     * <p>The debug level</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * <p>Construct a CatalogReader object.</p>
+     */
+    public CatalogReader() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+    }
+
+    /**
+     * <p>Start parsing an OASIS Open Catalog file. The file is
+     * actually read and parsed
+     * as needed by <code>nextEntry</code>.</p>
+     *
+     * @param fileUrl  The URL or filename of the catalog file to process
+     *
+     * @throws MalformedURLException Improper fileUrl
+     * @throws IOException Error reading catalog file
+     */
+    public void parseCatalog(String fileUrl)
+	throws MalformedURLException, IOException {
+	catfilename = fileUrl;
+	URL catalog;
+
+	try {
+	    catalog = new URL(fileUrl);
+	} catch (MalformedURLException e) {
+	    catalog = new URL("file:///" + fileUrl);
+	}
+
+	try {
+	    catfile = new DataInputStream(catalog.openStream());
+	} catch (FileNotFoundException e) {
+	    debug(1, "Failed to load catalog, file not found", catalog.toString());
+	}
+    }
+
+    /**
+     * <p>The destructor.</p>
+     *
+     * <p>Makes sure the catalog file is closed.</p>
+     */
+    protected void finalize() throws IOException {
+	if (catfile != null) {
+	    catfile.close();
+	}
+	catfile = null;
+    }
+
+    /**
+     * <p>Get the next entry from the file</p>
+     *
+     * @throws IOException Error reading catalog file
+     * @return A CatalogEntry object for the next entry in the catalog
+     */
+    public CatalogEntry nextEntry() throws IOException {
+	if (catfile == null) {
+	    return null;
+	}
+
+	boolean confused = false;
+
+	while (true) {
+	    String token = nextToken();
+
+	    if (token == null) {
+		catfile.close();
+		catfile = null;
+		return null;
+	    }
+
+	    if (token.equalsIgnoreCase("BASE")
+		|| token.equalsIgnoreCase("CATALOG")
+		|| token.equalsIgnoreCase("DOCUMENT")
+		|| token.equalsIgnoreCase("OVERRIDE")
+		|| token.equalsIgnoreCase("SGMLDECL")) {
+		String spec = nextToken();
+		confused = false;
+
+		try {
+		    if (token.equalsIgnoreCase("BASE")) {
+			return new CatalogEntry(CatalogEntry.BASE, spec);
+		    }
+		    if (token.equalsIgnoreCase("CATALOG")) {
+			return new CatalogEntry(CatalogEntry.CATALOG, spec);
+		    }
+		    if (token.equalsIgnoreCase("DOCUMENT")) {
+			return new CatalogEntry(CatalogEntry.DOCUMENT, spec);
+		    }
+		    if (token.equalsIgnoreCase("OVERRIDE")) {
+			return new CatalogEntry(CatalogEntry.OVERRIDE, spec);
+		    }
+		    if (token.equalsIgnoreCase("SGMLDECL")) {
+			return new CatalogEntry(CatalogEntry.SGMLDECL, spec);
+		    }
+		} catch (InvalidCatalogEntryTypeException icete) {
+		    debug(1, "Invalid catalog entry type", token);
+		    confused = true;
+		} catch (InvalidCatalogEntryException icete) {
+		    debug(1, "Invalid catalog entry", token, spec);
+		    confused = true;
+		}
+	    }
+
+	    if (token.equalsIgnoreCase("DELEGATE")
+		|| token.equalsIgnoreCase("DOCTYPE")
+		|| token.equalsIgnoreCase("DTDDECL")
+		|| token.equalsIgnoreCase("ENTITY")
+		|| token.equalsIgnoreCase("LINKTYPE")
+		|| token.equalsIgnoreCase("NOTATION")
+		|| token.equalsIgnoreCase("PUBLIC")
+		|| token.equalsIgnoreCase("SYSTEM")) {
+		String spec1 = nextToken();
+		String spec2 = nextToken();
+		confused = false;
+		try {
+		    if (token.equalsIgnoreCase("DELEGATE")) {
+			return new CatalogEntry(CatalogEntry.DELEGATE,
+						normalize(spec1), spec2);
+		    }
+		    if (token.equalsIgnoreCase("DOCTYPE")) {
+			return new CatalogEntry(CatalogEntry.DOCTYPE,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("DTDDECL")) {
+			return new CatalogEntry(CatalogEntry.DTDDECL,
+						normalize(spec1), spec2);
+		    }
+		    if (token.equalsIgnoreCase("ENTITY")) {
+			return new CatalogEntry(CatalogEntry.ENTITY,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("LINKTYPE")) {
+			return new CatalogEntry(CatalogEntry.LINKTYPE,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("NOTATION")) {
+			return new CatalogEntry(CatalogEntry.NOTATION,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("PUBLIC")) {
+			return new CatalogEntry(CatalogEntry.PUBLIC,
+						normalize(spec1), spec2);
+		    }
+		    if (token.equalsIgnoreCase("SYSTEM")) {
+			return new CatalogEntry(CatalogEntry.SYSTEM,
+						spec1, spec2);
+		    }
+		} catch (InvalidCatalogEntryTypeException icete) {
+		    debug(1, "Invalid catalog entry type", token);
+		    confused = true;
+		} catch (InvalidCatalogEntryException icete) {
+		    debug(1, "Invalid catalog entry", token, spec1, spec2);
+		    confused = true;
+		}
+	    }
+
+	    if (!confused) {
+		if (debug > 1) {
+		    System.out.println("Unrecognized token parsing catalog: '"
+				       + catfilename
+				       + "': "
+				       + token);
+		    System.out.println("\tSkipping to next recognized token.");
+		}
+		confused = true;
+	    }
+	}
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Normalize a public identifier.</p>
+     *
+     * <p>Public identifiers must be normalized according to the following
+     * rules before comparisons between them can be made:</p>
+     *
+     * <ul>
+     * <li>Whitespace characters are normalized to spaces (e.g., line feeds,
+     * tabs, etc. become spaces).</li>
+     * <li>Leading and trailing whitespace is removed.</li>
+     * <li>Multiple internal whitespaces are normalized to a single
+     * space.</li>
+     * </ul>
+     *
+     * <p>This method is declared static so that other classes
+     * can use it directly.</p>
+     *
+     * @param publicId The unnormalized public identifier.
+     *
+     * @return The normalized identifier.
+     */
+    public static String normalize(String publicId) {
+	String normal = publicId.replace('\t', ' ');
+	normal = normal.replace('\r', ' ');
+	normal = normal.replace('\n', ' ');
+	normal = normal.trim();
+
+	int pos;
+
+	while ((pos = normal.indexOf("  ")) >= 0) {
+	    normal = normal.substring(0, pos) + normal.substring(pos+1);
+	}
+
+	return normal;
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Return the next token in the catalog file.</p>
+     *
+     * @return The Catalog file token from the input stream.
+     * @throws IOException If an error occurs reading from the stream.
+     */
+    private String nextToken() throws IOException {
+	String token = "";
+	int ch, nextch;
+
+	// Skip over leading whitespace and comments
+	while (true) {
+	    // skip leading whitespace
+	    ch = catfile.read();
+	    while (ch <= ' ') {      // all ctrls are whitespace
+		ch = catfile.read();
+		if (ch < 0) {
+		    return null;
+		}
+	    }
+
+	    // now 'ch' is the current char from the file
+	    nextch = catfile.read();
+	    if (nextch < 0) {
+		return null;
+	    }
+
+	    if (ch == '-' && nextch == '-') {
+		// we've found a comment, skip it...
+		ch = ' ';
+		nextch = nextChar();
+		while (ch != '-' || nextch != '-') {
+		    ch = nextch;
+		    nextch = nextChar();
+		}
+
+		// Ok, we've found the end of the comment,
+		// loop back to the top and start again...
+	    } else {
+		stack[++top] = nextch;
+		stack[++top] = ch;
+		break;
+	    }
+	}
+
+	ch = nextChar();
+	if (ch == '"' || ch == '\'') {
+	    int quote = ch;
+	    while ((ch = nextChar()) != quote) {
+		char[] chararr = new char[1];
+		chararr[0] = (char) ch;
+		String s = new String(chararr);
+		token = token.concat(s);
+	    }
+	    return token;
+	} else {
+	    // return the next whitespace or comment delimited
+	    // string
+	    while (ch > ' ') {
+		nextch = nextChar();
+		if (ch == '-' && nextch == '-') {
+		    stack[++top] = ch;
+		    stack[++top] = nextch;
+		    return token;
+		} else {
+		    char[] chararr = new char[1];
+		    chararr[0] = (char) ch;
+		    String s = new String(chararr);
+		    token = token.concat(s);
+		    ch = nextch;
+		}
+	    }
+	    return token;
+	}
+    }
+
+    /**
+     * <p>Return the next logical character from the input stream.</p>
+     *
+     * @return The next (logical) character from the input stream. The
+     * character may be buffered from a previous lookahead.
+     *
+     * @throws IOException If an error occurs reading from the stream.
+     */
+    private int nextChar() throws IOException {
+	if (top < 0) {
+	    return catfile.read();
+	} else {
+	    return stack[top--];
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     */
+    private void debug(int level, String message, String token) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec The argument to the token.
+     */
+    private void debug(int level, String message, String token, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec1 The first argument to the token.
+     * @param spec2 The second argument to the token.
+     */
+    private void debug(int level, String message,
+		       String token, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+}

Property changes on: src/com/arbortext/catalog/CatalogReader.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/XMLCatalogReader.java
===================================================================
--- src/com/arbortext/catalog/XMLCatalogReader.java	(nonexistent)
+++ src/com/arbortext/catalog/XMLCatalogReader.java	(revision 71)
@@ -0,0 +1,415 @@
+// XMLCatalogReader.java - Read XML Catalog files
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.lang.Integer;
+import java.util.Vector;
+import java.util.Enumeration;
+import java.io.IOException;
+import java.net.URL;
+import java.net.MalformedURLException;
+import com.arbortext.catalog.CatalogEntry;
+import com.arbortext.catalog.CatalogReader;
+import com.arbortext.catalog.InvalidCatalogEntryTypeException;
+import com.arbortext.catalog.InvalidCatalogEntryException;
+import com.arbortext.catalog.UnknownCatalogFormatException;
+import com.arbortext.catalog.NotXMLCatalogException;
+import com.arbortext.catalog.NoXMLParserException;
+
+import org.xml.sax.*;
+
+/**
+ * <p>Parses XML Catalog files.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class reads XML Catalog files, returning a stream
+ * of tokens. At present, it recognizes John Cowan's
+ * <a href="http://www.ccil.org/~cowan/XML/XCatalog.html">XML Catalogs</a>
+ * (formerly XCatalogs). In the future, additional XML Catalog formats
+ * may be supported.</p>
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ * </dl>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class XMLCatalogReader implements DocumentHandler {
+    // These are class variables so that several methods can access them
+    /** The filename (URL) of the catalog being read */
+    private String catfilename = null;
+
+    /**
+     * <p>The debug level</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * Indicates that the catalog type is not XML
+     */
+    private static final int NOTXMLCATALOG = -1;
+
+    /**
+     * Indicates that the catalog type is unknown.
+     */
+    private static final int UNKNOWNCATALOG = 0;
+
+    /**
+     * Indicates that the catalog type is an XML Catalog (John Cowan's
+     * XCatalog)
+     */
+    private static final int XCATALOG = 1;
+
+    /**
+     * Indicates the catalog type.
+     */
+    private int catalogType = NOTXMLCATALOG;
+
+    /**
+     * <p>The list of entries scanned from the catalog.</p>
+     *
+     * <p>The SAX Parser is event-driven, but the Catalog class expects
+     * to iterate through the entries with
+     * <a href="#nextToken()">nextToken()</a>. So this class builds a
+     * vector of entries during the parse and returns them sequentially
+     * when <a href="#nextToken()">nextToken()</a> is called.</p>
+     *
+     * @see Catalog
+     */
+    private Vector catalogEntries = new Vector();
+
+    /**
+     * An enumerator for walking through the list of catalogEntries.
+     */
+    private Enumeration catalogEnum = null;
+
+    /**
+     * <p>The name of the parser class to load when parsing XML Catalogs.</p>
+     *
+     * <p>If a parser class is provided,
+     * subsequent attempts to parse Catalog files will begin
+     * by attemptiing an XML parse of the catalog file using a parser
+     * of this class.
+     * If the XML parse fails, the "default" text parse will be done
+     * instead.</p>
+     */
+    private String parserClass = null;
+
+    /**
+     * <p>Construct an XMLCatalogReader object.</p>
+     */
+    public XMLCatalogReader() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+    }
+
+    /**
+     * <p>Sets the parser class, enabling XML Catalog parsing.</p>
+     *
+     * <p>Sets the parser class that will be used for loading XML Catalogs.
+     * If this method is not called, all attempts to use the
+     * <code>XMLCatalogParser</code> will fail, throwing a
+     * <code>NoXMLParserException</code>.</p>
+     *
+     * @param parser The name of a class implementing the SAX Parser
+     * interface to be used for subsequent XML Catalog parsing.
+     *
+     * @see com.arbortext.catalog.NoXMLParserException
+     */
+    public void setParserClass(String parser) {
+	parserClass = parser;
+    }
+
+    /**
+     * <p>Attempt to parse an XML Catalog file.</p>
+     *
+     * @param fileUrl   The URL or filename of the catalog file to process
+     * @param catParser A SAX-compliant parser to use for reading the files
+     *
+     * @throws SAXException Error parsing catalog file.
+     * @throws IOException Error reading catalog file.
+     * @throws NoXMLParserException No Parser class provided.
+     * @throws NotXMLCatalogException The Catalog appears not to be XML.
+     * @throws UnknownCatalogFormatException Unexpected XML catalog type.
+     * @throws ClassNotFoundException Parser class can't be found.
+     * @throws InstantiationException Parser class can't be instantiated.
+     * @throws IllegalAccessException Error instantiating parser class.
+     * @throws ClassCastException Parser class isn't a SAX Parser.
+     */
+    public void parseCatalog(String fileUrl)
+	throws SAXException, IOException,
+	       NotXMLCatalogException, NoXMLParserException,
+	       UnknownCatalogFormatException, ClassNotFoundException,
+	       InstantiationException, IllegalAccessException,
+	       ClassCastException {
+	// Create an instance of the parser
+	if (parserClass == null) {
+	    throw new NoXMLParserException();
+	}
+
+	Parser parser = (Parser) Class.forName(parserClass).newInstance();
+
+	catfilename = fileUrl;
+	parser.setDocumentHandler(this);
+	parser.parse(fileUrl);
+
+	if (catalogType == NOTXMLCATALOG) {
+	    // Why doesn't the attempt to parse this file throw a
+	    // SAX Exception???
+	    throw new NotXMLCatalogException();
+	}
+
+	if (catalogType == UNKNOWNCATALOG) {
+	    throw new UnknownCatalogFormatException();
+	}
+    }
+
+    /**
+     * <p>Get the next entry from the file</p>
+     *
+     * @throws IOException Error reading catalog file
+     * @return A CatalogEntry object for the next entry in the catalog
+     */
+    public CatalogEntry nextEntry() throws IOException {
+	if (catalogEnum == null) {
+	    catalogEnum = catalogEntries.elements();
+	}
+
+	if (catalogEnum.hasMoreElements()) {
+	    return (CatalogEntry) catalogEnum.nextElement();
+	} else {
+	    return null;
+	}
+    }
+
+    // ----------------------------------------------------------------------
+
+    /*
+     * <p>Parse elements from John Cowan's XML Catalog doctype.</p>
+     *
+     * <p>Each recognized element is turned into an appropriate
+     * CatalogEntry and put onto the entries vector for later
+     * retrieval.</p>
+     *
+     * @param name The name of the element.
+     * @param atts The list of attributes on the element.
+     *
+     * @see CatalogEntry
+     */
+    private void xCatalogEntry (String name, AttributeList atts) {
+	CatalogEntry ce = null;
+
+	try {
+	    if (name.equals("Base")) {
+		ce = new CatalogEntry(CatalogEntry.BASE,
+				      atts.getValue("HRef"));
+		debug(3, "Base", atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Delegate")) {
+		ce = new CatalogEntry(CatalogEntry.DELEGATE,
+				      CatalogReader.normalize(atts.getValue("PublicId")),
+				      atts.getValue("HRef"));
+		debug(3, "Delegate",
+		      CatalogReader.normalize(atts.getValue("PublicId")),
+		      atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Extend")) {
+		ce = new CatalogEntry(CatalogEntry.CATALOG,
+				      atts.getValue("HRef"));
+		debug(3, "Extend", atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Map")) {
+		ce = new CatalogEntry(CatalogEntry.PUBLIC,
+				      CatalogReader.normalize(atts.getValue("PublicId")),
+				      atts.getValue("HRef"));
+		debug(3, "Map",
+		      CatalogReader.normalize(atts.getValue("PublicId")),
+		      atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Remap")) {
+		ce = new CatalogEntry(CatalogEntry.SYSTEM,
+				      atts.getValue("SystemId"),
+				      atts.getValue("HRef"));
+		debug(3, "Remap",
+		      CatalogReader.normalize(atts.getValue("SystemId")),
+		      atts.getValue("HRef"));
+	    }
+
+	    if (ce == null) {
+		// This is equivalent to an invalid catalog entry type
+		debug(1, "Invalid catalog entry type", name);
+	    }
+	} catch (InvalidCatalogEntryTypeException icete) {
+	    debug(1, "Invalid catalog entry type", name);
+	} catch (InvalidCatalogEntryException icete) {
+	    debug(1, "Invalid catalog entry", name);
+	}
+
+	if (ce != null) {
+	    catalogEntries.addElement(ce);
+	}
+    }
+
+    // ----------------------------------------------------------------------
+    // Implement the SAX DocumentHandler interface
+
+    /** <p>The SAX <code>setDocumentLocator</code> method. Does nothing.</p> */
+    public void setDocumentLocator (Locator locator) {
+	return;
+    }
+
+    /** <p>The SAX <code>startDocument</code> method. Does nothing.</p> */
+    public void startDocument ()
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>endDocument</code> method. Does nothing.</p> */
+    public void endDocument ()
+	throws SAXException {
+	return;
+    }
+
+    /**
+     * <p>The SAX <code>startElement</code> method.</p>
+     *
+     * <p>This element attempts to identify the type of catalog
+     * by looking at the name of the first element encountered.
+     * If it recognizes the element, it sets the <code>catalogType</code>
+     * appropriately.</p>
+     *
+     * <p>After the catalog type has been identified, the appropriate
+     * entry parser is called for each subsequent element in the
+     * catalog.</p>
+     *
+     * @param name The name of the element.
+     * @param atts The list of attributes on the element.
+     *
+     */
+    public void startElement (String name, AttributeList atts)
+	throws SAXException {
+
+	if (catalogType == UNKNOWNCATALOG || catalogType == NOTXMLCATALOG) {
+	    if (name.equals("XMLCatalog")) {
+		catalogType = XCATALOG;
+		return;
+	    }
+	}
+
+	if (catalogType == XCATALOG) {
+	    xCatalogEntry(name, atts);
+	}
+    }
+
+    /** <p>The SAX <code>endElement</code> method. Does nothing.</p> */
+    public void endElement (String name)
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>characters</code> method. Does nothing.</p> */
+    public void characters (char ch[], int start, int length)
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>ignorableWhitespace</code> method. Does nothing.</p> */
+    public void ignorableWhitespace (char ch[], int start, int length)
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>processingInstruction</code> method. Does nothing.</p> */
+    public void processingInstruction (String target, String data)
+	throws SAXException {
+	return;
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     */
+    private void debug(int level, String message, String token) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec The argument to the token.
+     */
+    private void debug(int level, String message, String token, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec1 The first argument to the token.
+     * @param spec2 The second argument to the token.
+     */
+    private void debug(int level, String message,
+		       String token, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+}

Property changes on: src/com/arbortext/catalog/XMLCatalogReader.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: src/com/arbortext/catalog/package.html
===================================================================
--- src/com/arbortext/catalog/package.html	(nonexistent)
+++ src/com/arbortext/catalog/package.html	(revision 71)
@@ -0,0 +1,18 @@
+<html>
+<head>
+<title>Arbortext Catalog package</title>
+</head>
+<body>
+<p>Arbortext Catalog package</p>
+
+<p>This package implements reads and implements the semantics of
+OASIS Catalog files as 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>It also reads John Cowan's
+<a href="http://www.ccil.org/~cowan/XML/XCatalog.html">XML Catalogs</a>
+which are an XML representation of OASIS Catalogs.</p>
+
+</body>
+</html>

Property changes on: src/com/arbortext/catalog/package.html
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/InvalidCatalogEntryException.java
===================================================================
--- com/arbortext/catalog/InvalidCatalogEntryException.java	(nonexistent)
+++ com/arbortext/catalog/InvalidCatalogEntryException.java	(revision 71)
@@ -0,0 +1,21 @@
+// InvalidCatalogEntryException.java - Bad Catalog entry type
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal bad Catalog entry</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to create
+ * a CatalogEntry instance with the wrong number of arguments or
+ * an obviously erroneous argument.</p>
+ */
+public class InvalidCatalogEntryException extends Exception {
+    public InvalidCatalogEntryException() { super(); }
+}
+

Property changes on: com/arbortext/catalog/InvalidCatalogEntryException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/UnknownCatalogFormatException.java
===================================================================
--- com/arbortext/catalog/UnknownCatalogFormatException.java	(nonexistent)
+++ com/arbortext/catalog/UnknownCatalogFormatException.java	(revision 71)
@@ -0,0 +1,19 @@
+// UnknownCatalogFormatException.java - Unknown XML Catalog format
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal unknown XML Catalog format.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an XML Catalog is loaded and the
+ * root element of the catalog file is unrecognized.</p>
+ */
+public class UnknownCatalogFormatException extends Exception {
+    public UnknownCatalogFormatException() { super(); }
+}

Property changes on: com/arbortext/catalog/UnknownCatalogFormatException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/InvalidCatalogEntryTypeException.java
===================================================================
--- com/arbortext/catalog/InvalidCatalogEntryTypeException.java	(nonexistent)
+++ com/arbortext/catalog/InvalidCatalogEntryTypeException.java	(revision 71)
@@ -0,0 +1,19 @@
+// InvalidCatalogEntryException.java - Bad Catalog entry type
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal bad Catalog entry type</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to create
+ * a CatalogEntry instance using an invalid entry type.</p>
+ */
+public class InvalidCatalogEntryTypeException extends Exception {
+    public InvalidCatalogEntryTypeException() { super(); }
+}

Property changes on: com/arbortext/catalog/InvalidCatalogEntryTypeException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/CatalogEntityResolver.java
===================================================================
--- com/arbortext/catalog/CatalogEntityResolver.java	(nonexistent)
+++ com/arbortext/catalog/CatalogEntityResolver.java	(revision 71)
@@ -0,0 +1,317 @@
+// CatalogEntityResolver.java - SAX entityResolver using OASIS Catalogs
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Integer;
+import java.net.URL;
+import java.net.MalformedURLException;
+
+import com.arbortext.catalog.Catalog;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+
+/**
+ * <p>Implements SAX entityResolver using OASIS Open Catalogs.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class implements the SAX entityResolver interface. It uses
+ * OASIS Open catalog files to provide a facility for mapping public
+ * or system identifiers in source documents to local system identifiers.
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ * </dl>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class CatalogEntityResolver implements EntityResolver {
+    /**
+     * <p>The debug level</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * <p>Indicates that unusable system identifiers should be ignored.</p>
+     */
+    private boolean retryBadSystemIds = false;
+
+    /**
+     * <p>The OASIS Open Catalog used for entity resolution.</p>
+     *
+     * <p>This field is exposed so that the catalog can be updated
+     * after creating the instance of CatalogEntityResolver that will
+     * be used by the parser.</p>
+     */
+    public Catalog catalog = null;
+
+    /**
+     * <p>Constructs a CatalogEntityResolver with an empty catalog.</p>
+     */
+    public CatalogEntityResolver() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+    }
+
+    /**
+     * <p>Set the Catalog that will be used to resolve entities.</p>
+     *
+     * <p>This is a convenience method for setting the
+     * <a href="#catalog"><code>catalog</code></a> field.</p>
+     */
+    public void setCatalog(Catalog cat) {
+	catalog = cat;
+    }
+
+    /**
+     * <p>Parse a Catalog file.</p>
+     *
+     * <p>This is really just a convenience method which calls
+     * <a href="#catalog"><code>catalog.parseCatalog()</code></a>.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	catalog.parseCatalog(fileName);
+    }
+
+    /**
+     * <p>Establish whether or not bad system identifiers should be
+     * ignored.</p>
+     *
+     * <p>The semantics of catalog file lookup are such that if a system
+     * identifier is supplied in the instance document, it is possible
+     * that it will be used in preference to alternative system identifiers
+     * in the catalog.</p>
+     *
+     * <p>If this variable is <code>true</code> and the system identifier
+     * passed to the entity resolver would be returned, the entity resolver
+     * attempts to open it. If it cannot be opened, the resolver
+     * does another catalog search,
+     * ignoring the fact that a system identifier was specified. If this
+     * second search locates a system identifer, it will be returned.</p>
+     *
+     * <p>This setting is initially <code>false</code> meaning that
+     * system identifiers
+     * in the document will be used in preference to some entries in
+     * the catalog.</p>
+     *
+     * @param retry If true, the resolver will retry Catalog lookups when
+     * the supplied system identifer cannot be opened.
+     */
+    public void setRetry(boolean retry) {
+	retryBadSystemIds = retry;
+    }
+
+    /**
+     * <p>Implements the <code>resolveEntity</code> method
+     * for the SAX interface.</p>
+     *
+     * <p>Presented with an optional public identifier and a system
+     * identifier, this function attempts to locate a mapping in the
+     * catalogs.</p>
+     *
+     * <p>If such a mapping is found, the resolver attempts to open
+     * the mapped value as an InputSource and return it. Exceptions are
+     * ignored and null is returned if the mapped value cannot be opened
+     * as an input source.</p>
+     *
+     * If no mapping is found (or an error occurs attempting to open
+     * the mapped value as an input source), null is returned and the system
+     * will use the specified system identifier as if no entityResolver
+     * was specified.</p>
+     *
+     * @param publicId  The public identifier for the entity in question.
+     * This may be null.
+     *
+     * @param systemId  The system identifier for the entity in question.
+     * XML requires a system identifier on all external entities, so this
+     * value is always specified.
+     *
+     * @return An InputSource for the mapped identifier, or null.
+     */
+    public InputSource resolveEntity (String publicId, String systemId) {
+	String resolved = null;
+
+	if (systemId != null) {
+	    try {
+		resolved = catalog.resolveSystem(systemId);
+	    } catch (MalformedURLException me) {
+		debug(1, "Malformed URL exception trying to resolve",
+		      publicId);
+		resolved = null;
+	    } catch (IOException ie) {
+		debug(1, "I/O exception trying to resolve", publicId);
+		resolved = null;
+	    }
+	}
+
+	if (resolved == null) {
+	    if (publicId != null) {
+		try {
+		    resolved = catalog.resolvePublic(publicId, systemId);
+		} catch (MalformedURLException me) {
+		    debug(1, "Malformed URL exception trying to resolve",
+			  publicId);
+		} catch (IOException ie) {
+		    debug(1, "I/O exception trying to resolve", publicId);
+		}
+	    }
+
+	    if (resolved != null) {
+		debug(2, "Resolved", publicId, resolved);
+	    }
+	} else {
+	    debug(2, "Resolved", systemId, resolved);
+	}
+
+	if (resolved == null && retryBadSystemIds
+	    && publicId != null && systemId != null) {
+	    URL systemURL = null;
+	    try {
+		systemURL = new URL(systemId);
+	    } catch (MalformedURLException e) {
+		try {
+		    systemURL = new URL("file:///" + systemId);
+		} catch (MalformedURLException e2) {
+		    systemURL = null;
+		}
+	    }
+
+	    if (systemURL != null) {
+		try {
+		    InputStream iStream = systemURL.openStream();
+
+		    // The systemId can be opened, so that's the one that
+		    // we'll use. There's no point making the caller open
+		    // it again though...
+
+		    InputSource iSource = new InputSource(systemId);
+		    iSource.setPublicId(publicId);
+		    iSource.setByteStream(iStream);
+		    return iSource;
+		} catch (Exception e) {
+		    // nop
+		}
+	    }
+
+	    // we got here, so it must be that the systemId cannot be
+	    // opened and the caller wants us to retry...
+	    debug(2, "Failed to open", systemId);
+	    debug(2, "\tAttempting catalog lookup without system identifier.");
+	    return resolveEntity(publicId, null);
+	}
+
+	if (resolved != null) {
+	    try {
+		InputSource iSource = new InputSource(resolved);
+		iSource.setPublicId(publicId);
+
+		// Ideally this method would not attempt to open the
+		// InputStream, but there is a bug (in Xerces, at least)
+		// that causes the parser to mistakenly open the wrong
+		// system identifier if the returned InputSource does
+		// not have a byteStream.
+		//
+		// It could be argued that we still shouldn't do this here,
+		// but since the purpose of calling the entityResolver is
+		// almost certainly to open the input stream, it seems to
+		// do little harm.
+		//
+		URL url = new URL(resolved);
+		InputStream iStream = url.openStream();
+		iSource.setByteStream(iStream);
+
+		return iSource;
+	    } catch (Exception e) {
+		debug(1, "Failed to create InputSource", resolved);
+		return null;
+	    }
+	}
+	return null;
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     */
+    private void debug(int level, String message) {
+	if (debug >= level) {
+	    System.out.println(message);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec An argument to the message.
+     */
+    private void debug(int level, String message, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec1 An argument to the message.
+     * @param spec1 Another argument to the message.
+     */
+    private void debug(int level, String message, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+}

Property changes on: com/arbortext/catalog/CatalogEntityResolver.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/NoXMLParserException.java
===================================================================
--- com/arbortext/catalog/NoXMLParserException.java	(nonexistent)
+++ com/arbortext/catalog/NoXMLParserException.java	(revision 71)
@@ -0,0 +1,22 @@
+// NoXMLParserException.java - Attempt to parse XML Catalog with no Parser
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal attempt to parse an XML Catalog without a Parser class.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to load an XML
+ * Catalog, but no Parser class has been provided.</p>
+ *
+ * @see XMLCatalogReader
+ * @see XMLCatalogReader#setParserClass
+ */
+public class NoXMLParserException extends Exception {
+    public NoXMLParserException() { super(); }
+}

Property changes on: com/arbortext/catalog/NoXMLParserException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/Catalog.java
===================================================================
--- com/arbortext/catalog/Catalog.java	(nonexistent)
+++ com/arbortext/catalog/Catalog.java	(revision 71)
@@ -0,0 +1,1406 @@
+// Catalog.java - Represents OASIS Open Catalog files.
+
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.lang.Integer;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Vector;
+import java.net.URL;
+import java.net.MalformedURLException;
+import com.arbortext.catalog.CatalogReader;
+import com.arbortext.catalog.XMLCatalogReader;
+import com.arbortext.catalog.NotXMLCatalogException;
+import com.arbortext.catalog.NoXMLParserException;
+import org.xml.sax.SAXException;
+
+/**
+ * <p>Represents OASIS Open Catalog files.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class loads one or more 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>)
+ * and provides
+ * methods for implementing the Catalog semantics.</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>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>This class processes the following Catalog entries:</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</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>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>
+ * </ul>
+ *
+ * <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>If provided with an SAX Parser class, this object can also load
+ * XML Catalogs. For the details about which XML Catalog formats are
+ * recognized, see {@link XMLCatalogReader}.
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ *
+ * <dt><b>xml.catalog.override</b></dt>
+ * <dd><p>Specifies the default override behavior. If override is true ("true",
+ * "yes", "1"), system identifiers in the catalog file are used in preference
+ * to system identifiers in the document. In other words, a value of false
+ * essentially disables catalog processing since almost all external
+ * entities are required to have a system identifier in XML.
+ * A value of true is assumed if the property is not set.</p></dd>
+ *
+ * <dt><b>xml.catalog.files</b></dt>
+ * <dd><p>Identifies the list of catalog <i>files</i> to parse initially.
+ * (Additional catalog files may be parsed if the CATALOG entry
+ * is used.) Components of the list should be separated by the system
+ * property "<code>path.separator</code>" character
+ * (typically ";" on DOS/Windows systems, ":" on Unix systems).</p>
+ *
+ * <p>Additional catalogs may also be loaded with the
+ * {@link #parseCatalog} method.</p>
+ * </dd>
+ * </dl>
+ *
+ * <p><b>Change Log:</b></p>
+ * <dl>
+ * <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>
+ *
+ * @see CatalogReader
+ * @see XMLCatalogReader
+ * @see CatalogEntry
+ *
+ * @author Abortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0.1
+ */
+public class Catalog {
+    /**
+     * The base URI for relative system identifiers in the catalog.
+     * This may be changed by BASE entries in the catalog.
+     */
+    private URL base;
+
+    /**
+     * The base URI of the Catalog file currently being parsed.
+     */
+    private URL catalogCwd;
+
+    /** The catalog entries currently known to the system. */
+    private Vector catalogEntries = new Vector();
+
+    /** The default initial override setting. */
+    private boolean default_override = true;
+
+    /**
+     * <p>The debug level.</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, more messages
+     * <li>3, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * <p>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
+     */
+    private Vector catalogFiles = new Vector();
+
+    /**
+     * <p>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>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
+     */
+    private Vector localCatalogFiles = new Vector();
+
+    /**
+     * <p>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>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>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>
+     */
+    private Vector catalogs = new Vector();
+
+    /**
+     * <p>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>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>
+     */
+    private Vector localDelegate = new Vector();
+
+    /**
+     * <p>The name of the parser class to load when parsing XML Catalogs.</p>
+     *
+     * <p>If a parser class is provided,
+     * subsequent attempts to parse Catalog files will begin
+     * by attemptiing an XML parse of the catalog file using a parser
+     * of this class.
+     * If the XML parse fails, the "default" text parse will be done
+     * instead.</p>
+     */
+    private String parserClass = null;
+
+    /**
+     * <p>Constructs an empty Catalog.</p>
+     *
+     * <p>The constructor interrogates the relevant system properties
+     * and initializes the catalog data structures.</p>
+     */
+    public Catalog() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+
+	property = System.getProperty("xml.catalog.override");
+
+	if (property != null) {
+	    default_override = (property.equalsIgnoreCase("true")
+				|| property.equalsIgnoreCase("yes")
+				|| property.equalsIgnoreCase("1"));
+	}
+    }
+
+    /**
+     * <p>Sets the parser class, enabling XML Catalog parsing.</p>
+     *
+     * <p>Sets the parser class that will be used for loading XML Catalogs.
+     * If this method is not called, all catalogs will be parsed as
+     * plain text (and assumed to conform to the
+     * <a href="http://www.oasis-open.org/html/a401.htm">OASIS Catalog
+     * format</a>).</p>
+     *
+     * @param parser The name of a class implementing the SAX Parser
+     * interface to be used for subsequent XML Catalog parsing.
+     */
+    public void setParserClass(String parser) {
+	parserClass = parser;
+    }
+
+    /**
+     * <p>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 MalformedURLException, IOException {
+	String PCS = System.getProperty("path.separator");
+	String catalog_files = System.getProperty("xml.catalog.files");
+
+	while (catalog_files != null) {
+	    int pos = catalog_files.indexOf(PCS);
+	    String catfile = null;
+
+	    if (pos > 0) {
+		catfile = catalog_files.substring(0, pos);
+		catalog_files = catalog_files.substring(pos+1);
+	    } else {
+		catfile = catalog_files;
+		catalog_files = null;
+	    }
+
+	    catalogFiles.addElement(catfile);
+	}
+
+	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);
+	}
+    }
+
+    /**
+     * <p>Parse a catalog file, augmenting internal data structures</p>
+     *
+     * @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 MalformedURLException, IOException {
+
+	// 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 files on the catalogFiles vector. This
+	// vector can grow during processing if CATALOG entries are
+	// encountered in the catalog
+	int curCat = 0;
+	while (curCat < catalogFiles.size()) {
+	    String catfile = (String) catalogFiles.elementAt(curCat++);
+
+	    if (catalogEntries.size() == 0 && catalogs.size() == 0) {
+		// We haven't parsed any catalogs yet, let this
+		// catalog be the first...
+		parseCatalogFile(catfile);
+	    } 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
+		while (curCat < catalogFiles.size()) {
+		    catfile = (String) catalogFiles.elementAt(curCat++);
+		    newQueue.addElement(catfile);
+		}
+
+		localCatalogFiles = new Vector();
+		catalogFiles = newQueue;
+		curCat = 0;
+	    }
+
+	    if (!localDelegate.isEmpty()) {
+		Enumeration e = localDelegate.elements();
+		while (e.hasMoreElements()) {
+		    catalogEntries.addElement(e.nextElement());
+		}
+		localDelegate = new Vector();
+	    }
+	}
+
+	// We've parsed them all, reinit the vector...
+	catalogFiles = new Vector();
+    }
+
+    /**
+     * <p>Parse a single catalog file, augmenting internal data structures</p>
+     *
+     * @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.
+     */
+    private synchronized void parseCatalogFile(String fileName)
+	throws MalformedURLException, IOException {
+
+	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
+	    String userdir = fixSlashes(System.getProperty("user.dir"));
+	    catalogCwd = new URL("file:///" + userdir + "/basename");
+	} catch (MalformedURLException e) {
+	    String userdir = fixSlashes(System.getProperty("user.dir"));
+	    debug(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) {
+		debug(1, "Malformed URL on catalog filename", 
+		      fixSlashes(fileName));
+		base = null;
+	    }
+	}
+
+	debug(1, "Loading catalog", fileName);
+	debug(3, "Default BASE", base.toString());
+
+	fileName = base.toString();
+
+	if (parserClass != null) {
+	    try {
+		XMLCatalogReader catfile = new XMLCatalogReader();
+		catfile.setParserClass(parserClass);
+		catfile.parseCatalog(fileName);
+
+		CatalogEntry ce = null;
+		while ((ce = catfile.nextEntry()) != null) {
+		    addEntry(ce);
+		}
+		return;
+	    } catch (SAXException e1) {
+		// not an XML catalog, continue with text parse
+	    } catch (NoXMLParserException e2) {
+		// not an XML catalog, continue with text parse
+	    } catch (NotXMLCatalogException e2) {
+		// not an XML catalog, continue with text parse
+	    } catch (InstantiationException e3) {
+		debug(1, "Cannot instantiate XML Parser class", parserClass);
+	    } catch (IllegalAccessException e4) {
+		debug(1, "Cannot access XML Parser class", parserClass);
+	    } catch (ClassNotFoundException e5) {
+		debug(1, "Cannot load XML Parser class", parserClass);
+	    } catch (UnknownCatalogFormatException e6) {
+		debug(1, "Unrecognized XML Catalog format.");
+		return;
+	    }
+	}
+
+	CatalogReader catfile = new CatalogReader();
+	catfile.parseCatalog(fileName);
+
+	// Process the contents of the catalog file as a whitespace
+	// delimited set of tokens
+	while ((entry = catfile.nextEntry()) != null) {
+	    addEntry(entry);
+	}
+    }
+
+    /**
+     * <p>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.
+     */
+    private void addEntry(CatalogEntry entry) {
+	switch (entry.entryType()) {
+	case CatalogEntry.BASE: {
+	    String value = entry.formalSystemIdentifier();
+	    URL newbase = null;
+
+	    debug(3, "BASE", value);
+
+	    try {
+		value = fixSlashes(value);
+		newbase = new URL(catalogCwd, value);
+	    } catch (MalformedURLException e) {
+		try {
+		    newbase = new URL("file:///" + value);
+		} catch (MalformedURLException e2) {
+		    debug(1, "Malformed URL on base", value);
+		    newbase = null;
+		}
+	    }
+
+	    if (newbase != null) {
+		base = newbase;
+	    }
+
+	    break;
+	}
+
+	case CatalogEntry.CATALOG: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+
+	    debug(3, "CATALOG", fsi);
+
+	    localCatalogFiles.addElement(fsi);
+	    break;
+	}
+
+	case CatalogEntry.DOCUMENT: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "DOCUMENT", fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.OVERRIDE: {
+	    debug(3, "OVERRIDE", entry.yes_or_no());
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.SGMLDECL: {
+	    // meaningless in XML
+	    break;
+	}
+	case CatalogEntry.DELEGATE: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "DELEGATE", entry.partialPublicId(), fsi);
+
+	    addDelegate(entry);
+	    break;
+	}
+	case CatalogEntry.DOCTYPE: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "DOCTYPE", entry.publicId(), fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.DTDDECL: {
+	    // meaningless in XML
+	    break;
+	}
+	case CatalogEntry.ENTITY: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "ENTITY", entry.entityName(), fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.LINKTYPE: {
+	    // meaningless in XML
+	    break;
+	}
+	case CatalogEntry.NOTATION: {
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+	    entry.updateFormalSystemIdentifier(fsi);
+
+	    debug(3, "NOTATION", entry.entityName(), fsi);
+
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.PUBLIC: {
+	    // This entry has to go in the vector because it would
+	    // be relevant in subsequent searches for notations.
+	    String publicid = entry.publicId();
+	    String systemid = makeAbsolute(entry.formalSystemIdentifier());
+
+	    debug(3, "PUBLIC", publicid, systemid);
+
+	    entry.updateFormalSystemIdentifier(systemid);
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	case CatalogEntry.SYSTEM: {
+	    String systemid = entry.systemId();
+	    String fsi = makeAbsolute(entry.formalSystemIdentifier());
+
+	    debug(3, "SYSTEM", systemid, fsi);
+
+	    entry.updateFormalSystemIdentifier(fsi);
+	    catalogEntries.addElement(entry);
+	    break;
+	}
+	}
+    }
+
+    /**
+     * <p>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>
+     *
+     * <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>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 MalformedURLException, 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 = new Catalog();
+		c.setParserClass(parserClass);
+		c.debug = debug;
+
+		c.parseCatalog(catfile);
+		catalogs.setElementAt(c, catPos);
+		c.parseAllCatalogs();
+	    }
+	}
+
+	// Parse all the DELEGATE catalogs
+	Enumeration enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.DELEGATE) {
+		Catalog dcat = new Catalog();
+		dcat.setParserClass(parserClass);
+		dcat.debug = debug;
+		dcat.parseCatalog(e.formalSystemIdentifier());
+	    }
+	}
+    }
+
+
+    /**
+     * <p>Return the applicable DOCTYPE system identifier.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	String resolved = 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(CatalogEntry.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.DOCTYPE
+		&& e.entityName().equals(entityName)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.DOCTYPE,
+					  entityName,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>Return the applicable DOCUMENT entry.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	// If there's a DOCUMENT entry, return it
+	Enumeration enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.DOCUMENT) {
+		return e.formalSystemIdentifier();
+	    }
+	}
+
+	return resolveSubordinateCatalogs(CatalogEntry.DOCUMENT,
+					  null, null, null);
+    }
+
+    /**
+     * <p>Return the applicable ENTITY system identifier.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	String resolved = 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(CatalogEntry.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.ENTITY
+		&& e.entityName().equals(entityName)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.ENTITY,
+					  entityName,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>Return the applicable NOTATION system identifier.</p>
+     *
+     * @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 MalformedURLException, IOException {
+	String resolved = 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(CatalogEntry.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.NOTATION
+		&& e.entityName().equals(notationName)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.NOTATION,
+					  notationName,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>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).
+     *
+     * @throws MalformedURLException The formal system identifier of a
+     * subordinate catalog cannot be turned into a valid URL.
+     * @throws IOException Error reading subordinate catalog file.
+     *
+     * @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.
+     */
+    public String resolvePublic(String publicId, String systemId) 
+	throws MalformedURLException, IOException {
+
+	// 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(CatalogEntry.PUBLIC,
+					     null,
+					     publicId,
+					     systemId);
+	if (resolved != null) {
+	    return resolved;
+	}
+
+	// Otherwise, look in the subordinate catalogs
+	return resolveSubordinateCatalogs(CatalogEntry.PUBLIC,
+					  null,
+					  publicId,
+					  systemId);
+    }
+
+    /**
+     * <p>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>There are four possible cases:</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).
+     *
+     * @throws MalformedURLException The formal system identifier of a
+     * delegated catalog cannot be turned into a valid URL.
+     * @throws IOException Error reading delegated catalog file.
+     *
+     * @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.
+     */
+    private synchronized String resolveLocalPublic(int entityType,
+				      String entityName,
+				      String publicId,
+				      String systemId)
+	throws MalformedURLException, IOException {
+
+	// Always normalize the public identifier before attempting a match
+	publicId = CatalogReader.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 enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.PUBLIC
+		&& e.publicId().equals(publicId)) {
+		if (over || systemId == null) {
+		    return e.formalSystemIdentifier();
+		}
+	    }
+	}
+
+	// If there's a DELEGATE entry in this catalog, use it
+	over = default_override;
+	enum = catalogEntries.elements();
+	Vector delCats = new Vector();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.OVERRIDE) {
+		over = e.yes_or_no().equalsIgnoreCase("YES");
+		continue;
+	    }
+
+	    if (e.entryType() == CatalogEntry.DELEGATE
+		&& (over || systemId == null)) {
+		String p = (String) e.partialPublicId();
+		if (p.length() <= publicId.length()
+		    && p.equals(publicId.substring(0, p.length()))) {
+		    // delegate this match to the other catalog
+
+		    delCats.addElement(e.formalSystemIdentifier());
+		}
+	    }
+	}
+
+	if (delCats.size() > 0) {
+	    Enumeration enumCats = delCats.elements();
+
+	    if (debug > 0) {
+		debug(1, "Switching to delegated catalog(s):");
+		while (enumCats.hasMoreElements()) {
+		    String delegatedCatalog = (String) enumCats.nextElement();
+		    debug(1, "\t" + delegatedCatalog);
+		}
+	    }
+
+	    Catalog dcat = new Catalog();
+	    dcat.setParserClass(parserClass);
+	    dcat.debug = debug;
+
+	    enumCats = delCats.elements();
+	    while (enumCats.hasMoreElements()) {
+		String delegatedCatalog = (String) enumCats.nextElement();
+		dcat.parseCatalog(delegatedCatalog);
+	    }
+
+	    return dcat.resolvePublic(publicId, null);
+	}
+
+	// Nada!
+	return null;
+    }
+
+    /**
+     * <p>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>The caller is responsible for doing any necessary
+     * normalization of the system identifier before calling
+     * this method. For example, a relative system identifier in
+     * a document might be converted to an absolute system identifier
+     * before attempting to resolve it.</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 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 resolveSystem(String systemId)
+	throws MalformedURLException, IOException {
+
+	// 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(CatalogEntry.SYSTEM,
+					  null,
+					  null,
+					  systemId);
+    }
+
+    /**
+     * <p>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
+     */
+    private String resolveLocalSystem(String systemId) {
+	String osname = System.getProperty("os.name");
+	boolean windows = (osname.indexOf("Windows") >= 0);
+	Enumeration enum = catalogEntries.elements();
+	while (enum.hasMoreElements()) {
+	    CatalogEntry e = (CatalogEntry) enum.nextElement();
+	    if (e.entryType() == CatalogEntry.SYSTEM
+		&& (e.systemId().equals(systemId)
+		    || (windows
+			&& e.systemId().equalsIgnoreCase(systemId)))) {
+		return e.formalSystemIdentifier();
+	    }
+	}
+	return null;
+    }
+
+
+    /**
+     * <p>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).
+     *
+     * @throws MalformedURLException The formal system identifier of a
+     * delegated catalog cannot be turned into a valid URL.
+     * @throws IOException Error reading delegated catalog file.
+     *
+     * @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.
+     */
+    private synchronized String resolveSubordinateCatalogs(int entityType,
+					      String entityName,
+					      String publicId,
+					      String systemId)
+	throws MalformedURLException, 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 = new Catalog();
+		c.setParserClass(parserClass);
+		c.debug = debug;
+
+		try {
+		    c.parseCatalog(catfile);
+		} catch (MalformedURLException mue) {
+		    debug(1, "Malformed Catalog URL", catfile);
+		} catch (FileNotFoundException fnfe) {
+		    debug(1, "Failed to load catalog, file not found",
+			  catfile);
+		} catch (IOException ioe) {
+		    debug(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?
+	    switch (entityType) {
+	    case CatalogEntry.DOCTYPE: {
+		resolved = c.resolveDoctype(entityName,
+					    publicId,
+					    systemId);
+		break;
+	    }
+	    case CatalogEntry.DOCUMENT: {
+		resolved = c.resolveDocument();
+		break;
+	    }
+	    case CatalogEntry.ENTITY: {
+		resolved = c.resolveEntity(entityName,
+					   publicId,
+					   systemId);
+		break;
+	    }
+	    case CatalogEntry.NOTATION: {
+		resolved = c.resolveNotation(entityName,
+					     publicId,
+					     systemId);
+		break;
+	    }
+	    case CatalogEntry.PUBLIC: {
+		resolved = c.resolvePublic(publicId, systemId);
+		break;
+	    }
+	    case CatalogEntry.SYSTEM: {
+		resolved = c.resolveSystem(systemId);
+		break;
+	    }
+	    }
+
+	    if (resolved != null) {
+		return resolved;
+	    }
+	}
+
+	return null;
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Replace backslashes with forward slashes. (URLs always use
+     * forward slashes.)</p>
+     *
+     * @param sysid The input system identifier.
+     * @return The same system identifier with backslashes turned into
+     * forward slashes.
+     */
+    private String fixSlashes (String sysid) {
+	return sysid.replace('\\', '/');
+    }
+
+    /**
+     * <p>Construct an absolute URI from a relative one, using the current
+     * base URI.</p>
+     *
+     * @param sysid The (possibly relative) system identifier
+     * @return The system identifier made absolute with respect to the
+     * current {@link #base}.
+     */
+    private String makeAbsolute(String sysid) {
+	URL local = null;
+
+	sysid = fixSlashes(sysid);
+
+	try {
+	    local = new URL(base, sysid);
+	} catch (MalformedURLException e) {
+	    debug(1, "Malformed URL on system identifier", sysid);
+	}
+
+	if (local != null) {
+	    return local.toString();
+	} else {
+	    return sysid;
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     */
+    private void debug(int level, String message) {
+	if (debug >= level) {
+	    System.out.println(message);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec An argument to the message.
+     */
+    private void debug(int level, String message, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param spec1 An argument to the message.
+     * @param spec1 Another argument to the message.
+     */
+    private void debug(int level, String message, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>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
+     */
+    private void addDelegate(CatalogEntry entry) {
+	int pos = 0;
+	String partial = entry.partialPublicId();
+
+	Enumeration local = localDelegate.elements();
+	while (local.hasMoreElements()) {
+	    CatalogEntry dpe = (CatalogEntry) local.nextElement();
+	    String dp = dpe.partialPublicId();
+	    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);
+	}
+    }
+}

Property changes on: com/arbortext/catalog/Catalog.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/NotXMLCatalogException.java
===================================================================
--- com/arbortext/catalog/NotXMLCatalogException.java	(nonexistent)
+++ com/arbortext/catalog/NotXMLCatalogException.java	(revision 71)
@@ -0,0 +1,19 @@
+// NotXMLCatalogException.java - Not an XML catalog
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+package com.arbortext.catalog;
+
+/**
+ * <p>Signal attempt to parse a non-XML Catalog with an XML Parser.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This exception is thrown if an attempt is made to load an XML
+ * Catalog and it appears not to be XML.</p>
+ */
+public class NotXMLCatalogException extends Exception {
+    public NotXMLCatalogException() { super(); }
+}

Property changes on: com/arbortext/catalog/NotXMLCatalogException.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/CatalogEntry.java
===================================================================
--- com/arbortext/catalog/CatalogEntry.java	(nonexistent)
+++ com/arbortext/catalog/CatalogEntry.java	(revision 71)
@@ -0,0 +1,253 @@
+// CatalogEntry.java - Represent OASIS Catalog entries
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import com.arbortext.catalog.InvalidCatalogEntryTypeException;
+import com.arbortext.catalog.InvalidCatalogEntryException;
+
+/**
+ * <p>Represents an OASIS Open Catalog entry.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>Instances of this class represent individual entries
+ * from an <a href="http://www.oasis-open.org/html/a401.htm">OASIS
+ * Open Catalog</a> file.</p>
+ * <p>While this could have been implemented as a base class with a
+ * separate subclass for each type of catalog entry, it didn't seem
+ * to be worth the extra overhead.</p>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class CatalogEntry {
+    // These have one argument in the catalog file
+
+    /** The entry type for a BASE entry */
+    public static final int BASE = 1;       // BASE fsispec
+    /** The entry type for a CATALOG entry */
+    public static final int CATALOG = 2;    // CATALOG fsispec
+    /** The entry type for a DOCUMENT entry */
+    public static final int DOCUMENT = 3;   // DOCUMENT fsispec
+    /** The entry type for a OVERRIDE entry */
+    public static final int OVERRIDE = 4;   // OVERRIDE (yes|no)
+    /** The entry type for a SGMLDECL entry */
+    public static final int SGMLDECL = 5;   // SGMLDECL fsispec
+
+    // These have two arguments in the catalog file
+    /** The entry type for a DELEGATE entry */
+    public static final int DELEGATE = 6;   // DELEGATE partialpublic fsispec
+    /** The entry type for a DOCTYPE entry */
+    public static final int DOCTYPE = 7;    // DOCTYPE entityname fsispec
+    /** The entry type for a DTDDECL entry */
+    public static final int DTDDECL = 8;    // DTDDECL publicid fsispec
+    /** The entry type for a ENTITY entry */
+    public static final int ENTITY = 9;     // ENTITY entityname fsispec
+    /** The entry type for a LINKTYPE entry */
+    public static final int LINKTYPE = 10;  // LINKTYPE entityname fsispec
+    /** The entry type for a NOTATION entry */
+    public static final int NOTATION = 11;  // NOTATION entityname fsispec
+    /** The entry type for a PUBLIC entry */
+    public static final int PUBLIC = 12;    // PUBLIC publicid fsispec
+    /** The entry type for a SYSTEM entry */
+    public static final int SYSTEM = 13;    // SYSTEM systemid fsispec
+
+    /** The entry type (one of BASE..SYSTEM) */
+    private int entryType = 0;
+
+    /** The first argument in a catalog entry */
+    private String spec1 = "";
+
+    /** The second argument in a catalog entry (usually an fsispec) */
+    private String spec2 = "";
+
+    /**
+     * <p>Construct a catalog entry of the specified type. The two-argument
+     * form of the constructor can be used for BASE, CATALOG, DOCUMENT,
+     * OVERRIDE, and SGMLDECL entries.</p>
+     *
+     * @param type The entry type.
+     * @param spec The argument to the entry, a formal system
+     * identifier in all cases except OVERRIDE when it must be either
+     * "yes" or "no".
+     */
+    public CatalogEntry(int type, String spec)
+	throws InvalidCatalogEntryTypeException,
+	       InvalidCatalogEntryException {
+
+	if (type < BASE || type > SYSTEM) {
+	    throw new InvalidCatalogEntryTypeException();
+	}
+
+	if (type > SGMLDECL) {
+	    throw new InvalidCatalogEntryException();
+	}
+
+	if (type == OVERRIDE
+	    && !(spec.equalsIgnoreCase("YES")
+		 || spec.equalsIgnoreCase("NO"))) {
+	    throw new InvalidCatalogEntryException();
+	}
+
+	entryType = type;
+	spec1 = spec;
+    }
+
+    /**
+     * <p>Construct a catalog entry of the specified type. The three-argument
+     * form of the constructor can be used for DELEGATE, DOCTYPE, DTDDECL,
+     * ENTITY, LINKTYPE, NOTATION, PUBLIC, and SYSTEM entries.</p>
+     *
+     * @param type The entry type.
+     * @param spec1 The first argument to the entry, usually an
+     * entity name or (partial) public identifier.
+     * @param spec2 The second argument to the entry, often a
+     * formal system identifier.
+     */
+    public CatalogEntry(int type, String spec, String fsispec)
+	throws InvalidCatalogEntryTypeException,
+	       InvalidCatalogEntryException {
+
+	if (type < BASE || type > SYSTEM) {
+	    throw new InvalidCatalogEntryTypeException();
+	}
+
+	if (type < DELEGATE) {
+	    throw new InvalidCatalogEntryException();
+	}
+
+	entryType = type;
+	spec1 = spec;
+	spec2 = fsispec;
+    }
+
+    /**
+     * <p>The entry type</p>
+     *
+     * @return The entry type
+     */
+    public int entryType() {
+	return entryType;
+    }
+
+    /**
+     * <p>The formal system identifier of the entry, if appropriate</p>
+     *
+     * @return The FSI for the entry, or null if it has no FSI.
+     */
+    public String formalSystemIdentifier() {
+	if (entryType > SGMLDECL) {
+	    return spec2;
+	} else {
+	    if (entryType != OVERRIDE) {
+		return spec1;
+	    } else {
+		return null;
+	    }
+	}
+    }
+
+    /**
+     * <p>The argument, YES or NO, of an OVERRIDE entry.</p>
+     *
+     * @return The YES or NO setting of an OVERRIDE entry,
+     * null otherwise.
+     */
+    public String yes_or_no() {
+	if (entryType != OVERRIDE) {
+	    return null;
+	} else {
+	    return spec1;
+	}
+    }
+
+    /**
+     * <p>The partial public identifier of a DELEGATE entry.</p>
+     *
+     * @return The partial public identifier of a DELEGATE entry,
+     * null otherwise.
+     */
+    public String partialPublicId() {
+	if (entryType != DELEGATE) {
+	    return null;
+	} else {
+	    return spec1;
+	}
+    }
+
+    /**
+     * <p>The entity name</p>
+     *
+     * @return The entity name of a DOCTYPE, ENTITY, LINKTYPE, or
+     * NOTATION entry,
+     * null otherwise.
+     */
+    public String entityName() {
+	if (entryType == DOCTYPE
+	    || entryType == ENTITY
+	    || entryType == LINKTYPE
+	    || entryType == NOTATION) {
+	    return spec1;
+	} else {
+	    return null;
+	}
+    }
+
+    /**
+     * <p>The public identifier</p>
+     *
+     * @return The public identifier of a DTDDECL or PUBLIC entry,
+     * null otherwise.
+     */
+    public String publicId() {
+	if (entryType == DTDDECL
+	    || entryType == PUBLIC) {
+	    return spec1;
+	} else {
+	    return null;
+	}
+    }
+
+    /**
+     * <p>The system identifier</p>
+     *
+     * @return The system identifier of a SYSTEM entry,
+     * null otherwise.
+     */
+    public String systemId() {
+	if (entryType != SYSTEM) {
+	    return null;
+	} else {
+	    return spec1;
+	}
+    }
+
+    /**
+     * <p>Update the formal system identifier</p>
+     *
+     * <p>The FSI initial specified in an entry may be relative (to
+     * the location of the catalog file or as modified by a BASE entry).
+     * A system processing catalog files
+     * (e.g. {@link com.arbortext.catalog.Catalog}),
+     * must be able to update the FSI in order to change it from a relative
+     * location to an absolute one.</p>
+     *
+     * @param newspec The new FSI
+     */
+    public void updateFormalSystemIdentifier(String newspec) {
+	if (entryType > SGMLDECL) {
+	    spec2 = newspec;
+	} else {
+	    spec1 = newspec;
+	}
+    }
+}

Property changes on: com/arbortext/catalog/CatalogEntry.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/CatalogReader.java
===================================================================
--- com/arbortext/catalog/CatalogReader.java	(nonexistent)
+++ com/arbortext/catalog/CatalogReader.java	(revision 71)
@@ -0,0 +1,432 @@
+// CatalogReader.java - Read OASIS Catalog files
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.lang.Integer;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.net.URL;
+import java.net.MalformedURLException;
+import com.arbortext.catalog.CatalogEntry;
+import com.arbortext.catalog.InvalidCatalogEntryTypeException;
+import com.arbortext.catalog.InvalidCatalogEntryException;
+
+/**
+ * <p>Parses OASIS Open Catalog files.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class reads OASIS Open Catalog files, returning a stream
+ * of tokens.</p>
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ * </dl>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class CatalogReader {
+    // These are class variables so that several methods can access them
+    /** The filename (URL) of the catalog being read */
+    private String catfilename = null;
+
+    /** The input stream used to read the catalog */
+    private DataInputStream catfile = null;
+
+    /**
+     * Lookahead stack. Reading a catalog sometimes requires up to
+     * two characters of lookahead.
+     */
+    private int[] stack = new int[3];
+
+    /** The current position on the lookahead stack */
+    private int top = -1;
+
+    /**
+     * <p>The debug level</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * <p>Construct a CatalogReader object.</p>
+     */
+    public CatalogReader() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+    }
+
+    /**
+     * <p>Start parsing an OASIS Open Catalog file. The file is
+     * actually read and parsed
+     * as needed by <code>nextEntry</code>.</p>
+     *
+     * @param fileUrl  The URL or filename of the catalog file to process
+     *
+     * @throws MalformedURLException Improper fileUrl
+     * @throws IOException Error reading catalog file
+     */
+    public void parseCatalog(String fileUrl)
+	throws MalformedURLException, IOException {
+	catfilename = fileUrl;
+	URL catalog;
+
+	try {
+	    catalog = new URL(fileUrl);
+	} catch (MalformedURLException e) {
+	    catalog = new URL("file:///" + fileUrl);
+	}
+
+	try {
+	    catfile = new DataInputStream(catalog.openStream());
+	} catch (FileNotFoundException e) {
+	    debug(1, "Failed to load catalog, file not found", catalog.toString());
+	}
+    }
+
+    /**
+     * <p>The destructor.</p>
+     *
+     * <p>Makes sure the catalog file is closed.</p>
+     */
+    protected void finalize() throws IOException {
+	if (catfile != null) {
+	    catfile.close();
+	}
+	catfile = null;
+    }
+
+    /**
+     * <p>Get the next entry from the file</p>
+     *
+     * @throws IOException Error reading catalog file
+     * @return A CatalogEntry object for the next entry in the catalog
+     */
+    public CatalogEntry nextEntry() throws IOException {
+	if (catfile == null) {
+	    return null;
+	}
+
+	boolean confused = false;
+
+	while (true) {
+	    String token = nextToken();
+
+	    if (token == null) {
+		catfile.close();
+		catfile = null;
+		return null;
+	    }
+
+	    if (token.equalsIgnoreCase("BASE")
+		|| token.equalsIgnoreCase("CATALOG")
+		|| token.equalsIgnoreCase("DOCUMENT")
+		|| token.equalsIgnoreCase("OVERRIDE")
+		|| token.equalsIgnoreCase("SGMLDECL")) {
+		String spec = nextToken();
+		confused = false;
+
+		try {
+		    if (token.equalsIgnoreCase("BASE")) {
+			return new CatalogEntry(CatalogEntry.BASE, spec);
+		    }
+		    if (token.equalsIgnoreCase("CATALOG")) {
+			return new CatalogEntry(CatalogEntry.CATALOG, spec);
+		    }
+		    if (token.equalsIgnoreCase("DOCUMENT")) {
+			return new CatalogEntry(CatalogEntry.DOCUMENT, spec);
+		    }
+		    if (token.equalsIgnoreCase("OVERRIDE")) {
+			return new CatalogEntry(CatalogEntry.OVERRIDE, spec);
+		    }
+		    if (token.equalsIgnoreCase("SGMLDECL")) {
+			return new CatalogEntry(CatalogEntry.SGMLDECL, spec);
+		    }
+		} catch (InvalidCatalogEntryTypeException icete) {
+		    debug(1, "Invalid catalog entry type", token);
+		    confused = true;
+		} catch (InvalidCatalogEntryException icete) {
+		    debug(1, "Invalid catalog entry", token, spec);
+		    confused = true;
+		}
+	    }
+
+	    if (token.equalsIgnoreCase("DELEGATE")
+		|| token.equalsIgnoreCase("DOCTYPE")
+		|| token.equalsIgnoreCase("DTDDECL")
+		|| token.equalsIgnoreCase("ENTITY")
+		|| token.equalsIgnoreCase("LINKTYPE")
+		|| token.equalsIgnoreCase("NOTATION")
+		|| token.equalsIgnoreCase("PUBLIC")
+		|| token.equalsIgnoreCase("SYSTEM")) {
+		String spec1 = nextToken();
+		String spec2 = nextToken();
+		confused = false;
+		try {
+		    if (token.equalsIgnoreCase("DELEGATE")) {
+			return new CatalogEntry(CatalogEntry.DELEGATE,
+						normalize(spec1), spec2);
+		    }
+		    if (token.equalsIgnoreCase("DOCTYPE")) {
+			return new CatalogEntry(CatalogEntry.DOCTYPE,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("DTDDECL")) {
+			return new CatalogEntry(CatalogEntry.DTDDECL,
+						normalize(spec1), spec2);
+		    }
+		    if (token.equalsIgnoreCase("ENTITY")) {
+			return new CatalogEntry(CatalogEntry.ENTITY,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("LINKTYPE")) {
+			return new CatalogEntry(CatalogEntry.LINKTYPE,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("NOTATION")) {
+			return new CatalogEntry(CatalogEntry.NOTATION,
+						spec1, spec2);
+		    }
+		    if (token.equalsIgnoreCase("PUBLIC")) {
+			return new CatalogEntry(CatalogEntry.PUBLIC,
+						normalize(spec1), spec2);
+		    }
+		    if (token.equalsIgnoreCase("SYSTEM")) {
+			return new CatalogEntry(CatalogEntry.SYSTEM,
+						spec1, spec2);
+		    }
+		} catch (InvalidCatalogEntryTypeException icete) {
+		    debug(1, "Invalid catalog entry type", token);
+		    confused = true;
+		} catch (InvalidCatalogEntryException icete) {
+		    debug(1, "Invalid catalog entry", token, spec1, spec2);
+		    confused = true;
+		}
+	    }
+
+	    if (!confused) {
+		if (debug > 1) {
+		    System.out.println("Unrecognized token parsing catalog: '"
+				       + catfilename
+				       + "': "
+				       + token);
+		    System.out.println("\tSkipping to next recognized token.");
+		}
+		confused = true;
+	    }
+	}
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Normalize a public identifier.</p>
+     *
+     * <p>Public identifiers must be normalized according to the following
+     * rules before comparisons between them can be made:</p>
+     *
+     * <ul>
+     * <li>Whitespace characters are normalized to spaces (e.g., line feeds,
+     * tabs, etc. become spaces).</li>
+     * <li>Leading and trailing whitespace is removed.</li>
+     * <li>Multiple internal whitespaces are normalized to a single
+     * space.</li>
+     * </ul>
+     *
+     * <p>This method is declared static so that other classes
+     * can use it directly.</p>
+     *
+     * @param publicId The unnormalized public identifier.
+     *
+     * @return The normalized identifier.
+     */
+    public static String normalize(String publicId) {
+	String normal = publicId.replace('\t', ' ');
+	normal = normal.replace('\r', ' ');
+	normal = normal.replace('\n', ' ');
+	normal = normal.trim();
+
+	int pos;
+
+	while ((pos = normal.indexOf("  ")) >= 0) {
+	    normal = normal.substring(0, pos) + normal.substring(pos+1);
+	}
+
+	return normal;
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Return the next token in the catalog file.</p>
+     *
+     * @return The Catalog file token from the input stream.
+     * @throws IOException If an error occurs reading from the stream.
+     */
+    private String nextToken() throws IOException {
+	String token = "";
+	int ch, nextch;
+
+	// Skip over leading whitespace and comments
+	while (true) {
+	    // skip leading whitespace
+	    ch = catfile.read();
+	    while (ch <= ' ') {      // all ctrls are whitespace
+		ch = catfile.read();
+		if (ch < 0) {
+		    return null;
+		}
+	    }
+
+	    // now 'ch' is the current char from the file
+	    nextch = catfile.read();
+	    if (nextch < 0) {
+		return null;
+	    }
+
+	    if (ch == '-' && nextch == '-') {
+		// we've found a comment, skip it...
+		ch = ' ';
+		nextch = nextChar();
+		while (ch != '-' || nextch != '-') {
+		    ch = nextch;
+		    nextch = nextChar();
+		}
+
+		// Ok, we've found the end of the comment,
+		// loop back to the top and start again...
+	    } else {
+		stack[++top] = nextch;
+		stack[++top] = ch;
+		break;
+	    }
+	}
+
+	ch = nextChar();
+	if (ch == '"' || ch == '\'') {
+	    int quote = ch;
+	    while ((ch = nextChar()) != quote) {
+		char[] chararr = new char[1];
+		chararr[0] = (char) ch;
+		String s = new String(chararr);
+		token = token.concat(s);
+	    }
+	    return token;
+	} else {
+	    // return the next whitespace or comment delimited
+	    // string
+	    while (ch > ' ') {
+		nextch = nextChar();
+		if (ch == '-' && nextch == '-') {
+		    stack[++top] = ch;
+		    stack[++top] = nextch;
+		    return token;
+		} else {
+		    char[] chararr = new char[1];
+		    chararr[0] = (char) ch;
+		    String s = new String(chararr);
+		    token = token.concat(s);
+		    ch = nextch;
+		}
+	    }
+	    return token;
+	}
+    }
+
+    /**
+     * <p>Return the next logical character from the input stream.</p>
+     *
+     * @return The next (logical) character from the input stream. The
+     * character may be buffered from a previous lookahead.
+     *
+     * @throws IOException If an error occurs reading from the stream.
+     */
+    private int nextChar() throws IOException {
+	if (top < 0) {
+	    return catfile.read();
+	} else {
+	    return stack[top--];
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     */
+    private void debug(int level, String message, String token) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec The argument to the token.
+     */
+    private void debug(int level, String message, String token, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec1 The first argument to the token.
+     * @param spec2 The second argument to the token.
+     */
+    private void debug(int level, String message,
+		       String token, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+}

Property changes on: com/arbortext/catalog/CatalogReader.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/XMLCatalogReader.java
===================================================================
--- com/arbortext/catalog/XMLCatalogReader.java	(nonexistent)
+++ com/arbortext/catalog/XMLCatalogReader.java	(revision 71)
@@ -0,0 +1,415 @@
+// XMLCatalogReader.java - Read XML Catalog files
+// Written by Norman Walsh, nwalsh@arbortext.com
+// NO WARRANTY! This class is in the public domain.
+
+package com.arbortext.catalog;
+
+import java.lang.Integer;
+import java.util.Vector;
+import java.util.Enumeration;
+import java.io.IOException;
+import java.net.URL;
+import java.net.MalformedURLException;
+import com.arbortext.catalog.CatalogEntry;
+import com.arbortext.catalog.CatalogReader;
+import com.arbortext.catalog.InvalidCatalogEntryTypeException;
+import com.arbortext.catalog.InvalidCatalogEntryException;
+import com.arbortext.catalog.UnknownCatalogFormatException;
+import com.arbortext.catalog.NotXMLCatalogException;
+import com.arbortext.catalog.NoXMLParserException;
+
+import org.xml.sax.*;
+
+/**
+ * <p>Parses XML Catalog files.</p>
+ *
+ * <blockquote>
+ * <em>This module, both source code and documentation, is in the
+ * Public Domain, and comes with <strong>NO WARRANTY</strong>.</em>
+ * </blockquote>
+ *
+ * <p>This class reads XML Catalog files, returning a stream
+ * of tokens. At present, it recognizes John Cowan's
+ * <a href="http://www.ccil.org/~cowan/XML/XCatalog.html">XML Catalogs</a>
+ * (formerly XCatalogs). In the future, additional XML Catalog formats
+ * may be supported.</p>
+ *
+ * <p>This code interrogates the following non-standard system properties:</p>
+ *
+ * <dl>
+ * <dt><b>xml.catalog.debug</b></dt>
+ * <dd><p>Sets the debug level. A value of 0 is assumed if the
+ * property is not set or is not a number.</p></dd>
+ * </dl>
+ *
+ * @see Catalog
+ *
+ * @author Arbortext, Inc.
+ * @author Norman Walsh
+ *         <a href="mailto:nwalsh@arbortext.com">nwalsh@arbortext.com</a>
+ * @version 1.0
+ */
+public class XMLCatalogReader implements DocumentHandler {
+    // These are class variables so that several methods can access them
+    /** The filename (URL) of the catalog being read */
+    private String catfilename = null;
+
+    /**
+     * <p>The debug level</p>
+     *
+     * <p>In general, higher numbers produce more information:</p>
+     * <ul>
+     * <li>0, no messages
+     * <li>1, minimal messages (high-level status)
+     * <li>2, detailed messages
+     * </ul>
+     */
+    public int debug = 0;
+
+    /**
+     * Indicates that the catalog type is not XML
+     */
+    private static final int NOTXMLCATALOG = -1;
+
+    /**
+     * Indicates that the catalog type is unknown.
+     */
+    private static final int UNKNOWNCATALOG = 0;
+
+    /**
+     * Indicates that the catalog type is an XML Catalog (John Cowan's
+     * XCatalog)
+     */
+    private static final int XCATALOG = 1;
+
+    /**
+     * Indicates the catalog type.
+     */
+    private int catalogType = NOTXMLCATALOG;
+
+    /**
+     * <p>The list of entries scanned from the catalog.</p>
+     *
+     * <p>The SAX Parser is event-driven, but the Catalog class expects
+     * to iterate through the entries with
+     * <a href="#nextToken()">nextToken()</a>. So this class builds a
+     * vector of entries during the parse and returns them sequentially
+     * when <a href="#nextToken()">nextToken()</a> is called.</p>
+     *
+     * @see Catalog
+     */
+    private Vector catalogEntries = new Vector();
+
+    /**
+     * An enumerator for walking through the list of catalogEntries.
+     */
+    private Enumeration catalogEnum = null;
+
+    /**
+     * <p>The name of the parser class to load when parsing XML Catalogs.</p>
+     *
+     * <p>If a parser class is provided,
+     * subsequent attempts to parse Catalog files will begin
+     * by attemptiing an XML parse of the catalog file using a parser
+     * of this class.
+     * If the XML parse fails, the "default" text parse will be done
+     * instead.</p>
+     */
+    private String parserClass = null;
+
+    /**
+     * <p>Construct an XMLCatalogReader object.</p>
+     */
+    public XMLCatalogReader() {
+	String property = System.getProperty("xml.catalog.debug");
+
+	if (property != null) {
+	    try {
+		debug = Integer.parseInt(property);
+	    } catch (NumberFormatException e) {
+		debug = 0;
+	    }
+	}
+    }
+
+    /**
+     * <p>Sets the parser class, enabling XML Catalog parsing.</p>
+     *
+     * <p>Sets the parser class that will be used for loading XML Catalogs.
+     * If this method is not called, all attempts to use the
+     * <code>XMLCatalogParser</code> will fail, throwing a
+     * <code>NoXMLParserException</code>.</p>
+     *
+     * @param parser The name of a class implementing the SAX Parser
+     * interface to be used for subsequent XML Catalog parsing.
+     *
+     * @see com.arbortext.catalog.NoXMLParserException
+     */
+    public void setParserClass(String parser) {
+	parserClass = parser;
+    }
+
+    /**
+     * <p>Attempt to parse an XML Catalog file.</p>
+     *
+     * @param fileUrl   The URL or filename of the catalog file to process
+     * @param catParser A SAX-compliant parser to use for reading the files
+     *
+     * @throws SAXException Error parsing catalog file.
+     * @throws IOException Error reading catalog file.
+     * @throws NoXMLParserException No Parser class provided.
+     * @throws NotXMLCatalogException The Catalog appears not to be XML.
+     * @throws UnknownCatalogFormatException Unexpected XML catalog type.
+     * @throws ClassNotFoundException Parser class can't be found.
+     * @throws InstantiationException Parser class can't be instantiated.
+     * @throws IllegalAccessException Error instantiating parser class.
+     * @throws ClassCastException Parser class isn't a SAX Parser.
+     */
+    public void parseCatalog(String fileUrl)
+	throws SAXException, IOException,
+	       NotXMLCatalogException, NoXMLParserException,
+	       UnknownCatalogFormatException, ClassNotFoundException,
+	       InstantiationException, IllegalAccessException,
+	       ClassCastException {
+	// Create an instance of the parser
+	if (parserClass == null) {
+	    throw new NoXMLParserException();
+	}
+
+	Parser parser = (Parser) Class.forName(parserClass).newInstance();
+
+	catfilename = fileUrl;
+	parser.setDocumentHandler(this);
+	parser.parse(fileUrl);
+
+	if (catalogType == NOTXMLCATALOG) {
+	    // Why doesn't the attempt to parse this file throw a
+	    // SAX Exception???
+	    throw new NotXMLCatalogException();
+	}
+
+	if (catalogType == UNKNOWNCATALOG) {
+	    throw new UnknownCatalogFormatException();
+	}
+    }
+
+    /**
+     * <p>Get the next entry from the file</p>
+     *
+     * @throws IOException Error reading catalog file
+     * @return A CatalogEntry object for the next entry in the catalog
+     */
+    public CatalogEntry nextEntry() throws IOException {
+	if (catalogEnum == null) {
+	    catalogEnum = catalogEntries.elements();
+	}
+
+	if (catalogEnum.hasMoreElements()) {
+	    return (CatalogEntry) catalogEnum.nextElement();
+	} else {
+	    return null;
+	}
+    }
+
+    // ----------------------------------------------------------------------
+
+    /*
+     * <p>Parse elements from John Cowan's XML Catalog doctype.</p>
+     *
+     * <p>Each recognized element is turned into an appropriate
+     * CatalogEntry and put onto the entries vector for later
+     * retrieval.</p>
+     *
+     * @param name The name of the element.
+     * @param atts The list of attributes on the element.
+     *
+     * @see CatalogEntry
+     */
+    private void xCatalogEntry (String name, AttributeList atts) {
+	CatalogEntry ce = null;
+
+	try {
+	    if (name.equals("Base")) {
+		ce = new CatalogEntry(CatalogEntry.BASE,
+				      atts.getValue("HRef"));
+		debug(3, "Base", atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Delegate")) {
+		ce = new CatalogEntry(CatalogEntry.DELEGATE,
+				      CatalogReader.normalize(atts.getValue("PublicId")),
+				      atts.getValue("HRef"));
+		debug(3, "Delegate",
+		      CatalogReader.normalize(atts.getValue("PublicId")),
+		      atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Extend")) {
+		ce = new CatalogEntry(CatalogEntry.CATALOG,
+				      atts.getValue("HRef"));
+		debug(3, "Extend", atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Map")) {
+		ce = new CatalogEntry(CatalogEntry.PUBLIC,
+				      CatalogReader.normalize(atts.getValue("PublicId")),
+				      atts.getValue("HRef"));
+		debug(3, "Map",
+		      CatalogReader.normalize(atts.getValue("PublicId")),
+		      atts.getValue("HRef"));
+	    }
+
+	    if (name.equals("Remap")) {
+		ce = new CatalogEntry(CatalogEntry.SYSTEM,
+				      atts.getValue("SystemId"),
+				      atts.getValue("HRef"));
+		debug(3, "Remap",
+		      CatalogReader.normalize(atts.getValue("SystemId")),
+		      atts.getValue("HRef"));
+	    }
+
+	    if (ce == null) {
+		// This is equivalent to an invalid catalog entry type
+		debug(1, "Invalid catalog entry type", name);
+	    }
+	} catch (InvalidCatalogEntryTypeException icete) {
+	    debug(1, "Invalid catalog entry type", name);
+	} catch (InvalidCatalogEntryException icete) {
+	    debug(1, "Invalid catalog entry", name);
+	}
+
+	if (ce != null) {
+	    catalogEntries.addElement(ce);
+	}
+    }
+
+    // ----------------------------------------------------------------------
+    // Implement the SAX DocumentHandler interface
+
+    /** <p>The SAX <code>setDocumentLocator</code> method. Does nothing.</p> */
+    public void setDocumentLocator (Locator locator) {
+	return;
+    }
+
+    /** <p>The SAX <code>startDocument</code> method. Does nothing.</p> */
+    public void startDocument ()
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>endDocument</code> method. Does nothing.</p> */
+    public void endDocument ()
+	throws SAXException {
+	return;
+    }
+
+    /**
+     * <p>The SAX <code>startElement</code> method.</p>
+     *
+     * <p>This element attempts to identify the type of catalog
+     * by looking at the name of the first element encountered.
+     * If it recognizes the element, it sets the <code>catalogType</code>
+     * appropriately.</p>
+     *
+     * <p>After the catalog type has been identified, the appropriate
+     * entry parser is called for each subsequent element in the
+     * catalog.</p>
+     *
+     * @param name The name of the element.
+     * @param atts The list of attributes on the element.
+     *
+     */
+    public void startElement (String name, AttributeList atts)
+	throws SAXException {
+
+	if (catalogType == UNKNOWNCATALOG || catalogType == NOTXMLCATALOG) {
+	    if (name.equals("XMLCatalog")) {
+		catalogType = XCATALOG;
+		return;
+	    }
+	}
+
+	if (catalogType == XCATALOG) {
+	    xCatalogEntry(name, atts);
+	}
+    }
+
+    /** <p>The SAX <code>endElement</code> method. Does nothing.</p> */
+    public void endElement (String name)
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>characters</code> method. Does nothing.</p> */
+    public void characters (char ch[], int start, int length)
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>ignorableWhitespace</code> method. Does nothing.</p> */
+    public void ignorableWhitespace (char ch[], int start, int length)
+	throws SAXException {
+	return;
+    }
+
+    /** <p>The SAX <code>processingInstruction</code> method. Does nothing.</p> */
+    public void processingInstruction (String target, String data)
+	throws SAXException {
+	return;
+    }
+
+    // -----------------------------------------------------------------
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     */
+    private void debug(int level, String message, String token) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec The argument to the token.
+     */
+    private void debug(int level, String message, String token, String spec) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec);
+	}
+    }
+
+    /**
+     * <p>Print debug message (if the debug level is high enough).</p>
+     *
+     * @param level The debug level of this message. This message
+     * will only be
+     * displayed if the current debug level is at least equal to this
+     * value.
+     * @param message The text of the message.
+     * @param token The catalog file token being processed.
+     * @param spec1 The first argument to the token.
+     * @param spec2 The second argument to the token.
+     */
+    private void debug(int level, String message,
+		       String token, String spec1, String spec2) {
+	if (debug >= level) {
+	    System.out.println(message + ": " + token + " " + spec1);
+	    System.out.println("\t" + spec2);
+	}
+    }
+}

Property changes on: com/arbortext/catalog/XMLCatalogReader.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
Index: com/arbortext/catalog/package.html
===================================================================
--- com/arbortext/catalog/package.html	(nonexistent)
+++ com/arbortext/catalog/package.html	(revision 71)
@@ -0,0 +1,18 @@
+<html>
+<head>
+<title>Arbortext Catalog package</title>
+</head>
+<body>
+<p>Arbortext Catalog package</p>
+
+<p>This package implements reads and implements the semantics of
+OASIS Catalog files as 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>It also reads John Cowan's
+<a href="http://www.ccil.org/~cowan/XML/XCatalog.html">XML Catalogs</a>
+which are an XML representation of OASIS Catalogs.</p>
+
+</body>
+</html>

Property changes on: com/arbortext/catalog/package.html
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
