1 /*
   2  * Copyright (c) 2015, 2017, 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 java.net.URI;
  28 import java.util.ArrayList;
  29 import java.util.HashMap;
  30 import java.util.List;
  31 import java.util.Map;
  32 
  33 /**
  34  * Represents a group entry.
  35  *
  36  * @since 9
  37  */
  38 class GroupEntry extends BaseEntry {
  39     static final int ATTRIBUTE_PREFER = 0;
  40     static final int ATTRIBUTE_DEFFER = 1;
  41     static final int ATTRIBUTE_RESOLUTION = 2;
  42 
  43     //Unmodifiable features when the Catalog is created
  44     CatalogFeatures features;
  45 
  46     //Value of the prefer attribute
  47     boolean isPreferPublic = true;
  48 
  49     //The parent of the catalog instance
  50     CatalogImpl parent = null;
  51 
  52     //The catalog instance this group belongs to
  53     CatalogImpl catalog;
  54 
  55     //A list of all entries in a catalog or group
  56     List<BaseEntry> entries = new ArrayList<>();
  57 
  58     //loaded delegated catalog by system id
  59     Map<String, CatalogImpl> delegateCatalogs = new HashMap<>();
  60 
  61     //A list of all loaded Catalogs, including this, and next catalogs
  62     Map<String, CatalogImpl> loadedCatalogs = new HashMap<>();
  63 
  64     /*
  65      A list of Catalog Ids that have already been searched in a matching
  66      operation. Check this list before constructing new Catalog to avoid circular
  67      reference.
  68      */
  69     List<String> catalogsSearched = new ArrayList<>();
  70 
  71     //A flag to indicate whether the current match is a system or uri
  72     boolean isInstantMatch = false;
  73 
  74     //A match of a rewrite type
  75     String rewriteMatch = null;
  76 
  77     //The length of the longest match of a rewrite type
  78     int longestRewriteMatch = 0;
  79 
  80     //A match of a suffix type
  81     String suffixMatch = null;
  82 
  83     //The length of the longest match of a suffix type
  84     int longestSuffixMatch = 0;
  85 
  86     //Indicate whether a system entry has been searched
  87     boolean systemEntrySearched = false;
  88 
  89     /**
  90      * PreferType represents possible values of the prefer property
  91      */
  92     public static enum PreferType {
  93         PUBLIC("public"),
  94         SYSTEM("system");
  95 
  96         final String literal;
  97 
  98         PreferType(String literal) {
  99             this.literal = literal;
 100         }
 101 
 102         public boolean prefer(String prefer) {
 103             return literal.equals(prefer);
 104         }
 105     }
 106 
 107     /**
 108      * PreferType represents possible values of the resolve property
 109      */
 110     public static enum ResolveType {
 111         STRICT(CatalogFeatures.RESOLVE_STRICT),
 112         CONTINUE(CatalogFeatures.RESOLVE_CONTINUE),
 113         IGNORE(CatalogFeatures.RESOLVE_IGNORE);
 114 
 115         final String literal;
 116 
 117         ResolveType(String literal) {
 118             this.literal = literal;
 119         }
 120 
 121         static public ResolveType getType(String resolveType) {
 122             for (ResolveType type : ResolveType.values()) {
 123                 if (type.isType(resolveType)) {
 124                     return type;
 125                 }
 126             }
 127             return null;
 128         }
 129 
 130         public boolean isType(String type) {
 131             return literal.equals(type);
 132         }
 133     }
 134 
 135     /**
 136      * Constructs a GroupEntry
 137      *
 138      * @param type The type of the entry
 139      */
 140     public GroupEntry(CatalogEntryType type, CatalogImpl parent) {
 141         super(type);
 142         this.parent = parent;
 143     }
 144 
 145     /**
 146      * Constructs a group entry.
 147      *
 148      * @param base The baseURI attribute
 149      * @param attributes The attributes
 150      */
 151     public GroupEntry(String base, String... attributes) {
 152         this(null, base, attributes);
 153     }
 154 
 155     /**
 156      * Resets the group entry to its initial state.
 157      */
 158     public void reset() {
 159         isInstantMatch = false;
 160         rewriteMatch = null;
 161         longestRewriteMatch = 0;
 162         suffixMatch = null;
 163         longestSuffixMatch = 0;
 164         systemEntrySearched = false;
 165     }
 166     /**
 167      * Constructs a group entry.
 168      * @param catalog The catalog this GroupEntry belongs
 169      * @param base The baseURI attribute
 170      * @param attributes The attributes
 171      */
 172     public GroupEntry(CatalogImpl catalog, String base, String... attributes) {
 173         super(CatalogEntryType.GROUP, base);
 174         setPrefer(attributes[ATTRIBUTE_PREFER]);
 175         this.catalog = catalog;
 176     }
 177 
 178     /**
 179      * Adds an entry.
 180      *
 181      * @param entry The entry to be added.
 182      */
 183     public void addEntry(BaseEntry entry) {
 184         entries.add(entry);
 185     }
 186 
 187     /**
 188      * Sets the prefer property. If the value is null or empty, or any String
 189      * other than the defined, it will be assumed as the default value.
 190      *
 191      * @param value The value of the prefer attribute
 192      */
 193     public final void setPrefer(String value) {
 194         isPreferPublic = PreferType.PUBLIC.prefer(value);
 195     }
 196 
 197     /**
 198      * Queries the prefer attribute
 199      *
 200      * @return true if the prefer attribute is set to system, false if not.
 201      */
 202     public boolean isPreferPublic() {
 203         return isPreferPublic;
 204     }
 205 
 206     /**
 207      * Attempt to find a matching entry in the catalog by systemId.
 208      *
 209      * <p>
 210      * The method searches through the system-type entries, including system,
 211      * rewriteSystem, systemSuffix, delegateSystem, and group entries in the
 212      * current catalog in order to find a match.
 213      *
 214      *
 215      * @param systemId The system identifier of the external entity being
 216      * referenced.
 217      *
 218      * @return a URI string if a mapping is found, or null otherwise.
 219      */
 220     public String matchSystem(String systemId) {
 221         systemEntrySearched = true;
 222         String match = null;
 223         for (BaseEntry entry : entries) {
 224             switch (entry.type) {
 225                 case SYSTEM:
 226                     match = ((SystemEntry) entry).match(systemId);
 227                     //if there's a matching system entry, use it
 228                     if (match != null) {
 229                         isInstantMatch = true;
 230                         return match;
 231                     }
 232                     break;
 233                 case REWRITESYSTEM:
 234                     match = ((RewriteSystem) entry).match(systemId, longestRewriteMatch);
 235                     if (match != null) {
 236                         rewriteMatch = match;
 237                         longestRewriteMatch = ((RewriteSystem) entry).getSystemIdStartString().length();
 238                     }
 239                     break;
 240                 case SYSTEMSUFFIX:
 241                     match = ((SystemSuffix) entry).match(systemId, longestSuffixMatch);
 242                     if (match != null) {
 243                         suffixMatch = match;
 244                         longestSuffixMatch = ((SystemSuffix) entry).getSystemIdSuffix().length();
 245                     }
 246                     break;
 247                 case GROUP:
 248                     GroupEntry grpEntry = (GroupEntry) entry;
 249                     match = grpEntry.matchSystem(systemId);
 250                     if (grpEntry.isInstantMatch) {
 251                         //use it if there is a match of the system type
 252                         return match;
 253                     } else if (grpEntry.longestRewriteMatch > longestRewriteMatch) {
 254                         longestRewriteMatch = grpEntry.longestRewriteMatch;
 255                         rewriteMatch = match;
 256                     } else if (grpEntry.longestSuffixMatch > longestSuffixMatch) {
 257                         longestSuffixMatch = grpEntry.longestSuffixMatch;
 258                         suffixMatch = match;
 259                     }
 260                     break;
 261             }
 262         }
 263 
 264         if (longestRewriteMatch > 0) {
 265             return rewriteMatch;
 266         } else if (longestSuffixMatch > 0) {
 267             return suffixMatch;
 268         }
 269 
 270         //if no single match is found, try delegates
 271         return matchDelegate(CatalogEntryType.DELEGATESYSTEM, systemId);
 272     }
 273 
 274     /**
 275      * Attempt to find a matching entry in the catalog by publicId.
 276      *
 277      * <p>
 278      * The method searches through the public-type entries, including public,
 279      * delegatePublic, and group entries in the current catalog in order to find
 280      * a match.
 281      *
 282      *
 283      * @param publicId The public identifier of the external entity being
 284      * referenced.
 285      *
 286      * @return a URI string if a mapping is found, or null otherwise.
 287      */
 288     public String matchPublic(String publicId) {
 289         /*
 290            When both public and system identifiers are specified, and prefer is
 291         not public (that is, system), only system entry will be used.
 292         */
 293         if (!isPreferPublic && systemEntrySearched) {
 294             return null;
 295         }
 296         //match public entries
 297         String match = null;
 298         for (BaseEntry entry : entries) {
 299             switch (entry.type) {
 300                 case PUBLIC:
 301                     match = ((PublicEntry) entry).match(publicId);
 302                     break;
 303                 case URI:
 304                     match = ((UriEntry) entry).match(publicId);
 305                     break;
 306                 case GROUP:
 307                     match = ((GroupEntry) entry).matchPublic(publicId);
 308                     break;
 309             }
 310             if (match != null) {
 311                 return match;
 312             }
 313         }
 314 
 315         //if no single match is found, try delegates
 316         return matchDelegate(CatalogEntryType.DELEGATEPUBLIC, publicId);
 317     }
 318 
 319     /**
 320      * Attempt to find a matching entry in the catalog by the uri element.
 321      *
 322      * <p>
 323      * The method searches through the uri-type entries, including uri,
 324      * rewriteURI, uriSuffix, delegateURI and group entries in the current
 325      * catalog in order to find a match.
 326      *
 327      *
 328      * @param uri The URI reference of a resource.
 329      *
 330      * @return a URI string if a mapping is found, or null otherwise.
 331      */
 332     public String matchURI(String uri) {
 333         String match = null;
 334         for (BaseEntry entry : entries) {
 335             switch (entry.type) {
 336                 case URI:
 337                     match = ((UriEntry) entry).match(uri);
 338                     if (match != null) {
 339                         isInstantMatch = true;
 340                         return match;
 341                     }
 342                     break;
 343                 case REWRITEURI:
 344                     match = ((RewriteUri) entry).match(uri, longestRewriteMatch);
 345                     if (match != null) {
 346                         rewriteMatch = match;
 347                         longestRewriteMatch = ((RewriteUri) entry).getURIStartString().length();
 348                     }
 349                     break;
 350                 case URISUFFIX:
 351                     match = ((UriSuffix) entry).match(uri, longestSuffixMatch);
 352                     if (match != null) {
 353                         suffixMatch = match;
 354                         longestSuffixMatch = ((UriSuffix) entry).getURISuffix().length();
 355                     }
 356                     break;
 357                 case GROUP:
 358                     GroupEntry grpEntry = (GroupEntry) entry;
 359                     match = grpEntry.matchURI(uri);
 360                     if (grpEntry.isInstantMatch) {
 361                         //use it if there is a match of the uri type
 362                         return match;
 363                     } else if (grpEntry.longestRewriteMatch > longestRewriteMatch) {
 364                         rewriteMatch = match;
 365                     } else if (grpEntry.longestSuffixMatch > longestSuffixMatch) {
 366                         suffixMatch = match;
 367                     }
 368                     break;
 369             }
 370         }
 371 
 372         if (longestRewriteMatch > 0) {
 373             return rewriteMatch;
 374         } else if (longestSuffixMatch > 0) {
 375             return suffixMatch;
 376         }
 377 
 378         //if no single match is found, try delegates
 379         return matchDelegate(CatalogEntryType.DELEGATEURI, uri);
 380     }
 381 
 382     /**
 383      * Matches delegatePublic or delegateSystem against the specified id
 384      *
 385      * @param isSystem The flag to indicate whether the delegate is system or
 386      * public
 387      * @param id The system or public id to be matched
 388      * @return The URI string if a mapping is found, or null otherwise.
 389      */
 390     private String matchDelegate(CatalogEntryType type, String id) {
 391         String match = null;
 392         int longestMatch = 0;
 393         URI catalogId = null;
 394         URI temp;
 395 
 396         //Check delegate types in the current catalog
 397         for (BaseEntry entry : entries) {
 398             if (entry.type == type) {
 399                 if (type == CatalogEntryType.DELEGATESYSTEM) {
 400                     temp = ((DelegateSystem)entry).matchURI(id, longestMatch);
 401                 } else if (type == CatalogEntryType.DELEGATEPUBLIC) {
 402                     temp = ((DelegatePublic)entry).matchURI(id, longestMatch);
 403                 } else {
 404                     temp = ((DelegateUri)entry).matchURI(id, longestMatch);
 405                 }
 406                 if (temp != null) {
 407                     longestMatch = entry.getMatchId().length();
 408                     catalogId = temp;
 409                 }
 410             }
 411         }
 412 
 413         //Check delegate Catalogs
 414         if (catalogId != null) {
 415             Catalog delegateCatalog = loadCatalog(catalogId);
 416 
 417             if (delegateCatalog != null) {
 418                 if (type == CatalogEntryType.DELEGATESYSTEM) {
 419                     match = delegateCatalog.matchSystem(id);
 420                 } else if (type == CatalogEntryType.DELEGATEPUBLIC) {
 421                     match = delegateCatalog.matchPublic(id);
 422                 } else {
 423                     match = delegateCatalog.matchURI(id);
 424                 }
 425             }
 426         }
 427 
 428         return match;
 429     }
 430 
 431     /**
 432      * Loads all delegate catalogs.
 433      */
 434     void loadDelegateCatalogs() {
 435         entries.stream()
 436                 .filter((entry) -> (entry.type == CatalogEntryType.DELEGATESYSTEM ||
 437                         entry.type == CatalogEntryType.DELEGATEPUBLIC ||
 438                         entry.type == CatalogEntryType.DELEGATEURI))
 439                 .map((entry) -> (AltCatalog)entry)
 440                 .forEach((altCatalog) -> {
 441                         loadCatalog(altCatalog.getCatalogURI());
 442         });
 443     }
 444 
 445     /**
 446      * Loads a delegate catalog by the catalogId specified.
 447      * @param catalogId the catalog Id
 448      */
 449     Catalog loadCatalog(URI catalogURI) {
 450         CatalogImpl delegateCatalog = null;
 451         if (catalogURI != null) {
 452             String catalogId = catalogURI.toASCIIString();
 453             delegateCatalog = getLoadedCatalog(catalogId);
 454             if (delegateCatalog == null) {
 455                 if (verifyCatalogFile(catalogURI)) {
 456                     delegateCatalog = new CatalogImpl(catalog, features, catalogURI);
 457                     delegateCatalog.load();
 458                     delegateCatalogs.put(catalogId, delegateCatalog);
 459                 }
 460             }
 461         }
 462 
 463         return delegateCatalog;
 464     }
 465 
 466     /**
 467      * Returns a previously loaded Catalog object if found.
 468      *
 469      * @param catalogId The systemId of a catalog
 470      * @return a Catalog object previously loaded, or null if none in the saved
 471      * list
 472      */
 473     CatalogImpl getLoadedCatalog(String catalogId) {
 474         CatalogImpl c = null;
 475 
 476         //checl delegate Catalogs
 477         c = delegateCatalogs.get(catalogId);
 478         if (c == null) {
 479             //check other loaded Catalogs
 480             c = loadedCatalogs.get(catalogId);
 481         }
 482 
 483         return c;
 484     }
 485 
 486 
 487     /**
 488      * Verifies that the catalog file represented by the catalogId exists. If it
 489      * doesn't, returns false to ignore it as specified in the Catalog
 490      * specification, section 8. Resource Failures.
 491      * <p>
 492      * Verifies that the catalog represented by the catalogId has not been
 493      * searched or is not circularly referenced.
 494      *
 495      * @param catalogId The URI to a catalog
 496      * @throws CatalogException if circular reference is found.
 497      * @return true if the catalogId passed verification, false otherwise
 498      */
 499     final boolean verifyCatalogFile(URI catalogURI) {
 500         if (catalogURI == null) {
 501             return false;
 502         }
 503 
 504         //Ignore it if it doesn't exist
 505         if (Util.isFileUri(catalogURI) &&
 506                 !Util.isFileUriExist(catalogURI, false)) {
 507             return false;
 508         }
 509 
 510         String catalogId = catalogURI.toASCIIString();
 511         if (catalogsSearched.contains(catalogId) || isCircular(catalogId)) {
 512             CatalogMessages.reportRunTimeError(CatalogMessages.ERR_CIRCULAR_REFERENCE,
 513                     new Object[]{CatalogMessages.sanitize(catalogId)});
 514         }
 515 
 516         return true;
 517     }
 518 
 519     /**
 520      * Checks whether the catalog is circularly referenced
 521      * @param systemId the system identifier of the catalog to be loaded
 522      * @return true if is circular, false otherwise
 523      */
 524     boolean isCircular(String systemId) {
 525         if (parent == null) {
 526             return false;
 527         }
 528 
 529         if (parent.systemId.equals(systemId)) {
 530             return true;
 531         }
 532 
 533         return parent.isCircular(systemId);
 534     }
 535 }