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