0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: *
0017: */
0018:
0019: package org.apache.tools.ant.types;
0020:
0021: import java.lang.reflect.Method;
0022:
0023: import java.io.File;
0024: import java.io.FileInputStream;
0025: import java.io.IOException;
0026: import java.io.InputStream;
0027: import java.net.MalformedURLException;
0028: import java.net.URL;
0029: import java.util.Enumeration;
0030: import java.util.Vector;
0031: import javax.xml.parsers.ParserConfigurationException;
0032: import javax.xml.parsers.SAXParserFactory;
0033: import javax.xml.transform.Source;
0034: import javax.xml.transform.TransformerException;
0035: import javax.xml.transform.URIResolver;
0036: import javax.xml.transform.sax.SAXSource;
0037: import org.apache.tools.ant.AntClassLoader;
0038: import org.apache.tools.ant.BuildException;
0039: import org.apache.tools.ant.Project;
0040: import org.apache.tools.ant.util.FileUtils;
0041: import org.apache.tools.ant.util.JAXPUtils;
0042: import org.xml.sax.EntityResolver;
0043: import org.xml.sax.InputSource;
0044: import org.xml.sax.SAXException;
0045: import org.xml.sax.XMLReader;
0046:
0047: /**
0048: * <p>This data type provides a catalog of resource locations (such as
0049: * DTDs and XML entities), based on the <a
0050: * href="http://oasis-open.org/committees/entity/spec-2001-08-06.html">
0051: * OASIS "Open Catalog" standard</a>. The catalog entries are used
0052: * both for Entity resolution and URI resolution, in accordance with
0053: * the {@link org.xml.sax.EntityResolver EntityResolver} and {@link
0054: * javax.xml.transform.URIResolver URIResolver} interfaces as defined
0055: * in the <a href="http://java.sun.com/xml/jaxp">Java API for XML
0056: * Processing Specification</a>.</p>
0057: *
0058: * <p>Resource locations can be specified either in-line or in
0059: * external catalog file(s), or both. In order to use an external
0060: * catalog file, the xml-commons resolver library ("resolver.jar")
0061: * must be in your classpath. External catalog files may be either <a
0062: * href="http://oasis-open.org/committees/entity/background/9401.html">
0063: * plain text format</a> or <a
0064: * href="http://www.oasis-open.org/committees/entity/spec-2001-08-06.html">
0065: * XML format</a>. If the xml-commons resolver library is not found
0066: * in the classpath, external catalog files, specified in
0067: * <code><catalogpath></code> paths, will be ignored and a warning will
0068: * be logged. In this case, however, processing of inline entries will proceed
0069: * normally.</p>
0070: *
0071: * <p>Currently, only <code><dtd></code> and
0072: * <code><entity></code> elements may be specified inline; these
0073: * correspond to OASIS catalog entry types <code>PUBLIC</code> and
0074: * <code>URI</code> respectively.</p>
0075: *
0076: * <p>The following is a usage example:</p>
0077: *
0078: * <code>
0079: * <xmlcatalog><br>
0080: * <dtd publicId="" location="/path/to/file.jar" /><br>
0081: * <dtd publicId="" location="/path/to/file2.jar" /><br>
0082: * <entity publicId="" location="/path/to/file3.jar" /><br>
0083: * <entity publicId="" location="/path/to/file4.jar" /><br>
0084: * <catalogpath><br>
0085: * <pathelement location="/etc/sgml/catalog"/><br>
0086: * </catalogpath><br>
0087: * <catalogfiles dir="/opt/catalogs/" includes="**\catalog.xml" /><br>
0088: * </xmlcatalog><br>
0089: * </code>
0090: * <p>
0091: * Tasks wishing to use <code><xmlcatalog></code> must provide a method called
0092: * <code>createXMLCatalog</code> which returns an instance of
0093: * <code>XMLCatalog</code>. Nested DTD and entity definitions are handled by
0094: * the XMLCatalog object and must be labeled <code>dtd</code> and
0095: * <code>entity</code> respectively.</p>
0096: *
0097: * <p>The following is a description of the resolution algorithm:
0098: * entities/URIs/dtds are looked up in each of the following contexts,
0099: * stopping when a valid and readable resource is found:
0100: * <ol>
0101: * <li>In the local filesystem</li>
0102: * <li>In the classpath</li>
0103: * <li>Using the Apache xml-commons resolver (if it is available)</li>
0104: * <li>In URL-space</li>
0105: * </ol>
0106: * </p>
0107: *
0108: * <p>See {@link
0109: * org.apache.tools.ant.taskdefs.optional.XMLValidateTask
0110: * XMLValidateTask} for an example of a task that has integrated
0111: * support for XMLCatalogs.</p>
0112: *
0113: * <p>Possible future extension could provide for additional OASIS
0114: * entry types to be specified inline.</p>
0115: *
0116: */
0117: public class XMLCatalog extends DataType implements Cloneable,
0118: EntityResolver, URIResolver {
0119:
0120: /** helper for some File.toURL connversions */
0121: private static final FileUtils FILE_UTILS = FileUtils
0122: .getFileUtils();
0123:
0124: //-- Fields ----------------------------------------------------------------
0125:
0126: /** Holds dtd/entity objects until needed. */
0127: private Vector elements = new Vector();
0128:
0129: /**
0130: * Classpath in which to attempt to resolve resources.
0131: */
0132: private Path classpath;
0133:
0134: /**
0135: * Path listing external catalog files to search when resolving entities
0136: */
0137: private Path catalogPath;
0138:
0139: /**
0140: * The name of the bridge to the Apache xml-commons resolver
0141: * class, used to determine whether resolver.jar is present in the
0142: * classpath.
0143: */
0144: public static final String APACHE_RESOLVER = "org.apache.tools.ant.types.resolver.ApacheCatalogResolver";
0145:
0146: /**
0147: * Resolver base class
0148: */
0149: public static final String CATALOG_RESOLVER = "org.apache.xml.resolver.tools.CatalogResolver";
0150:
0151: //-- Methods ---------------------------------------------------------------
0152:
0153: /**
0154: * Default constructor
0155: */
0156: public XMLCatalog() {
0157: setChecked(false);
0158: }
0159:
0160: /**
0161: * Returns the elements of the catalog - ResourceLocation objects.
0162: *
0163: * @return the elements of the catalog - ResourceLocation objects
0164: */
0165: private Vector getElements() {
0166: return getRef().elements;
0167: }
0168:
0169: /**
0170: * Returns the classpath in which to attempt to resolve resources.
0171: *
0172: * @return the classpath
0173: */
0174: private Path getClasspath() {
0175: return getRef().classpath;
0176: }
0177:
0178: /**
0179: * Allows nested classpath elements. Not allowed if this catalog
0180: * is itself a reference to another catalog -- that is, a catalog
0181: * cannot both refer to another <em>and</em> contain elements or
0182: * other attributes.
0183: *
0184: * @return a Path instance to be configured.
0185: */
0186: public Path createClasspath() {
0187: if (isReference()) {
0188: throw noChildrenAllowed();
0189: }
0190: if (this .classpath == null) {
0191: this .classpath = new Path(getProject());
0192: }
0193: setChecked(false);
0194: return this .classpath.createPath();
0195: }
0196:
0197: /**
0198: * Allows simple classpath string. Not allowed if this catalog is
0199: * itself a reference to another catalog -- that is, a catalog
0200: * cannot both refer to another <em>and</em> contain elements or
0201: * other attributes.
0202: *
0203: * @param classpath the classpath to use to look up entities.
0204: */
0205: public void setClasspath(Path classpath) {
0206: if (isReference()) {
0207: throw tooManyAttributes();
0208: }
0209: if (this .classpath == null) {
0210: this .classpath = classpath;
0211: } else {
0212: this .classpath.append(classpath);
0213: }
0214: setChecked(false);
0215: }
0216:
0217: /**
0218: * Allows classpath reference. Not allowed if this catalog is
0219: * itself a reference to another catalog -- that is, a catalog
0220: * cannot both refer to another <em>and</em> contain elements or
0221: * other attributes.
0222: *
0223: * @param r an Ant reference containing a classpath.
0224: */
0225: public void setClasspathRef(Reference r) {
0226: if (isReference()) {
0227: throw tooManyAttributes();
0228: }
0229: createClasspath().setRefid(r);
0230: setChecked(false);
0231: }
0232:
0233: /** Creates a nested <code><catalogpath></code> element.
0234: * Not allowed if this catalog is itself a reference to another
0235: * catalog -- that is, a catalog cannot both refer to another
0236: * <em>and</em> contain elements or other attributes.
0237: *
0238: * @return a path to be configured as the catalog path.
0239: * @exception BuildException
0240: * if this is a reference and no nested elements are allowed.
0241: */
0242: public Path createCatalogPath() {
0243: if (isReference()) {
0244: throw noChildrenAllowed();
0245: }
0246: if (this .catalogPath == null) {
0247: this .catalogPath = new Path(getProject());
0248: }
0249: setChecked(false);
0250: return this .catalogPath.createPath();
0251: }
0252:
0253: /**
0254: * Allows catalogpath reference. Not allowed if this catalog is
0255: * itself a reference to another catalog -- that is, a catalog
0256: * cannot both refer to another <em>and</em> contain elements or
0257: * other attributes.
0258: *
0259: * @param r an Ant reference containing a classpath to be used as
0260: * the catalog path.
0261: */
0262: public void setCatalogPathRef(Reference r) {
0263: if (isReference()) {
0264: throw tooManyAttributes();
0265: }
0266: createCatalogPath().setRefid(r);
0267: setChecked(false);
0268: }
0269:
0270: /**
0271: * Returns the catalog path in which to attempt to resolve DTDs.
0272: *
0273: * @return the catalog path
0274: */
0275: public Path getCatalogPath() {
0276: return getRef().catalogPath;
0277: }
0278:
0279: /**
0280: * Creates the nested <code><dtd></code> element. Not
0281: * allowed if this catalog is itself a reference to another
0282: * catalog -- that is, a catalog cannot both refer to another
0283: * <em>and</em> contain elements or other attributes.
0284: *
0285: * @param dtd the information about the PUBLIC resource mapping to
0286: * be added to the catalog
0287: * @exception BuildException if this is a reference and no nested
0288: * elements are allowed.
0289: */
0290: public void addDTD(ResourceLocation dtd) throws BuildException {
0291: if (isReference()) {
0292: throw noChildrenAllowed();
0293: }
0294:
0295: getElements().addElement(dtd);
0296: setChecked(false);
0297: }
0298:
0299: /**
0300: * Creates the nested <code><entity></code> element. Not
0301: * allowed if this catalog is itself a reference to another
0302: * catalog -- that is, a catalog cannot both refer to another
0303: * <em>and</em> contain elements or other attributes.
0304: *
0305: * @param entity the information about the URI resource mapping to be
0306: * added to the catalog.
0307: * @exception BuildException if this is a reference and no nested
0308: * elements are allowed.
0309: */
0310: public void addEntity(ResourceLocation entity)
0311: throws BuildException {
0312: addDTD(entity);
0313: }
0314:
0315: /**
0316: * Loads a nested <code><xmlcatalog></code> into our
0317: * definition. Not allowed if this catalog is itself a reference
0318: * to another catalog -- that is, a catalog cannot both refer to
0319: * another <em>and</em> contain elements or other attributes.
0320: *
0321: * @param catalog Nested XMLCatalog
0322: */
0323: public void addConfiguredXMLCatalog(XMLCatalog catalog) {
0324: if (isReference()) {
0325: throw noChildrenAllowed();
0326: }
0327:
0328: // Add all nested elements to our catalog
0329: Vector newElements = catalog.getElements();
0330: Vector ourElements = getElements();
0331: Enumeration e = newElements.elements();
0332: while (e.hasMoreElements()) {
0333: ourElements.addElement(e.nextElement());
0334: }
0335:
0336: // Append the classpath of the nested catalog
0337: Path nestedClasspath = catalog.getClasspath();
0338: createClasspath().append(nestedClasspath);
0339:
0340: // Append the catalog path of the nested catalog
0341: Path nestedCatalogPath = catalog.getCatalogPath();
0342: createCatalogPath().append(nestedCatalogPath);
0343: setChecked(false);
0344: }
0345:
0346: /**
0347: * Makes this instance in effect a reference to another XMLCatalog
0348: * instance.
0349: *
0350: * <p>You must not set another attribute or nest elements inside
0351: * this element if you make it a reference. That is, a catalog
0352: * cannot both refer to another <em>and</em> contain elements or
0353: * attributes.</p>
0354: *
0355: * @param r the reference to which this catalog instance is associated
0356: * @exception BuildException if this instance already has been configured.
0357: */
0358: public void setRefid(Reference r) throws BuildException {
0359: if (!elements.isEmpty()) {
0360: throw tooManyAttributes();
0361: }
0362: super .setRefid(r);
0363: }
0364:
0365: /**
0366: * Implements the EntityResolver.resolveEntity() interface method.
0367: * @param publicId the public id to resolve.
0368: * @param systemId the system id to resolve.
0369: * @throws SAXException if there is a parsing problem.
0370: * @throws IOException if there is an IO problem.
0371: * @return the resolved entity.
0372: * @see org.xml.sax.EntityResolver#resolveEntity
0373: */
0374: public InputSource resolveEntity(String publicId, String systemId)
0375: throws SAXException, IOException {
0376:
0377: if (isReference()) {
0378: return getRef().resolveEntity(publicId, systemId);
0379: }
0380:
0381: dieOnCircularReference();
0382:
0383: log("resolveEntity: '" + publicId + "': '" + systemId + "'",
0384: Project.MSG_DEBUG);
0385:
0386: InputSource inputSource = getCatalogResolver().resolveEntity(
0387: publicId, systemId);
0388:
0389: if (inputSource == null) {
0390: log("No matching catalog entry found, parser will use: '"
0391: + systemId + "'", Project.MSG_DEBUG);
0392: }
0393:
0394: return inputSource;
0395: }
0396:
0397: /**
0398: * Implements the URIResolver.resolve() interface method.
0399: * @param href an href attribute.
0400: * @param base the base URI.
0401: * @return a Source object, or null if href cannot be resolved.
0402: * @throws TransformerException if an error occurs.
0403: * @see javax.xml.transform.URIResolver#resolve
0404: */
0405: public Source resolve(String href, String base)
0406: throws TransformerException {
0407:
0408: if (isReference()) {
0409: return getRef().resolve(href, base);
0410: }
0411:
0412: dieOnCircularReference();
0413:
0414: SAXSource source = null;
0415:
0416: String uri = removeFragment(href);
0417:
0418: log("resolve: '" + uri + "' with base: '" + base + "'",
0419: Project.MSG_DEBUG);
0420:
0421: source = (SAXSource) getCatalogResolver().resolve(uri, base);
0422:
0423: if (source == null) {
0424: log("No matching catalog entry found, parser will use: '"
0425: + href + "'", Project.MSG_DEBUG);
0426: //
0427: // Cannot return a null source, because we have to call
0428: // setEntityResolver (see setEntityResolver javadoc comment)
0429: //
0430: source = new SAXSource();
0431: URL baseURL = null;
0432: try {
0433: if (base == null) {
0434: baseURL = FILE_UTILS.getFileURL(getProject()
0435: .getBaseDir());
0436: } else {
0437: baseURL = new URL(base);
0438: }
0439: URL url = (uri.length() == 0 ? baseURL : new URL(
0440: baseURL, uri));
0441: source.setInputSource(new InputSource(url.toString()));
0442: } catch (MalformedURLException ex) {
0443: // At this point we are probably in failure mode, but
0444: // try to use the bare URI as a last gasp
0445: source.setInputSource(new InputSource(uri));
0446: }
0447: }
0448:
0449: setEntityResolver(source);
0450: return source;
0451: }
0452:
0453: /**
0454: * @since Ant 1.6
0455: */
0456: private XMLCatalog getRef() {
0457: if (!isReference()) {
0458: return this ;
0459: }
0460: return (XMLCatalog) getCheckedRef(XMLCatalog.class,
0461: "xmlcatalog");
0462: }
0463:
0464: /**
0465: * The instance of the CatalogResolver strategy to use.
0466: */
0467: private CatalogResolver catalogResolver = null;
0468:
0469: /**
0470: * Factory method for creating the appropriate CatalogResolver
0471: * strategy implementation.
0472: * <p> Until we query the classpath, we don't know whether the Apache
0473: * resolver (Norm Walsh's library from xml-commons) is available or not.
0474: * This method determines whether the library is available and creates the
0475: * appropriate implementation of CatalogResolver based on the answer.</p>
0476: * <p>This is an application of the Gang of Four Strategy Pattern
0477: * combined with Template Method.</p>
0478: */
0479: private CatalogResolver getCatalogResolver() {
0480:
0481: if (catalogResolver == null) {
0482:
0483: AntClassLoader loader = null;
0484:
0485: loader = getProject().createClassLoader(
0486: Path.systemClasspath);
0487:
0488: try {
0489: Class clazz = Class.forName(APACHE_RESOLVER, true,
0490: loader);
0491:
0492: // The Apache resolver is present - Need to check if it can
0493: // be seen by the catalog resolver class. Start by getting
0494: // the actual loader
0495: ClassLoader apacheResolverLoader = clazz
0496: .getClassLoader();
0497:
0498: // load the base class through this loader.
0499: Class baseResolverClass = Class.forName(
0500: CATALOG_RESOLVER, true, apacheResolverLoader);
0501:
0502: // and find its actual loader
0503: ClassLoader baseResolverLoader = baseResolverClass
0504: .getClassLoader();
0505:
0506: // We have the loader which is being used to load the
0507: // CatalogResolver. Can it see the ApacheResolver? The
0508: // base resolver will only be able to create the ApacheResolver
0509: // if it can see it - doesn't use the context loader.
0510: clazz = Class.forName(APACHE_RESOLVER, true,
0511: baseResolverLoader);
0512:
0513: Object obj = clazz.newInstance();
0514: //
0515: // Success! The xml-commons resolver library is
0516: // available, so use it.
0517: //
0518: catalogResolver = new ExternalResolver(clazz, obj);
0519: } catch (Throwable ex) {
0520: //
0521: // The xml-commons resolver library is not
0522: // available, so we can't use it.
0523: //
0524: catalogResolver = new InternalResolver();
0525: if (getCatalogPath() != null
0526: && getCatalogPath().list().length != 0) {
0527: log(
0528: "Warning: XML resolver not found; external catalogs"
0529: + " will be ignored",
0530: Project.MSG_WARN);
0531: }
0532: log("Failed to load Apache resolver: " + ex,
0533: Project.MSG_DEBUG);
0534: }
0535: }
0536: return catalogResolver;
0537: }
0538:
0539: /**
0540: * <p>This is called from the URIResolver to set an EntityResolver
0541: * on the SAX parser to be used for new XML documents that are
0542: * encountered as a result of the document() function, xsl:import,
0543: * or xsl:include. This is done because the XSLT processor calls
0544: * out to the SAXParserFactory itself to create a new SAXParser to
0545: * parse the new document. The new parser does not automatically
0546: * inherit the EntityResolver of the original (although arguably
0547: * it should). See below:</p>
0548: *
0549: * <tt>"If an application wants to set the ErrorHandler or
0550: * EntityResolver for an XMLReader used during a transformation,
0551: * it should use a URIResolver to return the SAXSource which
0552: * provides (with getXMLReader) a reference to the XMLReader"</tt>
0553: *
0554: * <p>...quoted from page 118 of the Java API for XML
0555: * Processing 1.1 specification</p>
0556: *
0557: */
0558: private void setEntityResolver(SAXSource source)
0559: throws TransformerException {
0560:
0561: XMLReader reader = source.getXMLReader();
0562: if (reader == null) {
0563: SAXParserFactory spFactory = SAXParserFactory.newInstance();
0564: spFactory.setNamespaceAware(true);
0565: try {
0566: reader = spFactory.newSAXParser().getXMLReader();
0567: } catch (ParserConfigurationException ex) {
0568: throw new TransformerException(ex);
0569: } catch (SAXException ex) {
0570: throw new TransformerException(ex);
0571: }
0572: }
0573: reader.setEntityResolver(this );
0574: source.setXMLReader(reader);
0575: }
0576:
0577: /**
0578: * Find a ResourceLocation instance for the given publicId.
0579: *
0580: * @param publicId the publicId of the Resource for which local information
0581: * is required.
0582: * @return a ResourceLocation instance with information on the local location
0583: * of the Resource or null if no such information is available.
0584: */
0585: private ResourceLocation findMatchingEntry(String publicId) {
0586: Enumeration e = getElements().elements();
0587: ResourceLocation element = null;
0588: while (e.hasMoreElements()) {
0589: Object o = e.nextElement();
0590: if (o instanceof ResourceLocation) {
0591: element = (ResourceLocation) o;
0592: if (element.getPublicId().equals(publicId)) {
0593: return element;
0594: }
0595: }
0596: }
0597: return null;
0598: }
0599:
0600: /**
0601: * Utility method to remove trailing fragment from a URI.
0602: * For example,
0603: * <code>http://java.sun.com/index.html#chapter1</code>
0604: * would return <code>http://java.sun.com/index.html</code>.
0605: *
0606: * @param uri The URI to process. It may or may not contain a
0607: * fragment.
0608: * @return The URI sans fragment.
0609: */
0610: private String removeFragment(String uri) {
0611: String result = uri;
0612: int hashPos = uri.indexOf("#");
0613: if (hashPos >= 0) {
0614: result = uri.substring(0, hashPos);
0615: }
0616: return result;
0617: }
0618:
0619: /**
0620: * Utility method to lookup a ResourceLocation in the filesystem.
0621: *
0622: * @return An InputSource for reading the file, or <code>null</code>
0623: * if the file does not exist or is not readable.
0624: */
0625: private InputSource filesystemLookup(ResourceLocation matchingEntry) {
0626:
0627: String uri = matchingEntry.getLocation();
0628: // the following line seems to be necessary on Windows under JDK 1.2
0629: uri = uri.replace(File.separatorChar, '/');
0630: URL baseURL = null;
0631:
0632: //
0633: // The ResourceLocation may specify a relative path for its
0634: // location attribute. This is resolved using the appropriate
0635: // base.
0636: //
0637: if (matchingEntry.getBase() != null) {
0638: baseURL = matchingEntry.getBase();
0639: } else {
0640: try {
0641: baseURL = FILE_UTILS.getFileURL(getProject()
0642: .getBaseDir());
0643: } catch (MalformedURLException ex) {
0644: throw new BuildException(
0645: "Project basedir cannot be converted to a URL");
0646: }
0647: }
0648:
0649: InputSource source = null;
0650: URL url = null;
0651: try {
0652: url = new URL(baseURL, uri);
0653: } catch (MalformedURLException ex) {
0654: // this processing is useful under Windows when the location of the DTD
0655: // has been given as an absolute path
0656: // see Bugzilla Report 23913
0657: File testFile = new File(uri);
0658: if (testFile.exists() && testFile.canRead()) {
0659: log("uri : '" + uri + "' matches a readable file",
0660: Project.MSG_DEBUG);
0661: try {
0662: url = FILE_UTILS.getFileURL(testFile);
0663: } catch (MalformedURLException ex1) {
0664: throw new BuildException(
0665: "could not find an URL for :"
0666: + testFile.getAbsolutePath());
0667: }
0668: } else {
0669: log("uri : '" + uri
0670: + "' does not match a readable file",
0671: Project.MSG_DEBUG);
0672:
0673: }
0674: }
0675:
0676: if (url != null && url.getProtocol().equals("file")) {
0677: String fileName = FILE_UTILS.fromURI(url.toString());
0678: if (fileName != null) {
0679: log("fileName " + fileName, Project.MSG_DEBUG);
0680: File resFile = new File(fileName);
0681: if (resFile.exists() && resFile.canRead()) {
0682: try {
0683: source = new InputSource(new FileInputStream(
0684: resFile));
0685: String sysid = JAXPUtils.getSystemId(resFile);
0686: source.setSystemId(sysid);
0687: log("catalog entry matched a readable file: '"
0688: + sysid + "'", Project.MSG_DEBUG);
0689: } catch (IOException ex) {
0690: // ignore
0691: }
0692: }
0693: }
0694: }
0695: return source;
0696: }
0697:
0698: /**
0699: * Utility method to lookup a ResourceLocation in the classpath.
0700: *
0701: * @return An InputSource for reading the resource, or <code>null</code>
0702: * if the resource does not exist in the classpath or is not readable.
0703: */
0704: private InputSource classpathLookup(ResourceLocation matchingEntry) {
0705:
0706: InputSource source = null;
0707:
0708: AntClassLoader loader = null;
0709: Path cp = classpath;
0710: if (cp != null) {
0711: cp = classpath.concatSystemClasspath("ignore");
0712: } else {
0713: cp = (new Path(getProject())).concatSystemClasspath("last");
0714: }
0715: loader = getProject().createClassLoader(cp);
0716:
0717: //
0718: // for classpath lookup we ignore the base directory
0719: //
0720: InputStream is = loader.getResourceAsStream(matchingEntry
0721: .getLocation());
0722:
0723: if (is != null) {
0724: source = new InputSource(is);
0725: URL entryURL = loader.getResource(matchingEntry
0726: .getLocation());
0727: String sysid = entryURL.toExternalForm();
0728: source.setSystemId(sysid);
0729: log("catalog entry matched a resource in the classpath: '"
0730: + sysid + "'", Project.MSG_DEBUG);
0731: }
0732:
0733: return source;
0734: }
0735:
0736: /**
0737: * Utility method to lookup a ResourceLocation in URL-space.
0738: *
0739: * @return An InputSource for reading the resource, or <code>null</code>
0740: * if the resource does not identify a valid URL or is not readable.
0741: */
0742: private InputSource urlLookup(ResourceLocation matchingEntry) {
0743:
0744: String uri = matchingEntry.getLocation();
0745: URL baseURL = null;
0746:
0747: //
0748: // The ResourceLocation may specify a relative url for its
0749: // location attribute. This is resolved using the appropriate
0750: // base.
0751: //
0752: if (matchingEntry.getBase() != null) {
0753: baseURL = matchingEntry.getBase();
0754: } else {
0755: try {
0756: baseURL = FILE_UTILS.getFileURL(getProject()
0757: .getBaseDir());
0758: } catch (MalformedURLException ex) {
0759: throw new BuildException(
0760: "Project basedir cannot be converted to a URL");
0761: }
0762: }
0763:
0764: InputSource source = null;
0765: URL url = null;
0766:
0767: try {
0768: url = new URL(baseURL, uri);
0769: } catch (MalformedURLException ex) {
0770: // ignore
0771: }
0772:
0773: if (url != null) {
0774: try {
0775: InputStream is = url.openStream();
0776: if (is != null) {
0777: source = new InputSource(is);
0778: String sysid = url.toExternalForm();
0779: source.setSystemId(sysid);
0780: log("catalog entry matched as a URL: '" + sysid
0781: + "'", Project.MSG_DEBUG);
0782: }
0783: } catch (IOException ex) {
0784: // ignore
0785: }
0786: }
0787:
0788: return source;
0789:
0790: }
0791:
0792: /**
0793: * Interface implemented by both the InternalResolver strategy and
0794: * the ExternalResolver strategy.
0795: */
0796: private interface CatalogResolver extends URIResolver,
0797: EntityResolver {
0798:
0799: InputSource resolveEntity(String publicId, String systemId);
0800:
0801: Source resolve(String href, String base)
0802: throws TransformerException;
0803: }
0804:
0805: /**
0806: * The InternalResolver strategy is used if the Apache resolver
0807: * library (Norm Walsh's library from xml-commons) is not
0808: * available. In this case, external catalog files will be
0809: * ignored.
0810: *
0811: */
0812: private class InternalResolver implements CatalogResolver {
0813:
0814: public InternalResolver() {
0815: log(
0816: "Apache resolver library not found, internal resolver will be used",
0817: Project.MSG_VERBOSE);
0818: }
0819:
0820: public InputSource resolveEntity(String publicId,
0821: String systemId) {
0822: InputSource result = null;
0823: ResourceLocation matchingEntry = findMatchingEntry(publicId);
0824:
0825: if (matchingEntry != null) {
0826:
0827: log("Matching catalog entry found for publicId: '"
0828: + matchingEntry.getPublicId() + "' location: '"
0829: + matchingEntry.getLocation() + "'",
0830: Project.MSG_DEBUG);
0831:
0832: result = filesystemLookup(matchingEntry);
0833:
0834: if (result == null) {
0835: result = classpathLookup(matchingEntry);
0836: }
0837:
0838: if (result == null) {
0839: result = urlLookup(matchingEntry);
0840: }
0841: }
0842: return result;
0843: }
0844:
0845: public Source resolve(String href, String base)
0846: throws TransformerException {
0847:
0848: SAXSource result = null;
0849: InputSource source = null;
0850:
0851: ResourceLocation matchingEntry = findMatchingEntry(href);
0852:
0853: if (matchingEntry != null) {
0854:
0855: log("Matching catalog entry found for uri: '"
0856: + matchingEntry.getPublicId() + "' location: '"
0857: + matchingEntry.getLocation() + "'",
0858: Project.MSG_DEBUG);
0859:
0860: //
0861: // Use the passed in base in preference to the base
0862: // from matchingEntry, which is either null or the
0863: // directory in which the external catalog file from
0864: // which it was obtained is located. We make a copy
0865: // so matchingEntry's original base is untouched.
0866: //
0867: // This is the standard behavior as per my reading of
0868: // the JAXP and XML Catalog specs. CKS 11/7/2002
0869: //
0870: ResourceLocation entryCopy = matchingEntry;
0871: if (base != null) {
0872: try {
0873: URL baseURL = new URL(base);
0874: entryCopy = new ResourceLocation();
0875: entryCopy.setBase(baseURL);
0876: } catch (MalformedURLException ex) {
0877: // ignore
0878: }
0879: }
0880: entryCopy.setPublicId(matchingEntry.getPublicId());
0881: entryCopy.setLocation(matchingEntry.getLocation());
0882:
0883: source = filesystemLookup(entryCopy);
0884:
0885: if (source == null) {
0886: source = classpathLookup(entryCopy);
0887: }
0888:
0889: if (source == null) {
0890: source = urlLookup(entryCopy);
0891: }
0892:
0893: if (source != null) {
0894: result = new SAXSource(source);
0895: }
0896: }
0897: return result;
0898: }
0899: }
0900:
0901: /**
0902: * The ExternalResolver strategy is used if the Apache resolver
0903: * library (Norm Walsh's library from xml-commons) is available in
0904: * the classpath. The ExternalResolver is a essentially a superset
0905: * of the InternalResolver.
0906: *
0907: */
0908: private class ExternalResolver implements CatalogResolver {
0909:
0910: private Method setXMLCatalog = null;
0911: private Method parseCatalog = null;
0912: private Method resolveEntity = null;
0913: private Method resolve = null;
0914:
0915: /** The instance of the ApacheCatalogResolver bridge class */
0916: private Object resolverImpl = null;
0917:
0918: private boolean externalCatalogsProcessed = false;
0919:
0920: public ExternalResolver(Class resolverImplClass,
0921: Object resolverImpl) {
0922:
0923: this .resolverImpl = resolverImpl;
0924:
0925: //
0926: // Get Method instances for each of the methods we need to
0927: // call on the resolverImpl using reflection. We can't
0928: // call them directly, because they require on the
0929: // xml-commons resolver library which may not be available
0930: // in the classpath.
0931: //
0932: try {
0933: setXMLCatalog = resolverImplClass.getMethod(
0934: "setXMLCatalog",
0935: new Class[] { XMLCatalog.class });
0936:
0937: parseCatalog = resolverImplClass.getMethod(
0938: "parseCatalog", new Class[] { String.class });
0939:
0940: resolveEntity = resolverImplClass.getMethod(
0941: "resolveEntity", new Class[] { String.class,
0942: String.class });
0943:
0944: resolve = resolverImplClass.getMethod("resolve",
0945: new Class[] { String.class, String.class });
0946: } catch (NoSuchMethodException ex) {
0947: throw new BuildException(ex);
0948: }
0949:
0950: log(
0951: "Apache resolver library found, xml-commons resolver will be used",
0952: Project.MSG_VERBOSE);
0953: }
0954:
0955: public InputSource resolveEntity(String publicId,
0956: String systemId) {
0957: InputSource result = null;
0958:
0959: processExternalCatalogs();
0960:
0961: ResourceLocation matchingEntry = findMatchingEntry(publicId);
0962:
0963: if (matchingEntry != null) {
0964:
0965: log("Matching catalog entry found for publicId: '"
0966: + matchingEntry.getPublicId() + "' location: '"
0967: + matchingEntry.getLocation() + "'",
0968: Project.MSG_DEBUG);
0969:
0970: result = filesystemLookup(matchingEntry);
0971:
0972: if (result == null) {
0973: result = classpathLookup(matchingEntry);
0974: }
0975:
0976: if (result == null) {
0977: try {
0978: result = (InputSource) resolveEntity.invoke(
0979: resolverImpl, new Object[] { publicId,
0980: systemId });
0981: } catch (Exception ex) {
0982: throw new BuildException(ex);
0983: }
0984: }
0985: } else {
0986: //
0987: // We didn't match a ResourceLocation, but since we
0988: // only support PUBLIC and URI entry types internally,
0989: // it is still possible that there is another entry in
0990: // an external catalog that will match. We call
0991: // Apache resolver's resolveEntity method to cover
0992: // this possibility.
0993: //
0994: try {
0995: result = (InputSource) resolveEntity.invoke(
0996: resolverImpl, new Object[] { publicId,
0997: systemId });
0998: } catch (Exception ex) {
0999: throw new BuildException(ex);
1000: }
1001: }
1002:
1003: return result;
1004: }
1005:
1006: public Source resolve(String href, String base)
1007: throws TransformerException {
1008:
1009: SAXSource result = null;
1010: InputSource source = null;
1011:
1012: processExternalCatalogs();
1013:
1014: ResourceLocation matchingEntry = findMatchingEntry(href);
1015:
1016: if (matchingEntry != null) {
1017:
1018: log("Matching catalog entry found for uri: '"
1019: + matchingEntry.getPublicId() + "' location: '"
1020: + matchingEntry.getLocation() + "'",
1021: Project.MSG_DEBUG);
1022:
1023: //
1024: // Use the passed in base in preference to the base
1025: // from matchingEntry, which is either null or the
1026: // directory in which the external catalog file from
1027: // which it was obtained is located. We make a copy
1028: // so matchingEntry's original base is untouched. Of
1029: // course, if there is no base, no need to make a
1030: // copy...
1031: //
1032: // This is the standard behavior as per my reading of
1033: // the JAXP and XML Catalog specs. CKS 11/7/2002
1034: //
1035: ResourceLocation entryCopy = matchingEntry;
1036: if (base != null) {
1037: try {
1038: URL baseURL = new URL(base);
1039: entryCopy = new ResourceLocation();
1040: entryCopy.setBase(baseURL);
1041: } catch (MalformedURLException ex) {
1042: // ignore
1043: }
1044: }
1045: entryCopy.setPublicId(matchingEntry.getPublicId());
1046: entryCopy.setLocation(matchingEntry.getLocation());
1047:
1048: source = filesystemLookup(entryCopy);
1049:
1050: if (source == null) {
1051: source = classpathLookup(entryCopy);
1052: }
1053:
1054: if (source != null) {
1055: result = new SAXSource(source);
1056: } else {
1057: try {
1058: result = (SAXSource) resolve.invoke(
1059: resolverImpl,
1060: new Object[] { href, base });
1061: } catch (Exception ex) {
1062: throw new BuildException(ex);
1063: }
1064: }
1065: } else {
1066: //
1067: // We didn't match a ResourceLocation, but since we
1068: // only support PUBLIC and URI entry types internally,
1069: // it is still possible that there is another entry in
1070: // an external catalog that will match. We call
1071: // Apache resolver's resolveEntity method to cover
1072: // this possibility.
1073: //
1074: try {
1075: result = (SAXSource) resolve.invoke(resolverImpl,
1076: new Object[] { href, base });
1077: } catch (Exception ex) {
1078: throw new BuildException(ex);
1079: }
1080: }
1081: return result;
1082: }
1083:
1084: /**
1085: * Process each external catalog file specified in a
1086: * <code><catalogpath></code>. It will be
1087: * parsed by the resolver library, and the individual elements
1088: * will be added back to us (that is, the controlling
1089: * XMLCatalog instance) via a callback mechanism.
1090: */
1091: private void processExternalCatalogs() {
1092:
1093: if (!externalCatalogsProcessed) {
1094:
1095: try {
1096: setXMLCatalog.invoke(resolverImpl,
1097: new Object[] { XMLCatalog.this });
1098: } catch (Exception ex) {
1099: throw new BuildException(ex);
1100: }
1101:
1102: // Parse each catalog listed in nested <catalogpath> elements
1103: Path catPath = getCatalogPath();
1104: if (catPath != null) {
1105: log("Using catalogpath '" + getCatalogPath() + "'",
1106: Project.MSG_DEBUG);
1107: String[] catPathList = getCatalogPath().list();
1108:
1109: for (int i = 0; i < catPathList.length; i++) {
1110: File catFile = new File(catPathList[i]);
1111: log("Parsing " + catFile, Project.MSG_DEBUG);
1112: try {
1113: parseCatalog.invoke(resolverImpl,
1114: new Object[] { catFile.getPath() });
1115: } catch (Exception ex) {
1116: throw new BuildException(ex);
1117: }
1118: }
1119: }
1120: }
1121: externalCatalogsProcessed = true;
1122: }
1123: }
1124: } //-- XMLCatalog
|