1 /*
   2  * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.xml.catalog;
  26 
  27 import com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl;
  28 import java.io.IOException;
  29 import java.net.MalformedURLException;
  30 import java.net.URI;
  31 import java.net.URISyntaxException;
  32 import java.net.URL;
  33 import java.util.ArrayList;
  34 import java.util.Iterator;
  35 import java.util.List;
  36 import java.util.NoSuchElementException;
  37 import java.util.Spliterator;
  38 import java.util.Spliterators;
  39 import java.util.stream.Stream;
  40 import java.util.stream.StreamSupport;
  41 import static javax.xml.catalog.BaseEntry.CatalogEntryType;
  42 import static javax.xml.catalog.CatalogFeatures.DEFER_TRUE;
  43 import javax.xml.catalog.CatalogFeatures.Feature;
  44 import static javax.xml.catalog.CatalogMessages.formatMessage;
  45 import javax.xml.parsers.ParserConfigurationException;
  46 import javax.xml.parsers.SAXParser;
  47 import javax.xml.parsers.SAXParserFactory;
  48 import org.xml.sax.SAXException;
  49 
  50 /**
  51  * Implementation of the Catalog.
  52  *
  53  * @since 9
  54  */
  55 class CatalogImpl extends GroupEntry implements Catalog {
  56 
  57     //Catalog level, 0 means the top catalog
  58     int level = 0;
  59 
  60     //Value of the defer attribute to determine if alternative catalogs are read
  61     boolean isDeferred = true;
  62 
  63     //Value of the resolve attribute
  64     ResolveType resolveType = ResolveType.STRICT;
  65 
  66     //indicate whether the Catalog is empty
  67     boolean isEmpty;
  68 
  69     //Current parsed Catalog
  70     int current = 0;
  71 
  72     //System Id for this catalog
  73     String systemId;
  74 
  75     //The parent
  76     CatalogImpl parent = null;
  77 
  78     /*
  79      A list of catalog entry files from the input, excluding the current catalog.
  80      Paths in the List are normalized.
  81      */
  82     List<String> inputFiles;
  83 
  84     //A list of catalogs specified using the nextCatalog element
  85     List<NextCatalog> nextCatalogs;
  86 
  87     //reuse the parser
  88     SAXParser parser;
  89 
  90     /**
  91      * Construct a Catalog with specified path.
  92      *
  93      * @param file The path to a catalog file.
  94      * @throws CatalogException If an error happens while parsing the specified
  95      * catalog file.
  96      */
  97     public CatalogImpl(CatalogFeatures f, String... file) throws CatalogException {
  98         this(null, f, file);
  99     }
 100 
 101     /**
 102      * Construct a Catalog with specified path.
 103      *
 104      * @param parent The parent catalog
 105      * @param file The path to a catalog file.
 106      * @throws CatalogException If an error happens while parsing the specified
 107      * catalog file.
 108      */
 109     public CatalogImpl(CatalogImpl parent, CatalogFeatures f, String... file) throws CatalogException {
 110         super(CatalogEntryType.CATALOG);
 111         if (f == null) {
 112             throw new NullPointerException(
 113                     formatMessage(CatalogMessages.ERR_NULL_ARGUMENT, new Object[]{"CatalogFeatures"}));
 114         }
 115 
 116         if (file.length > 0) {
 117             CatalogMessages.reportNPEOnNull("The path to the catalog file", file[0]);
 118         }
 119 
 120         init(parent, f);
 121 
 122         //Path of catalog files
 123         String[] catalogFile = file;
 124         if (level == 0 && file.length == 0) {
 125             String files = features.get(Feature.FILES);
 126             if (files != null) {
 127                 catalogFile = files.split(";[ ]*");
 128             }
 129         }
 130 
 131         /*
 132          In accordance with 8. Resource Failures of the Catalog spec, missing
 133          Catalog entry files are to be ignored.
 134          */
 135         if ((catalogFile != null && catalogFile.length > 0)) {
 136             int start = 0;
 137             URI uri = null;
 138             for (String temp : catalogFile) {
 139                 uri = getSystemId(temp);
 140                 start++;
 141                 if (verifyCatalogFile(uri)) {
 142                     systemId = uri.toASCIIString();
 143                     try {
 144                         baseURI = new URL(systemId);
 145                     } catch (MalformedURLException e) {
 146                         CatalogMessages.reportRunTimeError(CatalogMessages.ERR_INVALID_PATH,
 147                                 new Object[]{temp}, e);
 148                     }
 149                     break;
 150                 }
 151             }
 152 
 153             //Save the rest of input files as alternative catalogs
 154             if (level == 0 && catalogFile.length > start) {
 155                 inputFiles = new ArrayList<>();
 156                 for (int i = start; i < catalogFile.length; i++) {
 157                     if (catalogFile[i] != null) {
 158                         inputFiles.add(catalogFile[i]);
 159                     }
 160                 }
 161             }
 162 
 163             if (systemId != null) {
 164                 parse(systemId);
 165             }
 166         }
 167     }
 168 
 169     private void init(CatalogImpl parent, CatalogFeatures f) {
 170         this.parent = parent;
 171         if (parent == null) {
 172             level = 0;
 173         } else {
 174             level = parent.level + 1;
 175         }
 176         if (f == null) {
 177             this.features = CatalogFeatures.defaults();
 178         } else {
 179             this.features = f;
 180         }
 181         setPrefer(features.get(Feature.PREFER));
 182         setDeferred(features.get(Feature.DEFER));
 183         setResolve(features.get(Feature.RESOLVE));
 184     }
 185 
 186     /**
 187      * Resets the Catalog instance to its initial state.
 188      */
 189     @Override
 190     public void reset() {
 191         super.reset();
 192         current = 0;
 193         if (level == 0) {
 194             catalogsSearched.clear();
 195         }
 196         entries.stream().filter((entry) -> (entry.type == CatalogEntryType.GROUP)).forEach((entry) -> {
 197             ((GroupEntry) entry).reset();
 198         });
 199 
 200         if (parent != null) {
 201             this.loadedCatalogs = parent.loadedCatalogs;
 202             this.catalogsSearched = parent.catalogsSearched;
 203         }
 204     }
 205 
 206     /**
 207      * Returns whether this Catalog instance is the top (main) Catalog.
 208      *
 209      * @return true if the instance is the top Catalog, false otherwise
 210      */
 211     boolean isTop() {
 212         return level == 0;
 213     }
 214 
 215     /**
 216      * Gets the parent of this catalog.
 217      *
 218      * @returns The parent catalog
 219      */
 220     public Catalog getParent() {
 221         return this.parent;
 222     }
 223 
 224     /**
 225      * Sets the defer property. If the value is null or empty, or any String
 226      * other than the defined, it will be assumed as the default value.
 227      *
 228      * @param value The value of the defer attribute
 229      */
 230     public final void setDeferred(String value) {
 231         isDeferred = DEFER_TRUE.equals(value);
 232     }
 233 
 234     /**
 235      * Queries the defer attribute
 236      *
 237      * @return true if the prefer attribute is set to system, false if not.
 238      */
 239     public boolean isDeferred() {
 240         return isDeferred;
 241     }
 242 
 243     /**
 244      * Sets the resolve property. If the value is null or empty, or any String
 245      * other than the defined, it will be assumed as the default value.
 246      *
 247      * @param value The value of the resolve attribute
 248      */
 249     public final void setResolve(String value) {
 250         resolveType = ResolveType.getType(value);
 251     }
 252 
 253     /**
 254      * Gets the value of the resolve attribute
 255      *
 256      * @return The value of the resolve attribute
 257      */
 258     public final ResolveType getResolve() {
 259         return resolveType;
 260     }
 261 
 262     /**
 263      * Marks the Catalog as being searched already.
 264      */
 265     void markAsSearched() {
 266         catalogsSearched.add(systemId);
 267     }
 268 
 269     /**
 270      * Parses the catalog.
 271      *
 272      * @param systemId The systemId of the catalog
 273      * @throws CatalogException if parsing the catalog failed
 274      */
 275     private void parse(String systemId) {
 276         if (parser == null) {
 277             parser = getParser();
 278         }
 279 
 280         try {
 281             CatalogReader reader = new CatalogReader(this, parser);
 282             parser.parse(systemId, reader);
 283         } catch (SAXException | IOException ex) {
 284             CatalogMessages.reportRunTimeError(CatalogMessages.ERR_PARSING_FAILED, ex);
 285         }
 286     }
 287 
 288     /**
 289      * Resolves the specified file path to an absolute systemId. If it is
 290      * relative, it shall be resolved using the base or user.dir property if
 291      * base is not specified.
 292      *
 293      * @param file The specified file path
 294      * @return The systemId of the file
 295      * @throws CatalogException if the specified file path can not be converted
 296      * to a system id
 297      */
 298     private URI getSystemId(String file) {
 299         URI temp = null;
 300 
 301         try {
 302             temp = Util.verifyAndGetURI(file, baseURI);
 303         } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) {
 304             CatalogMessages.reportRunTimeError(CatalogMessages.ERR_INVALID_PATH,
 305                     new Object[]{file}, e);
 306         }
 307 
 308         return temp;
 309     }
 310 
 311     /**
 312      * Returns a SAXParser instance
 313      * @return a SAXParser instance
 314      * @throws CatalogException if constructing a SAXParser failed
 315      */
 316     private SAXParser getParser() {
 317         SAXParser p = null;
 318         try {
 319             SAXParserFactory spf = new SAXParserFactoryImpl();
 320             spf.setNamespaceAware(true);
 321             spf.setValidating(false);
 322             spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
 323             p = spf.newSAXParser();
 324         } catch (ParserConfigurationException | SAXException e) {
 325             CatalogMessages.reportRunTimeError(CatalogMessages.ERR_PARSING_FAILED, e);
 326         }
 327         return p;
 328     }
 329 
 330     /**
 331      * Indicate that the catalog is empty
 332      *
 333      * @return True if the catalog is empty; False otherwise.
 334      */
 335     public boolean isEmpty() {
 336         return isEmpty;
 337     }
 338 
 339     @Override
 340     public Stream<Catalog> catalogs() {
 341         Iterator<Catalog> iter = new Iterator<Catalog>() {
 342             Catalog nextCatalog = null;
 343 
 344             //Current index of the input files
 345             int inputFilesIndex = 0;
 346 
 347             //Next catalog
 348             int nextCatalogIndex = 0;
 349 
 350             @Override
 351             public boolean hasNext() {
 352                 if (nextCatalog != null) {
 353                     return true;
 354                 } else {
 355                     nextCatalog = nextCatalog();
 356                     return (nextCatalog != null);
 357                 }
 358             }
 359 
 360             @Override
 361             public Catalog next() {
 362                 if (nextCatalog != null || hasNext()) {
 363                     Catalog catalog = nextCatalog;
 364                     nextCatalog = null;
 365                     return catalog;
 366                 } else {
 367                     throw new NoSuchElementException();
 368                 }
 369             }
 370 
 371             /**
 372              * Returns the next alternative catalog.
 373              *
 374              * @return the next catalog if any
 375              */
 376             private Catalog nextCatalog() {
 377                 Catalog c = null;
 378 
 379                 //Check those specified in nextCatalogs
 380                 if (nextCatalogs != null) {
 381                     while (c == null && nextCatalogIndex < nextCatalogs.size()) {
 382                         c = getCatalog(nextCatalogs.get(nextCatalogIndex++).getCatalogURI());
 383                     }
 384                 }
 385 
 386                 //Check the input list
 387                 if (c == null && inputFiles != null) {
 388                     while (c == null && inputFilesIndex < inputFiles.size()) {
 389                         c = getCatalog(getSystemId(inputFiles.get(inputFilesIndex++)));
 390                     }
 391                 }
 392 
 393                 return c;
 394             }
 395         };
 396 
 397         return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
 398                 iter, Spliterator.ORDERED | Spliterator.NONNULL), false);
 399     }
 400 
 401     /**
 402      * Adds the catalog to the nextCatalog list
 403      *
 404      * @param catalog a catalog specified in a nextCatalog entry
 405      */
 406     void addNextCatalog(NextCatalog catalog) {
 407         if (catalog == null) {
 408             return;
 409         }
 410 
 411         if (nextCatalogs == null) {
 412             nextCatalogs = new ArrayList<>();
 413         }
 414 
 415         nextCatalogs.add(catalog);
 416     }
 417 
 418     /**
 419      * Loads all alternative catalogs.
 420      */
 421     void loadNextCatalogs() {
 422         //loads catalogs specified in nextCatalogs
 423         if (nextCatalogs != null) {
 424             for (NextCatalog next : nextCatalogs) {
 425                 getCatalog(next.getCatalogURI());
 426             }
 427         }
 428 
 429         //loads catalogs from the input list
 430         if (inputFiles != null) {
 431             for (String file : inputFiles) {
 432                 getCatalog(getSystemId(file));
 433             }
 434         }
 435     }
 436 
 437     /**
 438      * Returns a Catalog object by the specified path.
 439      *
 440      * @param path the path to a catalog
 441      * @return a Catalog object
 442      */
 443     Catalog getCatalog(URI uri) {
 444         if (uri == null) {
 445             return null;
 446         }
 447 
 448         Catalog c = null;
 449         String path = uri.toASCIIString();
 450 
 451         if (verifyCatalogFile(uri)) {
 452             c = getLoadedCatalog(path);
 453             if (c == null) {
 454                 c = new CatalogImpl(this, features, path);
 455                 saveLoadedCatalog(path, c);
 456             }
 457         }
 458         return c;
 459     }
 460 
 461     /**
 462      * Saves a loaded Catalog.
 463      *
 464      * @param catalogId the catalogId associated with the Catalog object
 465      * @param c the Catalog to be saved
 466      */
 467     void saveLoadedCatalog(String catalogId, Catalog c) {
 468         loadedCatalogs.put(catalogId, c);
 469     }
 470 
 471     /**
 472      * Returns a count of all loaded catalogs, including delegate catalogs.
 473      *
 474      * @return a count of all loaded catalogs
 475      */
 476     int loadedCatalogCount() {
 477         return loadedCatalogs.size() + delegateCatalogs.size();
 478     }
 479 }