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.databind;
028
029import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
030import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
031import gov.nist.secauto.metaschema.core.metapath.StaticContext;
032import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
033import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
034import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
035import gov.nist.secauto.metaschema.core.model.IFlagContainer;
036import gov.nist.secauto.metaschema.core.model.IModule;
037import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
038import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
039import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
040import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
041import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
042import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
043import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
044import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
045import gov.nist.secauto.metaschema.core.util.ObjectUtils;
046import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
047import gov.nist.secauto.metaschema.databind.io.BindingException;
048import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
049import gov.nist.secauto.metaschema.databind.io.Format;
050import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
051import gov.nist.secauto.metaschema.databind.io.IDeserializer;
052import gov.nist.secauto.metaschema.databind.io.ISerializer;
053import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
054import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
055import gov.nist.secauto.metaschema.databind.model.IClassBinding;
056import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
057import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
058
059import org.json.JSONObject;
060import org.xml.sax.SAXException;
061
062import java.io.IOException;
063import java.math.BigInteger;
064import java.nio.file.Files;
065import java.nio.file.Path;
066import java.time.ZonedDateTime;
067import java.util.List;
068
069import javax.xml.namespace.QName;
070import javax.xml.transform.Source;
071
072import edu.umd.cs.findbugs.annotations.NonNull;
073import edu.umd.cs.findbugs.annotations.Nullable;
074
075/**
076 * Provides information supporting a binding between a set of Module models and
077 * corresponding Java classes.
078 */
079public interface IBindingContext extends IModuleLoaderStrategy {
080
081  /**
082   * Get the singleton {@link IBindingContext} instance, which can be used to load
083   * information that binds a model to a set of Java classes.
084   *
085   * @return a new binding context
086   */
087  @NonNull
088  static IBindingContext instance() {
089    return DefaultBindingContext.instance();
090  }
091
092  /**
093   * Register a matcher used to identify a bound class by the content's root name.
094   *
095   * @param matcher
096   *          the matcher implementation
097   * @return this instance
098   */
099  @NonNull
100  IBindingContext registerBindingMatcher(@NonNull IBindingMatcher matcher);
101
102  /**
103   * Determine the bound class for the provided XML {@link QName}.
104   *
105   * @param rootQName
106   *          the root XML element's QName
107   * @return the bound class or {@code null} if not recognized
108   * @see IBindingContext#registerBindingMatcher(IBindingMatcher)
109   */
110  @Nullable
111  Class<?> getBoundClassForXmlQName(@NonNull QName rootQName);
112
113  /**
114   * Determine the bound class for the provided JSON/YAML property/item name using
115   * any registered matchers.
116   *
117   * @param rootName
118   *          the JSON/YAML property/item name
119   * @return the bound class or {@code null} if not recognized
120   * @see IBindingContext#registerBindingMatcher(IBindingMatcher)
121   */
122  @Nullable
123  Class<?> getBoundClassForJsonName(@NonNull String rootName);
124
125  /**
126   * Get's the {@link IDataTypeAdapter} associated with the specified Java class,
127   * which is used to read and write XML, JSON, and YAML data to and from
128   * instances of that class. Thus, this adapter supports a direct binding between
129   * the Java class and structured data in one of the supported formats. Adapters
130   * are used to support bindings for simple data objects (e.g., {@link String},
131   * {@link BigInteger}, {@link ZonedDateTime}, etc).
132   *
133   * @param <TYPE>
134   *          the class type of the adapter
135   * @param clazz
136   *          the Java {@link Class} for the bound type
137   * @return the adapter instance or {@code null} if the provided class is not
138   *         bound
139   */
140  @Nullable
141  <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz);
142
143  /**
144   * Generate, compile, and load a set of generated Module annotated Java classes
145   * based on the provided Module {@code module}.
146   *
147   * @param module
148   *          the Module module to generate classes for
149   * @param compilePath
150   *          the path to the directory to generate classes in
151   * @return this instance
152   * @throws IOException
153   *           if an error occurred while generating or loading the classes
154   */
155  @SuppressWarnings("PMD.UseProperClassLoader") // false positive
156  @NonNull
157  default IBindingContext registerModule(
158      @NonNull IModule module,
159      @NonNull Path compilePath) throws IOException {
160    Files.createDirectories(compilePath);
161
162    ClassLoader classLoader = ModuleCompilerHelper.newClassLoader(
163        compilePath,
164        ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));
165
166    ModuleCompilerHelper.compileMetaschema(module, compilePath).getGlobalDefinitionClassesAsStream()
167        .filter(definitionInfo -> {
168          boolean retval = false;
169          IFlagContainer definition = definitionInfo.getDefinition();
170          if (definition instanceof IAssemblyDefinition) {
171            IAssemblyDefinition assembly = (IAssemblyDefinition) definition;
172            if (assembly.isRoot()) {
173              retval = true;
174            }
175          }
176          return retval;
177        })
178        .map(
179            generatedClass -> {
180              try {
181                @SuppressWarnings("unchecked") Class<IAssemblyClassBinding> clazz
182                    = ObjectUtils.notNull((Class<IAssemblyClassBinding>) classLoader
183                        .loadClass(generatedClass.getClassName().reflectionName()));
184
185                IAssemblyDefinition definition = (IAssemblyDefinition) generatedClass.getDefinition();
186                return new DynamicBindingMatcher(
187                    definition,
188                    clazz);
189              } catch (ClassNotFoundException ex) {
190                throw new IllegalStateException(ex);
191              }
192            })
193        .forEachOrdered(
194            matcher -> registerBindingMatcher(
195                ObjectUtils.notNull(
196                    matcher)));
197    return this;
198  }
199
200  /**
201   * Gets a data {@link ISerializer} which can be used to write Java instance data
202   * for the provided class to the requested format. The provided class must be a
203   * bound Java class with a {@link MetaschemaAssembly} or {@link MetaschemaField}
204   * annotation for which a {@link IClassBinding} exists.
205   *
206   * @param <CLASS>
207   *          the Java type this deserializer can write data from
208   * @param format
209   *          the format to serialize into
210   * @param clazz
211   *          the Java data type to serialize
212   * @return the serializer instance
213   * @throws NullPointerException
214   *           if any of the provided arguments, except the configuration, are
215   *           {@code null}
216   * @throws IllegalArgumentException
217   *           if the provided class is not bound to a Module assembly or field
218   * @throws UnsupportedOperationException
219   *           if the requested format is not supported by the implementation
220   * @see #getClassBinding(Class)
221   */
222  @NonNull
223  <CLASS> ISerializer<CLASS> newSerializer(@NonNull Format format, @NonNull Class<CLASS> clazz);
224
225  /**
226   * Gets a data {@link IDeserializer} which can be used to read Java instance
227   * data for the provided class from the requested format. The provided class
228   * must be a bound Java class with a {@link MetaschemaAssembly} or
229   * {@link MetaschemaField} annotation for which a {@link IClassBinding} exists.
230   *
231   * @param <CLASS>
232   *          the Java type this deserializer can read data into
233   * @param format
234   *          the format to serialize into
235   * @param clazz
236   *          the Java data type to serialize
237   * @return the deserializer instance
238   * @throws NullPointerException
239   *           if any of the provided arguments, except the configuration, are
240   *           {@code null}
241   * @throws IllegalArgumentException
242   *           if the provided class is not bound to a Module assembly or field
243   * @throws UnsupportedOperationException
244   *           if the requested format is not supported by the implementation
245   * @see #getClassBinding(Class)
246   */
247  @NonNull
248  <CLASS> IDeserializer<CLASS> newDeserializer(@NonNull Format format, @NonNull Class<CLASS> clazz);
249
250  /**
251   * Get a new {@link IBoundLoader} instance.
252   *
253   * @return the instance
254   */
255  @NonNull
256  IBoundLoader newBoundLoader();
257
258  /**
259   * Create a deep copy of the provided bound object.
260   *
261   * @param <CLASS>
262   *          the bound object type
263   * @param other
264   *          the object to copy
265   * @param parentInstance
266   *          the object's parent or {@code null}
267   * @return a deep copy of the provided object
268   * @throws BindingException
269   *           if an error occurred copying content between java instances
270   * @throws NullPointerException
271   *           if the provided object is {@code null}
272   * @throws IllegalArgumentException
273   *           if the provided class is not bound to a Module assembly or field
274   */
275  @NonNull
276  <CLASS> CLASS copyBoundObject(@NonNull CLASS other, Object parentInstance) throws BindingException;
277
278  /**
279   * Get a new single use constraint validator.
280   *
281   * @param handler
282   *          the validation handler to use to process the validation results
283   *
284   * @return the validator
285   */
286  default IConstraintValidator newValidator(@NonNull IConstraintValidationHandler handler) {
287    IBoundLoader loader = newBoundLoader();
288    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
289
290    DynamicContext context = StaticContext.newInstance().newDynamicContext();
291    context.setDocumentLoader(loader);
292
293    return new DefaultConstraintValidator(context, handler);
294  }
295
296  /**
297   * Perform constraint validation on the provided bound object represented as an
298   * {@link INodeItem}.
299   *
300   * @param nodeItem
301   *          the node item to validate
302   * @return the validation result
303   * @throws IllegalArgumentException
304   *           if the provided class is not bound to a Module assembly or field
305   */
306  default IValidationResult validate(@NonNull INodeItem nodeItem) {
307    FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
308    IConstraintValidator validator = newValidator(handler);
309    validator.validate(nodeItem);
310    validator.finalizeValidation();
311    return handler;
312  }
313
314  /**
315   * Load and perform schema and constraint validation on the target. The
316   * constraint validation will only be performed if the schema validation passes.
317   *
318   * @param target
319   *          the target to validate
320   * @param asFormat
321   *          the schema format to use to validate the target
322   * @param schemaProvider
323   *          provides callbacks to get the appropriate schemas
324   * @return the validation result
325   * @throws IOException
326   *           if an error occurred while reading the target
327   * @throws SAXException
328   *           if an error occurred when parsing the target as XML
329   */
330  default IValidationResult validate(
331      @NonNull Path target,
332      @NonNull Format asFormat,
333      @NonNull IValidationSchemaProvider schemaProvider) throws IOException, SAXException {
334    IValidationResult retval;
335    switch (asFormat) {
336    case JSON:
337      retval = new JsonSchemaContentValidator(schemaProvider.getJsonSchema()).validate(target);
338      break;
339    case XML:
340      List<Source> schemaSources = schemaProvider.getXmlSchemas();
341      retval = new XmlSchemaContentValidator(schemaSources).validate(target);
342      break;
343    case YAML:
344      JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
345      assert json != null;
346      retval = new JsonSchemaContentValidator(schemaProvider.getJsonSchema())
347          .validate(json, ObjectUtils.notNull(target.toUri()));
348      break;
349    default:
350      throw new UnsupportedOperationException("Unsupported format: " + asFormat.name());
351    }
352
353    if (retval.isPassing()) {
354      IValidationResult constraintValidationResult = validateWithConstraints(target);
355      retval = AggregateValidationResult.aggregate(retval, constraintValidationResult);
356    }
357    return retval;
358  }
359
360  /**
361   * Load and validate the provided {@code target} using the associated Module
362   * module constraints.
363   *
364   * @param target
365   *          the file to load and validate
366   * @return the validation results
367   * @throws IOException
368   *           if an error occurred while loading the document
369   */
370  default IValidationResult validateWithConstraints(@NonNull Path target) throws IOException {
371    IBoundLoader loader = newBoundLoader();
372    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
373
374    DynamicContext dynamicContext = StaticContext.newInstance().newDynamicContext();
375    dynamicContext.setDocumentLoader(loader);
376    IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);
377
378    return validate(nodeItem);
379  }
380
381  interface IValidationSchemaProvider {
382    /**
383     * Get a JSON schema to use for content validation.
384     *
385     * @return the JSON schema
386     * @throws IOException
387     *           if an error occurred while loading the schema
388     */
389    @NonNull
390    JSONObject getJsonSchema() throws IOException;
391
392    /**
393     * Get a XML schema to use for content validation.
394     *
395     * @return the XML schema sources
396     * @throws IOException
397     *           if an error occurred while loading the schema
398     */
399    @NonNull
400    List<Source> getXmlSchemas() throws IOException;
401  }
402
403}