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 */
25package javax.xml.catalog;
26
27import java.net.URI;
28import java.util.ArrayList;
29import java.util.HashMap;
30import java.util.List;
31import java.util.Map;
32
33/**
34 * Represents a group entry.
35 *
36 * @since 9
37 */
38class 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     * @param parent the parent Catalog
140     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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                    } else if (grpEntry.longestSuffixMatch > longestSuffixMatch) {
376                        suffixMatch = match;
377                    }
378                    break;
379            }
380        }
381
382        if (longestRewriteMatch > 0) {
383            return rewriteMatch;
384        } else if (longestSuffixMatch > 0) {
385            return suffixMatch;
386        }
387
388        //if no single match is found, try delegates
389        return matchDelegate(CatalogEntryType.DELEGATEURI, uri);
390    }
391
392    /**
393     * Matches delegatePublic or delegateSystem against the specified id
394     *
395     * @param type the type of the Catalog entry
396     * @param id the system or public id to be matched
397     * @return the URI string if a mapping is found, or null otherwise.
398     */
399    private String matchDelegate(CatalogEntryType type, String id) {
400        String match = null;
401        int longestMatch = 0;
402        URI catalogId = null;
403        URI temp;
404
405        //Check delegate types in the current catalog
406        for (BaseEntry entry : entries) {
407            if (entry.type == type) {
408                if (type == CatalogEntryType.DELEGATESYSTEM) {
409                    temp = ((DelegateSystem)entry).matchURI(id, longestMatch);
410                } else if (type == CatalogEntryType.DELEGATEPUBLIC) {
411                    temp = ((DelegatePublic)entry).matchURI(id, longestMatch);
412                } else {
413                    temp = ((DelegateUri)entry).matchURI(id, longestMatch);
414                }
415                if (temp != null) {
416                    longestMatch = entry.getMatchId().length();
417                    catalogId = temp;
418                }
419            }
420        }
421
422        //Check delegate Catalogs
423        if (catalogId != null) {
424            Catalog delegateCatalog = loadDelegateCatalog(catalog, catalogId);
425
426            if (delegateCatalog != null) {
427                if (type == CatalogEntryType.DELEGATESYSTEM) {
428                    match = delegateCatalog.matchSystem(id);
429                } else if (type == CatalogEntryType.DELEGATEPUBLIC) {
430                    match = delegateCatalog.matchPublic(id);
431                } else {
432                    match = delegateCatalog.matchURI(id);
433                }
434            }
435        }
436
437        return match;
438    }
439
440    /**
441     * Loads all delegate catalogs.
442     *
443     * @param parent the parent catalog of the delegate catalogs
444     */
445    void loadDelegateCatalogs(CatalogImpl parent) {
446        entries.stream()
447                .filter((entry) -> (entry.type == CatalogEntryType.DELEGATESYSTEM ||
448                        entry.type == CatalogEntryType.DELEGATEPUBLIC ||
449                        entry.type == CatalogEntryType.DELEGATEURI))
450                .map((entry) -> (AltCatalog)entry)
451                .forEach((altCatalog) -> {
452                        loadDelegateCatalog(parent, altCatalog.getCatalogURI());
453        });
454    }
455
456    /**
457     * Loads a delegate catalog by the catalogId specified.
458     *
459     * @param parent the parent catalog of the delegate catalog
460     * @param catalogURI the URI to the catalog
461     */
462    Catalog loadDelegateCatalog(CatalogImpl parent, URI catalogURI) {
463        CatalogImpl delegateCatalog = null;
464        if (catalogURI != null) {
465            String catalogId = catalogURI.toASCIIString();
466            if (verifyCatalogFile(parent, catalogURI)) {
467                delegateCatalog = getLoadedCatalog(catalogId);
468                if (delegateCatalog == null) {
469                    delegateCatalog = new CatalogImpl(parent, features, catalogURI);
470                    delegateCatalog.load();
471                    delegateCatalogs.put(catalogId, delegateCatalog);
472                }
473            }
474        }
475
476        return delegateCatalog;
477    }
478
479    /**
480     * Returns a previously loaded Catalog object if found.
481     *
482     * @param catalogId The systemId of a catalog
483     * @return a Catalog object previously loaded, or null if none in the saved
484     * list
485     */
486    CatalogImpl getLoadedCatalog(String catalogId) {
487        CatalogImpl c = null;
488
489        //check delegate Catalogs
490        c = delegateCatalogs.get(catalogId);
491        if (c == null) {
492            //check other loaded Catalogs
493            c = loadedCatalogs.get(catalogId);
494        }
495
496        return c;
497    }
498
499
500    /**
501     * Verifies that the catalog file represented by the catalogId exists. If it
502     * doesn't, returns false to ignore it as specified in the Catalog
503     * specification, section 8. Resource Failures.
504     * <p>
505     * Verifies that the catalog represented by the catalogId has not been
506     * searched or is not circularly referenced.
507     *
508     * @param parent the parent of the catalog to be loaded
509     * @param catalogURI the URI to the catalog
510     * @throws CatalogException if circular reference is found.
511     * @return true if the catalogId passed verification, false otherwise
512     */
513    final boolean verifyCatalogFile(CatalogImpl parent, URI catalogURI) {
514        if (catalogURI == null) {
515            return false;
516        }
517
518        //Ignore it if it doesn't exist
519        if (Util.isFileUri(catalogURI) &&
520                !Util.isFileUriExist(catalogURI, false)) {
521            return false;
522        }
523
524        String catalogId = catalogURI.toASCIIString();
525        if (catalogsSearched.contains(catalogId) || isCircular(parent, catalogId)) {
526            CatalogMessages.reportRunTimeError(CatalogMessages.ERR_CIRCULAR_REFERENCE,
527                    new Object[]{CatalogMessages.sanitize(catalogId)});
528        }
529
530        return true;
531    }
532
533    /**
534     * Checks whether the catalog is circularly referenced
535     *
536     * @param parent the parent of the catalog to be loaded
537     * @param systemId the system identifier of the catalog to be loaded
538     * @return true if is circular, false otherwise
539     */
540    boolean isCircular(CatalogImpl parent, String systemId) {
541        // first, check the parent of the catalog to be loaded
542        if (parent == null) {
543            return false;
544        }
545
546        if (parent.systemId.equals(systemId)) {
547            return true;
548        }
549
550       // next, check parent's parent
551        return parent.isCircular(parent.parent, systemId);
552    }
553}
554