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