001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.model.xml;
028
029import gov.nist.secauto.metaschema.core.model.MetaschemaException;
030import gov.nist.secauto.metaschema.core.util.CollectionUtil;
031import gov.nist.secauto.metaschema.core.util.ObjectUtils;
032
033import org.apache.logging.log4j.LogManager;
034import org.apache.logging.log4j.Logger;
035
036import java.io.File;
037import java.io.IOException;
038import java.net.MalformedURLException;
039import java.net.URI;
040import java.net.URISyntaxException;
041import java.net.URL;
042import java.nio.file.Path;
043import java.util.Collection;
044import java.util.Deque;
045import java.util.LinkedHashMap;
046import java.util.LinkedList;
047import java.util.Map;
048import java.util.stream.Collectors;
049
050import edu.umd.cs.findbugs.annotations.NonNull;
051
052public abstract class AbstractLoader<T> {
053  private static final Logger LOGGER = LogManager.getLogger(ConstraintLoader.class);
054
055  @NonNull
056  private final Map<URI, T> cache = new LinkedHashMap<>(); // NOPMD - intentional
057
058  /**
059   * Retrieve the set of loaded resources.
060   *
061   * @return the set of loaded resources
062   */
063  @NonNull
064  public Collection<T> getLoadedResources() {
065    return CollectionUtil.unmodifiableCollection(ObjectUtils.notNull(cache.values()));
066  }
067
068  /**
069   * Retrieve a mapping of resource URIs to the associated loaded resource.
070   *
071   * @return the mapping
072   */
073  @NonNull
074  protected Map<URI, T> getCachedEntries() {
075    return CollectionUtil.unmodifiableMap(cache);
076  }
077
078  /**
079   * Load a resource from the specified URI.
080   *
081   * @param resource
082   *          the resource to load
083   * @return the loaded instance for the specified resource
084   * @throws MetaschemaException
085   *           if an error occurred while processing the resource
086   * @throws IOException
087   *           if an error occurred parsing the resource
088   */
089  @NonNull
090  public T load(@NonNull URI resource) throws MetaschemaException, IOException {
091    if (!resource.isAbsolute()) {
092      throw new IllegalArgumentException(String.format("The URI '%s' must be absolute.", resource.toString()));
093    }
094    return loadInternal(resource, new LinkedList<>());
095  }
096
097  /**
098   * Load a resource from the specified path.
099   *
100   * @param path
101   *          the resource to load
102   * @return the loaded instance for the specified resource
103   * @throws MetaschemaException
104   *           if an error occurred while processing the resource
105   * @throws IOException
106   *           if an error occurred parsing the resource
107   */
108  @NonNull
109  public T load(@NonNull Path path) throws MetaschemaException, IOException {
110    return loadInternal(ObjectUtils.notNull(path.toAbsolutePath().normalize().toUri()), new LinkedList<>());
111  }
112
113  /**
114   * Load a resource from the specified file.
115   *
116   * @param file
117   *          the resource to load
118   * @return the loaded instance for the specified resource
119   * @throws MetaschemaException
120   *           if an error occurred while processing the resource
121   * @throws IOException
122   *           if an error occurred parsing the resource
123   */
124  @NonNull
125  public T load(@NonNull File file) throws MetaschemaException, IOException {
126    return load(file.toPath());
127  }
128
129  /**
130   * Loads a resource from the specified URL.
131   *
132   * @param url
133   *          the URL to load the resource from
134   * @return the loaded instance for the specified resource
135   * @throws MetaschemaException
136   *           if an error occurred while processing the resource
137   * @throws IOException
138   *           if an error occurred parsing the resource
139   */
140  @NonNull
141  public T load(@NonNull URL url) throws MetaschemaException, IOException {
142    try {
143      URI resource = url.toURI();
144      return loadInternal(ObjectUtils.notNull(resource), new LinkedList<>());
145    } catch (URISyntaxException ex) {
146      // this should not happen
147      LOGGER.error("Invalid url", ex);
148      throw new IOException(ex);
149    }
150  }
151
152  /**
153   * Loads a resource from the provided URI.
154   * <p>
155   * If the resource imports other resources, the provided
156   * {@code visitedResources} can be used to track circular imports. This is
157   * useful when this method recurses into included resources.
158   * <p>
159   * Previously loaded resources are provided by the cache. This method will add
160   * the resource to the cache after all imported resources have been loaded.
161   *
162   * @param resource
163   *          the resource to load
164   * @param visitedResources
165   *          a LIFO queue representing previously visited resources in an import
166   *          chain
167   * @return the loaded resource
168   * @throws MetaschemaException
169   *           if an error occurred while processing the resource
170   * @throws MalformedURLException
171   *           if the provided URI is malformed
172   * @throws IOException
173   *           if an error occurred parsing the resource
174   */
175  @NonNull
176  protected T loadInternal(@NonNull URI resource, @NonNull Deque<URI> visitedResources)
177      throws MetaschemaException, MalformedURLException, IOException {
178    // first check if the current resource has been visited to prevent cycles
179    if (visitedResources.contains(resource)) {
180      throw new MetaschemaException("Cycle detected in metaschema includes for '" + resource + "'. Call stack: '"
181          + visitedResources.stream().map(n -> n.toString()).collect(Collectors.joining(",")));
182    }
183
184    T retval = cache.get(resource);
185    if (retval == null) {
186      LOGGER.info("Loading module '{}'", resource);
187
188      try {
189        visitedResources.push(resource);
190        retval = parseResource(resource, visitedResources);
191      } finally {
192        visitedResources.pop();
193      }
194      cache.put(resource, retval);
195    } else {
196      if (LOGGER.isDebugEnabled()) {
197        LOGGER.debug("Found metaschema in cache '{}'", resource);
198      }
199    }
200    return ObjectUtils.notNull(retval);
201  }
202
203  /**
204   * Parse the provided {@code resource}.
205   *
206   * @param resource
207   *          the resource to parse
208   * @param visitedResources
209   *          a stack representing previously parsed resources imported by the
210   *          provided {@code resource}
211   * @return the parsed resource
212   * @throws IOException
213   *           if an error occurred while parsing the resource
214   */
215  protected abstract T parseResource(@NonNull URI resource, @NonNull Deque<URI> visitedResources)
216      throws IOException;
217
218}