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