Source for gnu.xml.util.XCat

   1: /* XCat.java -- 
   2:    Copyright (C) 2001 Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package gnu.xml.util;
  40: 
  41: import gnu.java.lang.CPStringBuilder;
  42: 
  43: import java.io.ByteArrayOutputStream;
  44: import java.io.IOException;
  45: import java.net.URL;
  46: import java.util.Enumeration;
  47: import java.util.Hashtable;
  48: import java.util.StringTokenizer;
  49: import java.util.Stack;
  50: import java.util.Vector;
  51: 
  52: import org.xml.sax.Attributes;
  53: import org.xml.sax.ErrorHandler;
  54: import org.xml.sax.InputSource;
  55: import org.xml.sax.Locator;
  56: import org.xml.sax.SAXException;
  57: import org.xml.sax.SAXNotRecognizedException;
  58: import org.xml.sax.SAXParseException;
  59: import org.xml.sax.XMLReader;
  60: 
  61: import org.xml.sax.ext.DefaultHandler2;
  62: import org.xml.sax.ext.EntityResolver2;
  63: 
  64: import org.xml.sax.helpers.XMLReaderFactory;
  65: 
  66: /**
  67:  * Packages <a href=
  68:     "http://www.oasis-open.org/committees/entity/spec-2001-08-06.html"
  69:     >OASIS XML Catalogs</a>,
  70:  * primarily for entity resolution by parsers.
  71:  * That specification defines an XML syntax for mappings between
  72:  * identifiers declared in DTDs (particularly PUBLIC identifiers) and
  73:  * locations.  SAX has always supported such mappings, but conventions for
  74:  * an XML file syntax to maintain them have previously been lacking.
  75:  *
  76:  * <p> This has three main operational modes.  The primary intended mode is
  77:  * to create a resolver, then preloading it with one or more site-standard
  78:  * catalogs before using it with one or more SAX parsers: <pre>
  79:  *    XCat    catalog = new XCat ();
  80:  *    catalog.setErrorHandler (diagnosticErrorHandler);
  81:  *    catalog.loadCatalog ("file:/local/catalogs/catalog.cat");
  82:  *    catalog.loadCatalog ("http://shared/catalog.cat");
  83:  *    ...
  84:  *    catalog.disableLoading ();
  85:  *    parser1.setEntityResolver (catalog);
  86:  *    parser2.setEntityResolver (catalog);
  87:  *    ...</pre>
  88:  *
  89:  * <p>A second mode is to arrange that your application uses instances of
  90:  * this class as its entity resolver, and automatically loads catalogs
  91:  * referenced by <em>&lt;?oasis-xml-catalog...?&gt;</em> processing
  92:  * instructions found before the DTD in documents it parses.
  93:  * It would then discard the resolver after each parse.
  94:  *
  95:  * <p> A third mode applies catalogs in contexts other than entity
  96:  * resolution for parsers.
  97:  * The {@link #resolveURI resolveURI()} method supports resolving URIs
  98:  * stored in XML application data, rather than inside DTDs.
  99:  * Catalogs would be loaded as shown above, and the catalog could
 100:  * be used concurrently for parser entity resolution and for
 101:  * application URI resolution.
 102:  * </p>
 103:  *
 104:  * <center><hr width='70%'></center>
 105:  *
 106:  * <p>Errors in catalogs implicitly loaded (during resolution) are ignored
 107:  * beyond being reported through any <em>ErrorHandler</em> assigned using
 108:  * {@link #setErrorHandler setErrorHandler()}.  SAX exceptions
 109:  * thrown from such a handler won't abort resolution, although throwing a
 110:  * <em>RuntimeException</em> or <em>Error</em> will normally abort both
 111:  * resolution and parsing.  Useful diagnostic information is available to
 112:  * any <em>ErrorHandler</em> used to report problems, or from any exception
 113:  * thrown from an explicit {@link #loadCatalog loadCatalog()} invocation.
 114:  * Applications can use that information as troubleshooting aids.
 115:  *
 116:  * <p>While this class requires <em>SAX2 Extensions 1.1</em> classes in
 117:  * its class path, basic functionality does not require using a SAX2
 118:  * parser that supports the extended entity resolution functionality.
 119:  * See the original SAX1
 120:  * {@link #resolveEntity(java.lang.String,java.lang.String) resolveEntity()}
 121:  * method for a list of restrictions which apply when it is used with
 122:  * older SAX parsers.
 123:  *
 124:  * @see EntityResolver2
 125:  *
 126:  * @author David Brownell
 127:  */
 128: public class XCat implements EntityResolver2
 129: {
 130:     private Catalog        catalogs [];
 131:     private boolean        usingPublic = true;
 132:     private boolean        loadingPermitted = true;
 133:     private boolean        unified = true;
 134:     private String        parserClass;
 135:     private ErrorHandler    errorHandler;
 136: 
 137:     // private EntityResolver    next;    // chain to next if we fail...
 138: 
 139:     //
 140:     // NOTE:  This is a straightforward implementation, and if
 141:     // there are lots of "nextCatalog" or "delegate*" entries
 142:     // in use, two tweaks would be worth considering:
 143:     //
 144:     //    - Centralize some sort of cache (key by URI) for individual
 145:     //      resolvers.  That'd avoid multiple copies of a given catalog.
 146:     //
 147:     //    - Have resolution track what catalogs (+modes) have been
 148:     //      searched.  This would support loop detection.
 149:     //
 150: 
 151: 
 152:     /**
 153:      * Initializes without preloading a catalog.
 154:      * This API is convenient when you may want to arrange that catalogs
 155:      * are automatically loaded when explicitly referenced in documents,
 156:      * using the <em>oasis-xml-catalog</em> processing instruction.
 157:      * In such cases you won't usually be able to preload catalogs.
 158:      */
 159:     public XCat () { }
 160: 
 161:     /**
 162:      * Initializes, and preloads a catalog using the default SAX parser.
 163:      * This API is convenient when you operate with one or more standard
 164:      * catalogs.
 165:      *
 166:      * <p> This just delegates to {@link #loadCatalog loadCatalog()};
 167:      * see it for exception information.
 168:      *
 169:      * @param uri absolute URI for the catalog file.
 170:      */
 171:     public XCat (String uri)
 172:     throws SAXException, IOException
 173:     { loadCatalog (uri); }
 174: 
 175: 
 176:     /**
 177:      * Loads an OASIS XML Catalog.
 178:      * It is appended to the list of currently active catalogs, or
 179:      * reloaded if a catalog with the same URI was already loaded.
 180:      * Callers have control over what parser is used, how catalog parsing
 181:      * errors are reported, and whether URIs will be resolved consistently.
 182:      *
 183:      * <p> The OASIS specification says that errors detected when loading
 184:      * catalogs "must recover by ignoring the catalog entry file that
 185:      * failed, and proceeding."  In this API, that action can be the
 186:      * responsibility of applications, when they explicitly load any
 187:      * catalog using this method.
 188:      *
 189:      * <p>Note that catalogs referenced by this one will not be loaded
 190:      * at this time.  Catalogs referenced through <em>nextCatalog</em>
 191:      * or <em>delegate*</em> elements are normally loaded only if needed. 
 192:      *
 193:      * @see #setErrorHandler
 194:      * @see #setParserClass
 195:      * @see #setUnified
 196:      *
 197:      * @param uri absolute URI for the catalog file.
 198:      *
 199:      * @exception IOException As thrown by the parser, typically to
 200:      *    indicate problems reading data from that URI.
 201:      * @exception SAXException As thrown by the parser, typically to
 202:      *    indicate problems parsing data from that URI.  It may also
 203:      *  be thrown if the parser doesn't support necessary handlers. 
 204:      * @exception IllegalStateException When attempting to load a
 205:      *    catalog after loading has been {@link #disableLoading disabled},
 206:      *    such as after any entity or URI lookup has been performed.
 207:      */
 208:     public synchronized void loadCatalog (String uri)
 209:     throws SAXException, IOException
 210:     {
 211:     Catalog        catalog;
 212:     int        index = -1;
 213: 
 214:     if (!loadingPermitted)
 215:         throw new IllegalStateException ();
 216:     
 217:     uri = normalizeURI (uri);
 218:     if (catalogs != null) {
 219:         // maybe just reload
 220:         for (index = 0; index < catalogs.length; index++)
 221:         if (uri.equals (catalogs [index].catalogURI))
 222:             break;
 223:     }
 224:     catalog = loadCatalog (parserClass, errorHandler, uri, unified);
 225: 
 226:     // add to list of catalogs
 227:     if (catalogs == null) {
 228:         index = 0;
 229:         catalogs = new Catalog [1];
 230:     } else if (index == catalogs.length) {
 231:         Catalog        tmp [];
 232: 
 233:         tmp = new Catalog [index + 1];
 234:         System.arraycopy (catalogs, 0, tmp, 0, index);
 235:         catalogs = tmp;
 236:     }
 237:     catalogs [index] = catalog;
 238:     }
 239: 
 240: 
 241:     /**
 242:      * "New Style" external entity resolution for parsers.
 243:      * Calls to this method prevent explicit loading of additional catalogs
 244:      * using {@link #loadCatalog loadCatalog()}.
 245:      *
 246:      * <p>This supports the full core catalog functionality for locating
 247:      * (and relocating) parsed entities that have been declared in a
 248:      * document's DTD.
 249:      *
 250:      * @param name Entity name, such as "dudley", "%nell", or "[dtd]".
 251:      * @param publicId Either a normalized public ID, or null.
 252:      * @param baseURI Absolute base URI associated with systemId.
 253:      * @param systemId URI found in entity declaration (may be
 254:      *    relative to baseURI).
 255:      *
 256:      * @return Input source for accessing the external entity, or null
 257:      *    if no mapping was found.  The input source may have opened
 258:      *    the stream, and will have a fully resolved URI.
 259:      *
 260:      * @see #getExternalSubset
 261:      */
 262:     public InputSource resolveEntity (
 263:     String name,        // UNUSED ... systemId is always non-null
 264:     String publicId,
 265:     String baseURI,        // UNUSED ... it just lets sysId be relative
 266:     String systemId
 267:     ) throws SAXException, IOException
 268:     {
 269:     if (loadingPermitted)
 270:         disableLoading ();
 271: 
 272:     try {
 273:         // steps as found in OASIS XML catalog spec 7.1.2
 274:         // steps 1, 8 involve looping over the list of catalogs
 275:         for (int i = 0; i < catalogs.length; i++) {
 276:         InputSource    retval;
 277:         retval = catalogs [i].resolve (usingPublic, publicId, systemId);
 278:         if (retval != null)
 279:             return retval;
 280:         }
 281:     } catch (DoneDelegation x) {
 282:         // done!
 283:     }
 284:     // step 9 involves returning "no match" 
 285:     return null;
 286:     }
 287: 
 288: 
 289:     /**
 290:      * "New Style" parser callback to add an external subset.
 291:      * For documents that don't include an external subset, this may
 292:      * return one according to <em>doctype</em> catalog entries.
 293:      * (This functionality is not a core part of the OASIS XML Catalog
 294:      * specification, though it's presented in an appendix.)
 295:      * If no such entry is defined, this returns null to indicate that
 296:      * this document will not be modified to include such a subset.
 297:      * Calls to this method prevent explicit loading of additional catalogs
 298:      * using {@link #loadCatalog loadCatalog()}.
 299:      *
 300:      * <p><em>Warning:</em> That catalog functionality can be dangerous.
 301:      * It can provide definitions of general entities, and thereby mask
 302:      * certain well formedess errors.
 303:      *
 304:      * @param name Name of the document element, either as declared in
 305:      *    a DOCTYPE declaration or as observed in the text.
 306:      * @param baseURI Document's base URI (absolute).
 307:      *
 308:      * @return Input source for accessing the external subset, or null
 309:      *    if no mapping was found.  The input source may have opened
 310:      *    the stream, and will have a fully resolved URI.
 311:      */
 312:     public InputSource getExternalSubset (String name, String baseURI)
 313:     throws SAXException, IOException
 314:     {
 315:     if (loadingPermitted)
 316:         disableLoading ();
 317:     try {
 318:         for (int i = 0; i < catalogs.length; i++) {
 319:         InputSource retval = catalogs [i].getExternalSubset (name);
 320:         if (retval != null)
 321:             return retval;
 322:         }
 323:     } catch (DoneDelegation x) {
 324:         // done!
 325:     }
 326:     return null;
 327:     }
 328: 
 329: 
 330:     /**
 331:      * "Old Style" external entity resolution for parsers.
 332:      * This API provides only core functionality.
 333:      * Calls to this method prevent explicit loading of additional catalogs
 334:      * using {@link #loadCatalog loadCatalog()}.
 335:      *
 336:      * <p>The functional limitations of this interface include:</p><ul>
 337:      *
 338:      *    <li>Since system IDs will be absolutized before the resolver
 339:      *    sees them, matching against relative URIs won't work.
 340:      *    This may affect <em>system</em>, <em>rewriteSystem</em>,
 341:      *    and <em>delegateSystem</em> catalog entries.
 342:      *
 343:      *    <li>Because of that absolutization, documents declaring entities
 344:      *    with system IDs using URI schemes that the JVM does not recognize
 345:      *    may be unparsable.  URI schemes such as <em>file:/</em>,
 346:      *    <em>http://</em>, <em>https://</em>, and <em>ftp://</em>
 347:      *    will usually work reliably.
 348:      *
 349:      *    <li>Because missing external subsets can't be provided, the
 350:      *    <em>doctype</em> catalog entries will be ignored.
 351:      *    (The {@link #getExternalSubset getExternalSubset()} method is
 352:      *    a "New Style" resolution option.)
 353:      *
 354:      *    </ul>
 355:      *
 356:      * <p>Applications can tell whether this limited functionality will be
 357:      * used: if the feature flag associated with the {@link EntityResolver2}
 358:      * interface is not <em>true</em>, the limitations apply.  Applications
 359:      * can't usually know whether a given document and catalog will trigger
 360:      * those limitations.  The issue can only be bypassed by operational
 361:      * procedures such as not using catalogs or documents which involve
 362:      * those features.
 363:      *
 364:      * @param publicId Either a normalized public ID, or null
 365:      * @param systemId Always an absolute URI.
 366:      *
 367:      * @return Input source for accessing the external entity, or null
 368:      *    if no mapping was found.  The input source may have opened
 369:      *    the stream, and will have a fully resolved URI.
 370:      */
 371:     final public InputSource resolveEntity (String publicId, String systemId)
 372:     throws SAXException, IOException
 373:     {
 374:     return resolveEntity (null, publicId, null, systemId);
 375:     }
 376: 
 377: 
 378:     /**
 379:      * Resolves a URI reference that's not defined to the DTD.
 380:      * This is intended for use with URIs found in document text, such as
 381:      * <em>xml-stylesheet</em> processing instructions and in attribute
 382:      * values, where they are not recognized as URIs by XML parsers.
 383:      * Calls to this method prevent explicit loading of additional catalogs
 384:      * using {@link #loadCatalog loadCatalog()}.
 385:      *
 386:      * <p>This functionality is supported by the OASIS XML Catalog
 387:      * specification, but will never be invoked by an XML parser.
 388:      * It corresponds closely to functionality for mapping system
 389:      * identifiers for entities declared in DTDs; closely enough that
 390:      * this implementation's default behavior is that they be
 391:      * identical, to minimize potential confusion.
 392:      *
 393:      * <p>This method could be useful when implementing the
 394:      * {@link javax.xml.transform.URIResolver} interface, wrapping the
 395:      * input source in a {@link javax.xml.transform.sax.SAXSource}.
 396:      *
 397:      * @see #isUnified
 398:      * @see #setUnified
 399:      *
 400:      * @param baseURI The relevant base URI as specified by the XML Base
 401:      *    specification.  This recognizes <em>xml:base</em> attributes
 402:      *    as overriding the actual (physical) base URI.
 403:      * @param uri Either an absolute URI, or one relative to baseURI
 404:      *
 405:      * @return Input source for accessing the mapped URI, or null
 406:      *    if no mapping was found.  The input source may have opened
 407:      *    the stream, and will have a fully resolved URI.
 408:      */
 409:     public InputSource resolveURI (String baseURI, String uri)
 410:     throws SAXException, IOException
 411:     {
 412:     if (loadingPermitted)
 413:         disableLoading ();
 414: 
 415:     // NOTE:  baseURI isn't used here, but caller MUST have it,
 416:     // and heuristics _might_ use it in the future ... plus,
 417:     // it's symmetric with resolveEntity ().
 418: 
 419:     // steps 1, 6 involve looping
 420:     try {
 421:         for (int i = 0; i < catalogs.length; i++) {
 422:         InputSource    tmp = catalogs [i].resolveURI (uri);
 423:         if (tmp != null)
 424:             return tmp;
 425:         }
 426:     } catch (DoneDelegation x) {
 427:         // done
 428:     }
 429:     // step 7 reports no match
 430:     return null;
 431:     }
 432: 
 433: 
 434:     /** 
 435:      * Records that catalog loading is no longer permitted.
 436:      * Loading is automatically disabled when lookups are performed,
 437:      * and should be manually disabled when <em>startDTD()</em> (or
 438:      * any other DTD declaration callback) is invoked, or at the latest
 439:      * when the document root element is seen.
 440:      */
 441:     public synchronized void disableLoading ()
 442:     {
 443:     // NOTE:  this method and loadCatalog() are synchronized
 444:     // so that it's impossible to load (top level) catalogs
 445:     // after lookups start.  Likewise, deferred loading is also
 446:     // synchronized (for "next" and delegated catalogs) to
 447:     // ensure that parsers can share resolvers.
 448:     loadingPermitted = false;
 449:     }
 450: 
 451: 
 452:     /**
 453:      * Returns the error handler used to report catalog errors.
 454:      * Null is returned if the parser's default error handling
 455:      * will be used.
 456:      *
 457:      * @see #setErrorHandler
 458:      */
 459:     public ErrorHandler getErrorHandler ()
 460:     { return errorHandler; }
 461: 
 462:     /**
 463:      * Assigns the error handler used to report catalog errors.
 464:      * These errors may come either from the SAX2 parser or
 465:      * from the catalog parsing code driven by the parser. 
 466:      *
 467:      * <p> If you're sharing the resolver between parsers, don't
 468:      * change this once lookups have begun.
 469:      *
 470:      * @see #getErrorHandler
 471:      *
 472:      * @param parser The error handler, or null saying to use the default
 473:      *    (no diagnostics, and only fatal errors terminate loading).
 474:      */
 475:     public void setErrorHandler (ErrorHandler handler)
 476:     { errorHandler = handler; }
 477: 
 478: 
 479:     /**
 480:      * Returns the name of the SAX2 parser class used to parse catalogs.
 481:      * Null is returned if the system default is used.
 482:      * @see #setParserClass
 483:      */
 484:     public String getParserClass ()
 485:     { return parserClass; }
 486: 
 487:     /**
 488:      * Names the SAX2 parser class used to parse catalogs.
 489:      *
 490:      * <p> If you're sharing the resolver between parsers, don't change
 491:      * this once lookups have begun.
 492:      *
 493:      * <p> Note that in order to properly support the <em>xml:base</em>
 494:      * attribute and relative URI resolution, the SAX parser used to parse
 495:      * the catalog must provide a {@link Locator} and support the optional
 496:      * declaration and lexical handlers.
 497:      *
 498:      * @see #getParserClass
 499:      *
 500:      * @param parser The parser class name, or null saying to use the
 501:      *    system default SAX2 parser.
 502:      */
 503:     public void setParserClass (String parser)
 504:     { parserClass = parser; }
 505: 
 506: 
 507:     /**
 508:      * Returns true (the default) if all methods resolve
 509:      * a given URI in the same way.
 510:      * Returns false if calls resolving URIs as entities (such as
 511:      * {@link #resolveEntity resolveEntity()}) use different catalog entries
 512:      * than those resolving them as URIs ({@link #resolveURI resolveURI()}),
 513:      * which will generally produce different results.
 514:      *
 515:      * <p>The OASIS XML Catalog specification defines two related schemes
 516:      * to map URIs "as URIs" or "as system IDs".
 517:      * URIs use <em>uri</em>, <em>rewriteURI</em>, and <em>delegateURI</em>
 518:      * elements.  System IDs do the same things with <em>systemId</em>,
 519:      * <em>rewriteSystemId</em>, and <em>delegateSystemId</em>.
 520:      * It's confusing and error prone to maintain two parallel copies of
 521:      * such data.  Accordingly, this class makes that behavior optional.
 522:      * The <em>unified</em> interpretation of URI mappings is preferred,
 523:      * since it prevents surprises where one URI gets mapped to different
 524:      * contents depending on whether the reference happens to have come
 525:      * from a DTD (or not).
 526:      *
 527:      * @see #setUnified
 528:      */
 529:     public boolean isUnified ()
 530:     { return unified; }
 531: 
 532:     /**
 533:      * Assigns the value of the flag returned by {@link #isUnified}.
 534:      * Set it to false to be strictly conformant with the OASIS XML Catalog
 535:      * specification.  Set it to true to make all mappings for a given URI
 536:      * give the same result, regardless of the reason for the mapping.
 537:      *
 538:      * <p>Don't change this once you've loaded the first catalog.
 539:      *
 540:      * @param value new flag setting
 541:      */
 542:     public void setUnified (boolean value)
 543:     { unified = value; }
 544: 
 545: 
 546:     /**
 547:      * Returns true (the default) if a catalog's public identifier
 548:      * mappings will be used.
 549:      * When false is returned, such mappings are ignored except when
 550:      * system IDs are discarded, such as for
 551:      * entities using the <em>urn:publicid:</em> URI scheme in their
 552:      * system identifiers.  (See RFC 3151 for information about that
 553:      * URI scheme.  Using it in system identifiers may not work well
 554:      * with many SAX parsers unless the <em>resolve-dtd-uris</em>
 555:      * feature flag is set to false.)
 556:      * @see #setUsingPublic
 557:      */
 558:     public boolean isUsingPublic ()
 559:     { return usingPublic; }
 560: 
 561:     /**
 562:      * Specifies which catalog search mode is used.
 563:      * By default, public identifier mappings are able to override system
 564:      * identifiers when both are available.
 565:      * Applications may choose to ignore public
 566:      * identifier mappings in such cases, so that system identifiers
 567:      * declared in DTDs will only be overridden by an explicit catalog
 568:      * match for that system ID.
 569:      *
 570:      * <p> If you're sharing the resolver between parsers, don't
 571:      * change this once lookups have begun.
 572:      * @see #isUsingPublic
 573:      *
 574:      * @param value true to always use public identifier mappings,
 575:      *    false to only use them for system ids using the <em>urn:publicid:</em>
 576:      *    URI scheme.
 577:      */
 578:     public void setUsingPublic (boolean value)
 579:     { usingPublic = value; }
 580: 
 581: 
 582: 
 583:     // hmm, what's this do? :)
 584:     private static Catalog loadCatalog (
 585:     String        parserClass,
 586:     ErrorHandler    eh,
 587:     String        uri,
 588:     boolean        unified
 589:     ) throws SAXException, IOException
 590:     {
 591:     XMLReader    parser;
 592:     Loader        loader;
 593:     boolean        doesIntern = false;
 594: 
 595:     if (parserClass == null)
 596:         parser = XMLReaderFactory.createXMLReader ();
 597:     else
 598:         parser = XMLReaderFactory.createXMLReader (parserClass);
 599:     if (eh != null)
 600:         parser.setErrorHandler (eh);
 601:     // resolve-dtd-entities is at default value (unrecognized == true)
 602: 
 603:     try {
 604:         doesIntern = parser.getFeature (
 605:         "http://xml.org/sax/features/string-interning");
 606:     } catch (SAXNotRecognizedException e) { }
 607: 
 608:     loader = new Loader (doesIntern, eh, unified);
 609:     loader.cat.parserClass = parserClass;
 610:     loader.cat.catalogURI = uri;
 611: 
 612:     parser.setContentHandler (loader);
 613:     parser.setProperty (
 614:         "http://xml.org/sax/properties/declaration-handler",
 615:         loader);
 616:     parser.setProperty (
 617:         "http://xml.org/sax/properties/lexical-handler",
 618:         loader);
 619:     parser.parse (uri);
 620: 
 621:     return loader.cat;
 622:     }
 623: 
 624:     // perform one or both the normalizations for public ids
 625:     private static String normalizePublicId (boolean full, String publicId)
 626:     {
 627:     if (publicId.startsWith ("urn:publicid:")) {
 628:         CPStringBuilder    buf = new CPStringBuilder ();
 629:         char        chars [] = publicId.toCharArray ();
 630: boolean hasbug = false;
 631: 
 632:         for (int i = 13; i < chars.length; i++) {
 633:         switch (chars [i]) {
 634:         case '+':     buf.append (' '); continue;
 635:         case ':':     buf.append ("//"); continue;
 636:         case ';':     buf.append ("::"); continue;
 637:         case '%':
 638: // FIXME unhex that char!  meanwhile, warn and fallthrough ...
 639:             hasbug = true;
 640:         default:    buf.append (chars [i]); continue;
 641:         }
 642:         }
 643:         publicId = buf.toString ();
 644: if (hasbug)
 645: System.err.println ("nyet unhexing public id: " + publicId);
 646:         full = true;
 647:     }
 648: 
 649:     // SAX parsers do everything except that URN mapping, but
 650:     // we can't trust other sources to normalize correctly
 651:     if (full) {
 652:         StringTokenizer    tokens;
 653:         String        token;
 654: 
 655:         tokens = new StringTokenizer (publicId, " \r\n");
 656:         publicId = null;
 657:         while (tokens.hasMoreTokens ()) {
 658:         if (publicId == null)
 659:             publicId = tokens.nextToken ();
 660:         else
 661:             publicId += " " + tokens.nextToken ();
 662:         }
 663:     }
 664:     return publicId;
 665:     }
 666: 
 667:     private static boolean isUriExcluded (int c)
 668:     { return c <= 0x20 || c >= 0x7f || "\"<>^`{|}".indexOf (c) != -1; }
 669: 
 670:     private static int hexNibble (int c)
 671:     {
 672:     if (c < 10)
 673:         return c + '0';
 674:     return ('a' - 10) + c;
 675:     }
 676: 
 677:     // handles URIs with "excluded" characters
 678:     private static String normalizeURI (String systemId)
 679:     {
 680:     int            length = systemId.length ();
 681: 
 682:     for (int i = 0; i < length; i++) {
 683:         char    c = systemId.charAt (i);
 684: 
 685:         // escape non-ASCII plus "excluded" characters
 686:         if (isUriExcluded (c)) {
 687:         byte            buf [];
 688:         ByteArrayOutputStream    out;
 689:         int                b;
 690: 
 691:         // a JVM that doesn't know UTF8 and 8859_1 is unusable!
 692:         try {
 693:             buf = systemId.getBytes ("UTF8");
 694:             out = new ByteArrayOutputStream (buf.length + 10);
 695: 
 696:             for (i = 0; i < buf.length; i++) {
 697:             b = buf [i] & 0x0ff;
 698:             if (isUriExcluded (b)) {
 699:                 out.write ((int) '%');
 700:                 out.write (hexNibble (b >> 4));
 701:                 out.write (hexNibble (b & 0x0f));
 702:             } else
 703:                 out.write (b);
 704:             }
 705:             return out.toString ("8859_1");
 706:         } catch (IOException e) {
 707:             throw new RuntimeException (
 708:             "can't normalize URI: " + e.getMessage ());
 709:         }
 710:         }
 711:     }
 712:     return systemId;
 713:     }
 714: 
 715:     // thrown to mark authoritative end of a search
 716:     private static class DoneDelegation extends SAXException
 717:     {
 718:     DoneDelegation () { }
 719:     }
 720: 
 721: 
 722:     /**
 723:      * Represents a OASIS XML Catalog, and encapsulates much of
 724:      * the catalog functionality.
 725:      */
 726:     private static class Catalog
 727:     {
 728:     // loading infrastructure
 729:     String        catalogURI;
 730:     ErrorHandler    eh;
 731:     boolean        unified;
 732:     String        parserClass;
 733: 
 734:     // catalog data
 735:     boolean        hasPreference;
 736:     boolean        usingPublic;
 737: 
 738:     Hashtable    publicIds;
 739:     Hashtable    publicDelegations;
 740: 
 741:     Hashtable    systemIds;
 742:     Hashtable    systemRewrites;
 743:     Hashtable    systemDelegations;
 744: 
 745:     Hashtable    uris;
 746:     Hashtable    uriRewrites;
 747:     Hashtable    uriDelegations;
 748: 
 749:     Hashtable    doctypes;
 750: 
 751:     Vector        next;
 752: 
 753:     // nonpublic!
 754:     Catalog () { }
 755: 
 756:     
 757:     // steps as found in OASIS XML catalog spec 7.1.2
 758:     private InputSource locatePublicId (String publicId)
 759:     throws SAXException, IOException
 760:     {
 761:         // 5. return (first) 'public' entry
 762:         if (publicIds != null) {
 763:         String    retval = (String) publicIds.get (publicId);
 764:         if (retval != null) {
 765:             // IF the URI is accessible ...
 766:             return new InputSource (retval);
 767:         }
 768:         }
 769: 
 770:         // 6. return delegatePublic catalog match [complex]
 771:         if (publicDelegations != null)
 772:         return checkDelegations (publicDelegations, publicId,
 773:                 publicId, null);
 774:         
 775:         return null;
 776:     }
 777: 
 778:     // steps as found in OASIS XML catalog spec 7.1.2 or 7.2.2
 779:     private InputSource mapURI (
 780:         String    uri,
 781:         Hashtable    ids,
 782:         Hashtable    rewrites,
 783:         Hashtable    delegations
 784:     ) throws SAXException, IOException
 785:     {
 786:         // 7.1.2: 2. return (first) 'system' entry
 787:         // 7.2.2: 2. return (first) 'uri' entry
 788:         if (ids != null) {
 789:         String    retval = (String) ids.get (uri);
 790:         if (retval != null) {
 791:             // IF the URI is accessible ...
 792:             return new InputSource (retval);
 793:         }
 794:         }
 795: 
 796:         // 7.1.2: 3. return 'rewriteSystem' entries
 797:         // 7.2.2: 3. return 'rewriteURI' entries
 798:         if (rewrites != null) {
 799:         String    prefix = null;
 800:         String    replace = null;
 801:         int    prefixLen = -1;
 802: 
 803:         for (Enumeration e = rewrites.keys ();
 804:             e.hasMoreElements ();
 805:             /* NOP */) {
 806:             String    temp = (String) e.nextElement ();
 807:             int        len = -1;
 808: 
 809:             if (!uri.startsWith (temp))
 810:             continue;
 811:             if (prefix != null
 812:                 && (len = temp.length ()) < prefixLen)
 813:             continue;
 814:             prefix = temp;
 815:             prefixLen = len;
 816:             replace = (String) rewrites.get (temp);
 817:         }
 818:         if (prefix != null) {
 819:             CPStringBuilder    buf = new CPStringBuilder (replace);
 820:             buf.append (uri.substring (prefixLen));
 821:             // IF the URI is accessible ...
 822:             return new InputSource (buf.toString ());
 823:         }
 824:         }
 825: 
 826:         // 7.1.2: 4. return 'delegateSystem' catalog match [complex]
 827:         // 7.2.2: 4. return 'delegateURI' catalog match [complex]
 828:         if (delegations != null)
 829:         return checkDelegations (delegations, uri, null, uri);
 830: 
 831:         return null;
 832:     }
 833: 
 834: 
 835:     /**
 836:      * Returns a URI for an external entity.
 837:      */
 838:     public InputSource resolve (
 839:         boolean    usingPublic,
 840:         String    publicId,
 841:         String    systemId
 842:     ) throws SAXException, IOException
 843:     {
 844:         boolean    preferSystem;
 845:         InputSource    retval;
 846: 
 847:         if (hasPreference)
 848:         preferSystem = !this.usingPublic;
 849:         else
 850:         preferSystem = !usingPublic;
 851:         
 852:         if (publicId != null)
 853:         publicId = normalizePublicId (false, publicId);
 854: 
 855:         // behavior here matches section 7.1.1 of the oasis spec
 856:         if (systemId != null) {
 857:         if (systemId.startsWith ("urn:publicid:")) {
 858:             String    temp = normalizePublicId (true, systemId);
 859:             if (publicId == null) {
 860:             publicId = temp;
 861:             systemId = null;
 862:             } else if (!publicId.equals (temp)) {
 863:             // error; ok to recover by:
 864:             systemId = null;
 865:             }
 866:         } else
 867:             systemId = normalizeURI (systemId);
 868:         }
 869: 
 870:         if (systemId == null && publicId == null)
 871:         return null;
 872: 
 873:         if (systemId != null) {
 874:         retval = mapURI (systemId, systemIds, systemRewrites,
 875:                     systemDelegations);
 876:         if (retval != null) {
 877:             retval.setPublicId (publicId);
 878:             return retval;
 879:         }
 880:         }
 881: 
 882:         if (publicId != null
 883:             && !(systemId != null && preferSystem)) {
 884:         retval = locatePublicId (publicId);
 885:         if (retval != null) {
 886:             retval.setPublicId (publicId);
 887:             return retval;
 888:         }
 889:         }
 890: 
 891:         // 7. apply nextCatalog entries
 892:         if (next != null) {
 893:         int    length = next.size ();
 894:         for (int i = 0; i < length; i++) {
 895:             Catalog    n = getNext (i);
 896:             retval = n.resolve (usingPublic, publicId, systemId);
 897:             if (retval != null)
 898:             return retval;
 899:         }
 900:         }
 901: 
 902:         return null;
 903:     }
 904: 
 905:     /**
 906:      * Maps one URI into another, for resources that are not defined
 907:      * using XML external entity or notation syntax.
 908:      */
 909:     public InputSource resolveURI (String uri)
 910:     throws SAXException, IOException
 911:     {
 912:         if (uri.startsWith ("urn:publicid:"))
 913:         return resolve (true, normalizePublicId (true, uri), null);
 914: 
 915:         InputSource    retval;
 916: 
 917:         uri = normalizeURI (uri);
 918: 
 919:         // 7.2.2 steps 2-4
 920:         retval = mapURI (uri, uris, uriRewrites, uriDelegations);
 921:         if (retval != null)
 922:         return retval;
 923: 
 924:         // 7.2.2 step 5. apply nextCatalog entries
 925:         if (next != null) {
 926:         int    length = next.size ();
 927:         for (int i = 0; i < length; i++) {
 928:             Catalog    n = getNext (i);
 929:             retval = n.resolveURI (uri);
 930:             if (retval != null)
 931:             return retval;
 932:         }
 933:         }
 934: 
 935:         return null;
 936:     }
 937: 
 938: 
 939:     /**
 940:      * Finds the external subset associated with a given root element.
 941:      */
 942:     public InputSource getExternalSubset (String name)
 943:     throws SAXException, IOException
 944:     {
 945:         if (doctypes != null) {
 946:         String    value = (String) doctypes.get (name);
 947:         if (value != null) {
 948:             // IF the URI is accessible ...
 949:             return new InputSource (value);
 950:         }
 951:         }
 952:         if (next != null) {
 953:         int    length = next.size ();
 954:         for (int i = 0; i < length; i++) {
 955:             Catalog    n = getNext (i);
 956:             if (n == null)
 957:             continue;
 958:             InputSource    retval = n.getExternalSubset (name);
 959:             if (retval != null)
 960:             return retval;
 961:         }
 962:         }
 963:         return null;
 964:     }
 965: 
 966:     private synchronized Catalog getNext (int i)
 967:     throws SAXException, IOException
 968:     {
 969:         Object    obj;
 970: 
 971:         if (next == null || i < 0 || i >= next.size ())
 972:         return null;
 973:         obj = next.elementAt (i);
 974:         if (obj instanceof Catalog)
 975:         return (Catalog) obj;
 976:         
 977:         // ok, we deferred reading that catalog till now.
 978:         // load and cache it.
 979:         Catalog    cat = null;
 980: 
 981:         try {
 982:         cat = loadCatalog (parserClass, eh, (String) obj, unified);
 983:         next.setElementAt (cat, i);
 984:         } catch (SAXException e) {
 985:         // must fail quietly, says the OASIS spec
 986:         } catch (IOException e) {
 987:         // same applies here
 988:         }
 989:         return cat;
 990:     }
 991: 
 992:     private InputSource checkDelegations (
 993:         Hashtable    delegations,
 994:         String    id,
 995:         String    publicId,    // only one of public/system
 996:         String    systemId    // will be non-null...
 997:     ) throws SAXException, IOException
 998:     {
 999:         Vector    matches = null;
1000:         int        length = 0;
1001: 
1002:         // first, see if any prefixes match.
1003:         for (Enumeration e = delegations.keys ();
1004:             e.hasMoreElements ();
1005:             /* NOP */) {
1006:         String    prefix = (String) e.nextElement ();
1007: 
1008:         if (!id.startsWith (prefix))
1009:             continue;
1010:         if (matches == null)
1011:             matches = new Vector ();
1012:         
1013:         // maintain in longer->shorter sorted order
1014:         // NOTE:  assumes not many matches will fire!
1015:         int    index;
1016: 
1017:         for (index = 0; index < length; index++) {
1018:             String    temp = (String) matches.elementAt (index);
1019:             if (prefix.length () > temp.length ()) {
1020:             matches.insertElementAt (prefix, index);
1021:             break;
1022:             }
1023:         }
1024:         if (index == length)
1025:             matches.addElement (prefix);
1026:         length++;
1027:         }
1028:         if (matches == null)
1029:         return null;
1030: 
1031:         // now we know the list of catalogs to replace our "top level"
1032:         // list ... we use it here, rather than somehow going back and
1033:         // restarting, since this helps avoid reading most catalogs.
1034:         // this assumes stackspace won't be a problem.
1035:         for (int i = 0; i < length; i++) {
1036:         Catalog        catalog = null;
1037:         InputSource    result;
1038: 
1039:         // get this catalog.  we may not have read it yet.
1040:         synchronized (delegations) {
1041:             Object    prefix = matches.elementAt (i);
1042:             Object    cat = delegations.get (prefix);
1043: 
1044:             if (cat instanceof Catalog)
1045:             catalog = (Catalog) cat;
1046:             else {
1047:             try {
1048:                 // load and cache that catalog
1049:                 catalog = loadCatalog (parserClass, eh,
1050:                     (String) cat, unified);
1051:                 delegations.put (prefix, catalog);
1052:             } catch (SAXException e) {
1053:                 // must ignore, says the OASIS spec
1054:             } catch (IOException e) {
1055:                 // same applies here
1056:             }
1057:             }
1058:         }
1059: 
1060:         // ignore failed loads, and proceed
1061:         if (catalog == null)
1062:             continue;
1063:         
1064:         // we have a catalog ... resolve!
1065:         // usingPublic value can't matter, there's no choice
1066:         result = catalog.resolve (true, publicId, systemId);
1067:         if (result != null)
1068:             return result;
1069:         }
1070: 
1071:         // if there were no successes, the entire
1072:         // lookup failed (all the way to top level)
1073:         throw new DoneDelegation ();
1074:     }
1075:     }
1076: 
1077: 
1078:     /** This is the namespace URI used for OASIS XML Catalogs.  */
1079:     private static final String    catalogNamespace =
1080:         "urn:oasis:names:tc:entity:xmlns:xml:catalog";
1081: 
1082: 
1083:     /**
1084:      * Loads/unmarshals one catalog.
1085:      */
1086:     private static class Loader extends DefaultHandler2
1087:     {
1088:     private boolean        preInterned;
1089:     private ErrorHandler    handler;
1090:     private boolean        unified;
1091:     private int        ignoreDepth;
1092:     private Locator        locator;
1093:     private boolean        started;
1094:     private Hashtable    externals;
1095:     private Stack        bases;
1096: 
1097:     Catalog            cat = new Catalog ();
1098: 
1099: 
1100:     /**
1101:      * Constructor.
1102:      * @param flag true iff the parser already interns strings.
1103:      * @param eh Errors and warnings are delegated to this.
1104:      * @param unified true keeps one table for URI mappings;
1105:      *    false matches OASIS spec, storing mappings
1106:      *    for URIs and SYSTEM ids in parallel tables.
1107:      */
1108:     Loader (boolean flag, ErrorHandler eh, boolean unified)
1109:     {
1110:         preInterned = flag;
1111:         handler = eh;
1112:         this.unified = unified;
1113:         cat.unified = unified;
1114:         cat.eh = eh;
1115:     }
1116: 
1117: 
1118:     // strips out fragments
1119:     private String nofrag (String uri)
1120:     throws SAXException
1121:     {
1122:         if (uri.indexOf ('#') != -1) {
1123:         warn ("URI with fragment: " + uri);
1124:         uri = uri.substring (0, uri.indexOf ('#'));
1125:         }
1126:         return uri;
1127:     }
1128: 
1129:     // absolutizes relative URIs
1130:     private String absolutize (String uri)
1131:     throws SAXException
1132:     {
1133:         // avoid creating URLs if they're already absolutized,
1134:         // or if the URI is already using a known scheme
1135:         if (uri.startsWith ("file:/")
1136:             || uri.startsWith ("http:/")
1137:             || uri.startsWith ("https:/")
1138:             || uri.startsWith ("ftp:/")
1139:             || uri.startsWith ("urn:")
1140:             )
1141:         return uri;
1142: 
1143:         // otherwise, let's hope the JDK handles this URI scheme.
1144:         try {
1145:         URL    base = (URL) bases.peek ();
1146:         return new URL (base, uri).toString ();
1147:         } catch (Exception e) {
1148:         fatal ("can't absolutize URI: " + uri);
1149:         return null;
1150:         }
1151:     }
1152: 
1153:     // recoverable error
1154:     private void error (String message)
1155:     throws SAXException
1156:     {
1157:         if (handler == null)
1158:         return;
1159:         handler.error (new SAXParseException (message, locator));
1160:     }
1161: 
1162:     // nonrecoverable error
1163:     private void fatal (String message)
1164:     throws SAXException
1165:     {
1166:         SAXParseException    spe;
1167:         
1168:         spe = new SAXParseException (message, locator);
1169:         if (handler != null)
1170:         handler.fatalError (spe);
1171:         throw spe;
1172:     }
1173: 
1174:     // low severity problem
1175:     private void warn (String message)
1176:     throws SAXException
1177:     {
1178:         if (handler == null)
1179:         return;
1180:         handler.warning (new SAXParseException (message, locator));
1181:     }
1182: 
1183:     // callbacks:
1184: 
1185:     public void setDocumentLocator (Locator l)
1186:         { locator = l; }
1187: 
1188:     public void startDocument ()
1189:     throws SAXException
1190:     {
1191:         if (locator == null)
1192:         error ("no locator!");
1193:         bases = new Stack ();
1194:         String    uri = locator.getSystemId ();
1195:         try {
1196:         bases.push (new URL (uri));
1197:         } catch (IOException e) {
1198:         fatal ("bad document base URI: " + uri);
1199:         }
1200:     }
1201: 
1202:     public void endDocument ()
1203:     throws SAXException
1204:     {
1205:         try {
1206:         if (!started)
1207:             error ("not a catalog!");
1208:         } finally {
1209:         locator = null;
1210:         handler = null;
1211:         externals = null;
1212:         bases = null;
1213:         }
1214:     }
1215: 
1216:     // XML Base support for external entities.
1217: 
1218:     // NOTE: expects parser is in default "resolve-dtd-uris" mode.
1219:     public void externalEntityDecl (String name, String pub, String sys)
1220:     throws SAXException
1221:     {
1222:         if (externals == null)
1223:         externals = new Hashtable ();
1224:         if (externals.get (name) == null)
1225:         externals.put (name, pub);
1226:     }
1227: 
1228:     public void startEntity (String name)
1229:     throws SAXException
1230:     {
1231:         if (externals == null)
1232:         return;
1233:         String uri = (String) externals.get (name);
1234: 
1235:         // NOTE: breaks if an EntityResolver substitutes these URIs.
1236:         // If toplevel loader supports one, must intercept calls...
1237:         if (uri != null) {
1238:         try {
1239:             bases.push (new URL (uri));
1240:         } catch (IOException e) {
1241:             fatal ("entity '" + name + "', bad URI: " + uri);
1242:         }
1243:         }
1244:     }
1245: 
1246:     public void endEntity (String name)
1247:     {
1248:         if (externals == null)
1249:         return;
1250:         String value = (String) externals.get (name);
1251: 
1252:         if (value != null)
1253:         bases.pop ();
1254:     }
1255: 
1256:     /**
1257:      * Processes catalog elements, saving their data.
1258:      */
1259:     public void startElement (String namespace, String local,
1260:         String qName, Attributes atts)
1261:     throws SAXException
1262:     {
1263:         // must ignore non-catalog elements, and their contents
1264:         if (ignoreDepth != 0 || !catalogNamespace.equals (namespace)) {
1265:         ignoreDepth++;
1266:         return;
1267:         }
1268: 
1269:         // basic sanity checks
1270:         if (!preInterned)
1271:         local = local.intern ();
1272:         if (!started) {
1273:         started = true;
1274:         if ("catalog" != local)
1275:             fatal ("root element not 'catalog': " + local);
1276:         }
1277: 
1278:         // Handle any xml:base attribute
1279:         String    xmlbase = atts.getValue ("xml:base");
1280: 
1281:         if (xmlbase != null) {
1282:         URL    base = (URL) bases.peek ();
1283:         try {
1284:             base = new URL (base, xmlbase);
1285:         } catch (IOException e) {
1286:             fatal ("can't resolve xml:base attribute: " + xmlbase);
1287:         }
1288:         bases.push (base);
1289:         } else
1290:         bases.push (bases.peek ());
1291: 
1292:         // fetch multi-element attributes, apply standard tweaks
1293:         // values (uri, catalog, rewritePrefix) get normalized too,
1294:         // as a precaution and since we may compare the values
1295:         String    catalog = atts.getValue ("catalog");
1296:         if (catalog != null)
1297:         catalog = normalizeURI (absolutize (catalog));
1298: 
1299:         String    rewritePrefix = atts.getValue ("rewritePrefix");
1300:         if (rewritePrefix != null)
1301:         rewritePrefix = normalizeURI (absolutize (rewritePrefix));
1302: 
1303:         String    systemIdStartString;
1304:         systemIdStartString = atts.getValue ("systemIdStartString");
1305:         if (systemIdStartString != null) {
1306:         systemIdStartString = normalizeURI (systemIdStartString);
1307:         // unmatchable <rewriteSystemId>, <delegateSystemId> elements
1308:         if (systemIdStartString.startsWith ("urn:publicid:")) {
1309:             error ("systemIdStartString is really a publicId!!");
1310:             return;
1311:         }
1312:         }
1313: 
1314:         String    uri = atts.getValue ("uri");
1315:         if (uri != null)
1316:         uri = normalizeURI (absolutize (uri));
1317: 
1318:         String    uriStartString;
1319:         uriStartString = atts.getValue ("uriStartString");
1320:         if (uriStartString != null) {
1321:         uriStartString = normalizeURI (uriStartString);
1322:         // unmatchable <rewriteURI>, <delegateURI> elements
1323:         if (uriStartString.startsWith ("urn:publicid:")) {
1324:             error ("uriStartString is really a publicId!!");
1325:             return;
1326:         }
1327:         }
1328: 
1329:         // strictly speaking "group" and "catalog" shouldn't nest
1330:         // ... arbitrary restriction, no evident motivation
1331: 
1332: // FIXME stack "prefer" settings (two elements only!) and use
1333: // them to populate different public mapping/delegation tables
1334: 
1335:         if ("catalog" == local || "group" == local) {
1336:         String    prefer = atts.getValue ("prefer");
1337: 
1338:         if (prefer != null && !"public".equals (prefer)) {
1339:             if (!"system".equals (prefer)) {
1340:             error ("in <" + local + " ... prefer='...'>, "
1341:                 + "assuming 'public'");
1342:             prefer = "public";
1343:             }
1344:         }
1345:         if (prefer != null) {
1346:             if ("catalog" == local) {
1347:             cat.hasPreference = true;
1348:             cat.usingPublic = "public".equals (prefer);
1349:             } else {
1350:             if (!cat.hasPreference || cat.usingPublic
1351:                     != "public".equals (prefer)) {
1352: fatal ("<group prefer=...> case not handled");
1353:             }
1354:             }
1355:         } else if ("group" == local && cat.hasPreference) {
1356: fatal ("<group prefer=...> case not handled");
1357:         }
1358: 
1359:         //
1360:         // PUBLIC ids:  cleanly set up for id substitution
1361:         //
1362:         } else if ("public" == local) {
1363:         String    publicId = atts.getValue ("publicId");
1364:         String    value = null;
1365: 
1366:         if (publicId == null || uri == null) {
1367:             error ("expecting <public publicId=... uri=.../>");
1368:             return;
1369:         }
1370:         publicId = normalizePublicId (true, publicId);
1371:         uri = nofrag (uri);
1372:         if (cat.publicIds == null)
1373:             cat.publicIds = new Hashtable ();
1374:         else
1375:             value = (String) cat.publicIds.get (publicId);
1376:         if (value != null) {
1377:             if (!value.equals (uri))
1378:             warn ("ignoring <public...> entry for " + publicId);
1379:         } else
1380:             cat.publicIds.put (publicId, uri);
1381: 
1382:         } else if ("delegatePublic" == local) {
1383:         String    publicIdStartString;
1384:         Object    value = null;
1385: 
1386:         publicIdStartString = atts.getValue ("publicIdStartString");
1387:         if (publicIdStartString == null || catalog == null) {
1388:             error ("expecting <delegatePublic "
1389:             + "publicIdStartString=... catalog=.../>");
1390:             return;
1391:         }
1392:         publicIdStartString = normalizePublicId (true,
1393:             publicIdStartString);
1394:         if (cat.publicDelegations == null)
1395:             cat.publicDelegations = new Hashtable ();
1396:         else
1397:             value = cat.publicDelegations.get (publicIdStartString);
1398:         if (value != null) {
1399:             if (!value.equals (catalog))
1400:             warn ("ignoring <delegatePublic...> entry for "
1401:                 + uriStartString);
1402:         } else
1403:             cat.publicDelegations.put (publicIdStartString, catalog);
1404: 
1405: 
1406:         //
1407:         // SYSTEM ids:  need substitution due to operational issues
1408:         //
1409:         } else if ("system" == local) {
1410:         String    systemId = atts.getValue ("systemId");
1411:         String    value = null;
1412: 
1413:         if (systemId == null || uri == null) {
1414:             error ("expecting <system systemId=... uri=.../>");
1415:             return;
1416:         }
1417:         systemId = normalizeURI (systemId);
1418:         uri = nofrag (uri);
1419:         if (systemId.startsWith ("urn:publicid:")) {
1420:             error ("systemId is really a publicId!!");
1421:             return;
1422:         }
1423:         if (cat.systemIds == null) {
1424:             cat.systemIds = new Hashtable ();
1425:             if (unified)
1426:             cat.uris = cat.systemIds;
1427:         } else
1428:             value = (String) cat.systemIds.get (systemId);
1429:         if (value != null) {
1430:             if (!value.equals (uri))
1431:             warn ("ignoring <system...> entry for " + systemId);
1432:         } else
1433:             cat.systemIds.put (systemId, uri);
1434: 
1435:         } else if ("rewriteSystem" == local) {
1436:         String    value = null;
1437: 
1438:         if (systemIdStartString == null || rewritePrefix == null
1439:             || systemIdStartString.length () == 0
1440:             || rewritePrefix.length () == 0
1441:             ) {
1442:             error ("expecting <rewriteSystem "
1443:             + "systemIdStartString=... rewritePrefix=.../>");
1444:             return;
1445:         }
1446:         if (cat.systemRewrites == null) {
1447:             cat.systemRewrites = new Hashtable ();
1448:             if (unified)
1449:             cat.uriRewrites = cat.systemRewrites;
1450:         } else
1451:             value = (String) cat.systemRewrites.get (
1452:                             systemIdStartString);
1453:         if (value != null) {
1454:             if (!value.equals (rewritePrefix))
1455:             warn ("ignoring <rewriteSystem...> entry for "
1456:                 + systemIdStartString);
1457:         } else
1458:             cat.systemRewrites.put (systemIdStartString,
1459:                     rewritePrefix);
1460: 
1461:         } else if ("delegateSystem" == local) {
1462:         Object    value = null;
1463: 
1464:         if (systemIdStartString == null || catalog == null) {
1465:             error ("expecting <delegateSystem "
1466:             + "systemIdStartString=... catalog=.../>");
1467:             return;
1468:         }
1469:         if (cat.systemDelegations == null) {
1470:             cat.systemDelegations = new Hashtable ();
1471:             if (unified)
1472:             cat.uriDelegations = cat.systemDelegations;
1473:         } else
1474:             value = cat.systemDelegations.get (systemIdStartString);
1475:         if (value != null) {
1476:             if (!value.equals (catalog))
1477:             warn ("ignoring <delegateSystem...> entry for "
1478:                 + uriStartString);
1479:         } else
1480:             cat.systemDelegations.put (systemIdStartString, catalog);
1481: 
1482: 
1483:         //
1484:         // URI:  just like "system" ID support, except that
1485:         // fragment IDs are disallowed in "system" elements.
1486:         //
1487:         } else if ("uri" == local) {
1488:         String    name = atts.getValue ("name");
1489:         String    value = null;
1490: 
1491:         if (name == null || uri == null) {
1492:             error ("expecting <uri name=... uri=.../>");
1493:             return;
1494:         }
1495:         if (name.startsWith ("urn:publicid:")) {
1496:             error ("name is really a publicId!!");
1497:             return;
1498:         }
1499:         name = normalizeURI (name);
1500:         if (cat.uris == null) {
1501:             cat.uris = new Hashtable ();
1502:             if (unified)
1503:             cat.systemIds = cat.uris;
1504:         } else
1505:             value = (String) cat.uris.get (name);
1506:         if (value != null) {
1507:             if (!value.equals (uri))
1508:             warn ("ignoring <uri...> entry for " + name);
1509:         } else
1510:             cat.uris.put (name, uri);
1511: 
1512:         } else if ("rewriteURI" == local) {
1513:         String value = null;
1514: 
1515:         if (uriStartString == null || rewritePrefix == null
1516:             || uriStartString.length () == 0
1517:             || rewritePrefix.length () == 0
1518:             ) {
1519:             error ("expecting <rewriteURI "
1520:             + "uriStartString=... rewritePrefix=.../>");
1521:             return;
1522:         }
1523:         if (cat.uriRewrites == null) {
1524:             cat.uriRewrites = new Hashtable ();
1525:             if (unified)
1526:             cat.systemRewrites = cat.uriRewrites;
1527:         } else
1528:             value = (String) cat.uriRewrites.get (uriStartString);
1529:         if (value != null) {
1530:             if (!value.equals (rewritePrefix))
1531:             warn ("ignoring <rewriteURI...> entry for "
1532:                 + uriStartString);
1533:         } else
1534:             cat.uriRewrites.put (uriStartString, rewritePrefix);
1535: 
1536:         } else if ("delegateURI" == local) {
1537:         Object    value = null;
1538: 
1539:         if (uriStartString == null || catalog == null) {
1540:             error ("expecting <delegateURI "
1541:             + "uriStartString=... catalog=.../>");
1542:             return;
1543:         }
1544:         if (cat.uriDelegations == null) {
1545:             cat.uriDelegations = new Hashtable ();
1546:             if (unified)
1547:             cat.systemDelegations = cat.uriDelegations;
1548:         } else
1549:             value = cat.uriDelegations.get (uriStartString);
1550:         if (value != null) {
1551:             if (!value.equals (catalog))
1552:             warn ("ignoring <delegateURI...> entry for "
1553:                 + uriStartString);
1554:         } else
1555:             cat.uriDelegations.put (uriStartString, catalog);
1556: 
1557:         //
1558:         // NON-DELEGATING approach to modularity
1559:         //
1560:         } else if ("nextCatalog" == local) {
1561:         if (catalog == null) {
1562:             error ("expecting <nextCatalog catalog=.../>");
1563:             return;
1564:         }
1565:         if (cat.next == null)
1566:             cat.next = new Vector ();
1567:         cat.next.addElement (catalog);
1568: 
1569:         //
1570:         // EXTENSIONS from appendix E
1571:         //
1572:         } else if ("doctype" == local) {
1573:         String    name = atts.getValue ("name");
1574:         String    value = null;
1575: 
1576:         if (name == null || uri == null) {
1577:             error ("expecting <doctype name=... uri=.../>");
1578:             return;
1579:         }
1580:         name = normalizeURI (name);
1581:         if (cat.doctypes == null)
1582:             cat.doctypes = new Hashtable ();
1583:         else
1584:             value = (String) cat.doctypes.get (name);
1585:         if (value != null) {
1586:             if (!value.equals (uri))
1587:             warn ("ignoring <doctype...> entry for "
1588:                 + uriStartString);
1589:         } else
1590:             cat.doctypes.put (name, uri);
1591:         
1592: 
1593:         //
1594:         // RESERVED ... ignore (like reserved attributes) but warn
1595:         //
1596:         } else {
1597:         warn ("ignoring unknown catalog element: " + local);
1598:         ignoreDepth++;
1599:         }
1600:     }
1601: 
1602:     public void endElement (String uri, String local, String qName)
1603:     throws SAXException
1604:     {
1605:         if (ignoreDepth != 0)
1606:         ignoreDepth--;
1607:         else
1608:         bases.pop ();
1609:     }
1610:     }
1611: }