View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.databind;
28  
29  import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
30  import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
31  import gov.nist.secauto.metaschema.core.metapath.StaticContext;
32  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
33  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
34  import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
35  import gov.nist.secauto.metaschema.core.model.IFlagContainer;
36  import gov.nist.secauto.metaschema.core.model.IModule;
37  import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
38  import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
39  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
40  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
41  import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
42  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
43  import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
44  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
45  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
46  import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
47  import gov.nist.secauto.metaschema.databind.io.BindingException;
48  import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
49  import gov.nist.secauto.metaschema.databind.io.Format;
50  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
51  import gov.nist.secauto.metaschema.databind.io.IDeserializer;
52  import gov.nist.secauto.metaschema.databind.io.ISerializer;
53  import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
54  import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
55  import gov.nist.secauto.metaschema.databind.model.IClassBinding;
56  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
57  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
58  
59  import org.json.JSONObject;
60  import org.xml.sax.SAXException;
61  
62  import java.io.IOException;
63  import java.math.BigInteger;
64  import java.nio.file.Files;
65  import java.nio.file.Path;
66  import java.time.ZonedDateTime;
67  import java.util.List;
68  
69  import javax.xml.namespace.QName;
70  import javax.xml.transform.Source;
71  
72  import edu.umd.cs.findbugs.annotations.NonNull;
73  import edu.umd.cs.findbugs.annotations.Nullable;
74  
75  /**
76   * Provides information supporting a binding between a set of Module models and
77   * corresponding Java classes.
78   */
79  public interface IBindingContext extends IModuleLoaderStrategy {
80  
81    /**
82     * Get the singleton {@link IBindingContext} instance, which can be used to load
83     * information that binds a model to a set of Java classes.
84     *
85     * @return a new binding context
86     */
87    @NonNull
88    static IBindingContext instance() {
89      return DefaultBindingContext.instance();
90    }
91  
92    /**
93     * Register a matcher used to identify a bound class by the content's root name.
94     *
95     * @param matcher
96     *          the matcher implementation
97     * @return this instance
98     */
99    @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 }