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: }