1 /*
2  * Copyright (c) 2015, 2019, 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 
PreferType(String literal)98         PreferType(String literal) {
99             this.literal = literal;
100         }
101 
prefer(String prefer)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 
ResolveType(String literal)117         ResolveType(String literal) {
118             this.literal = literal;
119         }
120 
getType(String resolveType)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 
isType(String type)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      * @param parent the parent Catalog
140      */
GroupEntry(CatalogEntryType type, CatalogImpl parent)141     public GroupEntry(CatalogEntryType type, CatalogImpl parent) {
142         super(type);
143         this.parent = parent;
144     }
145 
146     /**
147      * Constructs a group entry.
148      *
149      * @param base The baseURI attribute
150      * @param attributes The attributes
151      */
GroupEntry(String base, String... attributes)152     public GroupEntry(String base, String... attributes) {
153         this(null, base, attributes);
154     }
155 
156     /**
157      * Resets the group entry to its initial state.
158      */
reset()159     public void reset() {
160         isInstantMatch = false;
161         rewriteMatch = null;
162         longestRewriteMatch = 0;
163         suffixMatch = null;
164         longestSuffixMatch = 0;
165         systemEntrySearched = false;
166     }
167     /**
168      * Constructs a group entry.
169      * @param catalog the catalog this GroupEntry belongs to
170      * @param base the baseURI attribute
171      * @param attributes the attributes
172      */
GroupEntry(CatalogImpl catalog, String base, String... attributes)173     public GroupEntry(CatalogImpl catalog, String base, String... attributes) {
174         super(CatalogEntryType.GROUP, base);
175         setPrefer(attributes[ATTRIBUTE_PREFER]);
176         this.catalog = catalog;
177     }
178 
179     /**
180      * Sets the catalog for this GroupEntry.
181      *
182      * @param catalog the catalog this GroupEntry belongs to
183      */
setCatalog(CatalogImpl catalog)184     void setCatalog(CatalogImpl catalog) {
185         this.catalog = catalog;
186     }
187 
188     /**
189      * Adds an entry.
190      *
191      * @param entry The entry to be added.
192      */
addEntry(BaseEntry entry)193     public void addEntry(BaseEntry entry) {
194         entries.add(entry);
195     }
196 
197     /**
198      * Sets the prefer property. If the value is null or empty, or any String
199      * other than the defined, it will be assumed as the default value.
200      *
201      * @param value The value of the prefer attribute
202      */
setPrefer(String value)203     public final void setPrefer(String value) {
204         isPreferPublic = PreferType.PUBLIC.prefer(value);
205     }
206 
207     /**
208      * Queries the prefer attribute
209      *
210      * @return true if the prefer attribute is set to system, false if not.
211      */
isPreferPublic()212     public boolean isPreferPublic() {
213         return isPreferPublic;
214     }
215 
216     /**
217      * Attempt to find a matching entry in the catalog by systemId.
218      *
219      * <p>
220      * The method searches through the system-type entries, including system,
221      * rewriteSystem, systemSuffix, delegateSystem, and group entries in the
222      * current catalog in order to find a match.
223      *
224      *
225      * @param systemId The system identifier of the external entity being
226      * referenced.
227      *
228      * @return a URI string if a mapping is found, or null otherwise.
229      */
matchSystem(String systemId)230     public String matchSystem(String systemId) {
231         systemEntrySearched = true;
232         String match = null;
233         for (BaseEntry entry : entries) {
234             switch (entry.type) {
235                 case SYSTEM:
236                     match = ((SystemEntry) entry).match(systemId);
237                     //if there's a matching system entry, use it
238                     if (match != null) {
239                         isInstantMatch = true;
240                         return match;
241                     }
242                     break;
243                 case REWRITESYSTEM:
244                     match = ((RewriteSystem) entry).match(systemId, longestRewriteMatch);
245                     if (match != null) {
246                         rewriteMatch = match;
247                         longestRewriteMatch = ((RewriteSystem) entry).getSystemIdStartString().length();
248                     }
249                     break;
250                 case SYSTEMSUFFIX:
251                     match = ((SystemSuffix) entry).match(systemId, longestSuffixMatch);
252                     if (match != null) {
253                         suffixMatch = match;
254                         longestSuffixMatch = ((SystemSuffix) entry).getSystemIdSuffix().length();
255                     }
256                     break;
257                 case GROUP:
258                     GroupEntry grpEntry = (GroupEntry) entry;
259                     match = grpEntry.matchSystem(systemId);
260                     if (grpEntry.isInstantMatch) {
261                         //use it if there is a match of the system type
262                         return match;
263                     } else if (grpEntry.longestRewriteMatch > longestRewriteMatch) {
264                         longestRewriteMatch = grpEntry.longestRewriteMatch;
265                         rewriteMatch = match;
266                     } else if (grpEntry.longestSuffixMatch > longestSuffixMatch) {
267                         longestSuffixMatch = grpEntry.longestSuffixMatch;
268                         suffixMatch = match;
269                     }
270                     break;
271             }
272         }
273 
274         if (longestRewriteMatch > 0) {
275             return rewriteMatch;
276         } else if (longestSuffixMatch > 0) {
277             return suffixMatch;
278         }
279 
280         //if no single match is found, try delegates
281         return matchDelegate(CatalogEntryType.DELEGATESYSTEM, systemId);
282     }
283 
284     /**
285      * Attempt to find a matching entry in the catalog by publicId.
286      *
287      * <p>
288      * The method searches through the public-type entries, including public,
289      * delegatePublic, and group entries in the current catalog in order to find
290      * a match.
291      *
292      *
293      * @param publicId The public identifier of the external entity being
294      * referenced.
295      *
296      * @return a URI string if a mapping is found, or null otherwise.
297      */
matchPublic(String publicId)298     public String matchPublic(String publicId) {
299         /*
300            When both public and system identifiers are specified, and prefer is
301         not public (that is, system), only system entry will be used.
302         */
303         if (!isPreferPublic && systemEntrySearched) {
304             return null;
305         }
306         //match public entries
307         String match = null;
308         for (BaseEntry entry : entries) {
309             switch (entry.type) {
310                 case PUBLIC:
311                     match = ((PublicEntry) entry).match(publicId);
312                     break;
313                 case URI:
314                     match = ((UriEntry) entry).match(publicId);
315                     break;
316                 case GROUP:
317                     match = ((GroupEntry) entry).matchPublic(publicId);
318                     break;
319             }
320             if (match != null) {
321                 return match;
322             }
323         }
324 
325         //if no single match is found, try delegates
326         return matchDelegate(CatalogEntryType.DELEGATEPUBLIC, publicId);
327     }
328 
329     /**
330      * Attempt to find a matching entry in the catalog by the uri element.
331      *
332      * <p>
333      * The method searches through the uri-type entries, including uri,
334      * rewriteURI, uriSuffix, delegateURI and group entries in the current
335      * catalog in order to find a match.
336      *
337      *
338      * @param uri The URI reference of a resource.
339      *
340      * @return a URI string if a mapping is found, or null otherwise.
341      */
matchURI(String uri)342     public String matchURI(String uri) {
343         String match = null;
344         for (BaseEntry entry : entries) {
345             switch (entry.type) {
346                 case URI:
347                     match = ((UriEntry) entry).match(uri);
348                     if (match != null) {
349                         isInstantMatch = true;
350                         return match;
351                     }
352                     break;
353                 case REWRITEURI:
354                     match = ((RewriteUri) entry).match(uri, longestRewriteMatch);
355                     if (match != null) {
356                         rewriteMatch = match;
357                         longestRewriteMatch = ((RewriteUri) entry).getURIStartString().length();
358                     }
359                     break;
360                 case URISUFFIX:
361                     match = ((UriSuffix) entry).match(uri, longestSuffixMatch);
362                     if (match != null) {
363                         suffixMatch = match;
364                         longestSuffixMatch = ((UriSuffix) entry).getURISuffix().length();
365                     }
366                     break;
367                 case GROUP:
368                     GroupEntry grpEntry = (GroupEntry) entry;
369                     match = grpEntry.matchURI(uri);
370                     if (grpEntry.isInstantMatch) {
371                         //use it if there is a match of the uri type
372                         return match;
373                     } else if (grpEntry.longestRewriteMatch > longestRewriteMatch) {
374                         rewriteMatch = match;
375                         longestRewriteMatch = grpEntry.longestRewriteMatch;
376                     } else if (grpEntry.longestSuffixMatch > longestSuffixMatch) {
377                         suffixMatch = match;
378                         longestSuffixMatch = grpEntry.longestSuffixMatch;
379                     }
380                     break;
381             }
382         }
383 
384         if (longestRewriteMatch > 0) {
385             return rewriteMatch;
386         } else if (longestSuffixMatch > 0) {
387             return suffixMatch;
388         }
389 
390         //if no single match is found, try delegates
391         return matchDelegate(CatalogEntryType.DELEGATEURI, uri);
392     }
393 
394     /**
395      * Matches delegatePublic or delegateSystem against the specified id
396      *
397      * @param type the type of the Catalog entry
398      * @param id the system or public id to be matched
399      * @return the URI string if a mapping is found, or null otherwise.
400      */
matchDelegate(CatalogEntryType type, String id)401     private String matchDelegate(CatalogEntryType type, String id) {
402         String match = null;
403         int longestMatch = 0;
404         URI catalogId = null;
405         URI temp;
406 
407         //Check delegate types in the current catalog
408         for (BaseEntry entry : entries) {
409             if (entry.type == type) {
410                 if (type == CatalogEntryType.DELEGATESYSTEM) {
411                     temp = ((DelegateSystem)entry).matchURI(id, longestMatch);
412                 } else if (type == CatalogEntryType.DELEGATEPUBLIC) {
413                     temp = ((DelegatePublic)entry).matchURI(id, longestMatch);
414                 } else {
415                     temp = ((DelegateUri)entry).matchURI(id, longestMatch);
416                 }
417                 if (temp != null) {
418                     longestMatch = entry.getMatchId().length();
419                     catalogId = temp;
420                 }
421             }
422         }
423 
424         //Check delegate Catalogs
425         if (catalogId != null) {
426             Catalog delegateCatalog = loadDelegateCatalog(catalog, catalogId);
427 
428             if (delegateCatalog != null) {
429                 if (type == CatalogEntryType.DELEGATESYSTEM) {
430                     match = delegateCatalog.matchSystem(id);
431                 } else if (type == CatalogEntryType.DELEGATEPUBLIC) {
432                     match = delegateCatalog.matchPublic(id);
433                 } else {
434                     match = delegateCatalog.matchURI(id);
435                 }
436             }
437         }
438 
439         return match;
440     }
441 
442     /**
443      * Loads all delegate catalogs.
444      *
445      * @param parent the parent catalog of the delegate catalogs
446      */
loadDelegateCatalogs(CatalogImpl parent)447     void loadDelegateCatalogs(CatalogImpl parent) {
448         entries.stream()
449                 .filter((entry) -> (entry.type == CatalogEntryType.DELEGATESYSTEM ||
450                         entry.type == CatalogEntryType.DELEGATEPUBLIC ||
451                         entry.type == CatalogEntryType.DELEGATEURI))
452                 .map((entry) -> (AltCatalog)entry)
453                 .forEach((altCatalog) -> {
454                         loadDelegateCatalog(parent, altCatalog.getCatalogURI());
455         });
456     }
457 
458     /**
459      * Loads a delegate catalog by the catalogId specified.
460      *
461      * @param parent the parent catalog of the delegate catalog
462      * @param catalogURI the URI to the catalog
463      */
loadDelegateCatalog(CatalogImpl parent, URI catalogURI)464     Catalog loadDelegateCatalog(CatalogImpl parent, URI catalogURI) {
465         CatalogImpl delegateCatalog = null;
466         if (catalogURI != null) {
467             String catalogId = catalogURI.toASCIIString();
468             if (verifyCatalogFile(parent, catalogURI)) {
469                 delegateCatalog = getLoadedCatalog(catalogId);
470                 if (delegateCatalog == null) {
471                     delegateCatalog = new CatalogImpl(parent, features, catalogURI);
472                     delegateCatalog.load();
473                     delegateCatalogs.put(catalogId, delegateCatalog);
474                 }
475             }
476         }
477 
478         return delegateCatalog;
479     }
480 
481     /**
482      * Returns a previously loaded Catalog object if found.
483      *
484      * @param catalogId The systemId of a catalog
485      * @return a Catalog object previously loaded, or null if none in the saved
486      * list
487      */
getLoadedCatalog(String catalogId)488     CatalogImpl getLoadedCatalog(String catalogId) {
489         CatalogImpl c = null;
490 
491         //check delegate Catalogs
492         c = delegateCatalogs.get(catalogId);
493         if (c == null) {
494             //check other loaded Catalogs
495             c = loadedCatalogs.get(catalogId);
496         }
497 
498         return c;
499     }
500 
501 
502     /**
503      * Verifies that the catalog file represented by the catalogId exists. If it
504      * doesn't, returns false to ignore it as specified in the Catalog
505      * specification, section 8. Resource Failures.
506      * <p>
507      * Verifies that the catalog represented by the catalogId has not been
508      * searched or is not circularly referenced.
509      *
510      * @param parent the parent of the catalog to be loaded
511      * @param catalogURI the URI to the catalog
512      * @throws CatalogException if circular reference is found.
513      * @return true if the catalogId passed verification, false otherwise
514      */
verifyCatalogFile(CatalogImpl parent, URI catalogURI)515     final boolean verifyCatalogFile(CatalogImpl parent, URI catalogURI) {
516         if (catalogURI == null) {
517             return false;
518         }
519 
520         //Ignore it if it doesn't exist
521         if (Util.isFileUri(catalogURI) &&
522                 !Util.isFileUriExist(catalogURI, false)) {
523             return false;
524         }
525 
526         String catalogId = catalogURI.toASCIIString();
527         if (catalogsSearched.contains(catalogId) || isCircular(parent, catalogId)) {
528             CatalogMessages.reportRunTimeError(CatalogMessages.ERR_CIRCULAR_REFERENCE,
529                     new Object[]{CatalogMessages.sanitize(catalogId)});
530         }
531 
532         return true;
533     }
534 
535     /**
536      * Checks whether the catalog is circularly referenced
537      *
538      * @param parent the parent of the catalog to be loaded
539      * @param systemId the system identifier of the catalog to be loaded
540      * @return true if is circular, false otherwise
541      */
isCircular(CatalogImpl parent, String systemId)542     boolean isCircular(CatalogImpl parent, String systemId) {
543         // first, check the parent of the catalog to be loaded
544         if (parent == null) {
545             return false;
546         }
547 
548         if (parent.systemId.equals(systemId)) {
549             return true;
550         }
551 
552        // next, check parent's parent
553         return parent.isCircular(parent.parent, systemId);
554     }
555 }
556