001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind;
007
008import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
009import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
010import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
011import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
012import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
013import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
014import gov.nist.secauto.metaschema.core.model.IBoundObject;
015import gov.nist.secauto.metaschema.core.model.IModule;
016import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
017import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
018import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
019import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
020import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
021import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
022import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
023import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
024import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
025import gov.nist.secauto.metaschema.core.util.ObjectUtils;
026import gov.nist.secauto.metaschema.databind.io.BindingException;
027import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
028import gov.nist.secauto.metaschema.databind.io.Format;
029import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
030import gov.nist.secauto.metaschema.databind.io.IDeserializer;
031import gov.nist.secauto.metaschema.databind.io.ISerializer;
032import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
033import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
034import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
035import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
036import gov.nist.secauto.metaschema.databind.model.IBoundModule;
037import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
038import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
039
040import org.json.JSONObject;
041import org.json.JSONTokener;
042import org.xml.sax.SAXException;
043
044import java.io.BufferedInputStream;
045import java.io.FileNotFoundException;
046import java.io.IOException;
047import java.io.InputStream;
048import java.math.BigInteger;
049import java.net.URI;
050import java.net.URL;
051import java.nio.file.Path;
052import java.time.ZonedDateTime;
053import java.util.List;
054
055import javax.xml.namespace.QName;
056import javax.xml.transform.Source;
057
058import edu.umd.cs.findbugs.annotations.NonNull;
059import edu.umd.cs.findbugs.annotations.Nullable;
060
061/**
062 * Provides information supporting a binding between a set of Module models and
063 * corresponding Java classes.
064 */
065public interface IBindingContext {
066
067  /**
068   * Get the singleton {@link IBindingContext} instance, which can be used to load
069   * information that binds a model to a set of Java classes.
070   *
071   * @return a new binding context
072   */
073  @NonNull
074  static IBindingContext instance() {
075    return DefaultBindingContext.instance();
076  }
077
078  /**
079   * Register a matcher used to identify a bound class by the definition's root
080   * name.
081   *
082   * @param definition
083   *          the definition to match for
084   * @return the matcher
085   */
086  @NonNull
087  IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition);
088
089  /**
090   * Register a matcher used to identify a bound class by the definition's root
091   * name.
092   *
093   * @param clazz
094   *          the definition class to match for, which must represent a root
095   *          assembly definition
096   * @return the matcher
097   */
098  @NonNull
099  IBindingMatcher registerBindingMatcher(@NonNull Class<? extends IBoundObject> clazz);
100
101  /**
102   * Register a class binding for a given bound class.
103   *
104   * @param definition
105   *          the bound class information to register
106   * @return the old bound class information or {@code null} if no binding existed
107   *         for the associated class
108   */
109  @Nullable
110  IBoundDefinitionModelComplex registerClassBinding(@NonNull IBoundDefinitionModelComplex definition);
111
112  /**
113   * Get the {@link IBoundDefinitionModel} instance associated with the provided
114   * Java class.
115   * <p>
116   * Typically the class will have a {@link MetaschemaAssembly} or
117   * {@link MetaschemaField} annotation.
118   *
119   * @param clazz
120   *          the class binding to load
121   * @return the associated class binding instance or {@code null} if the class is
122   *         not bound
123   */
124  @Nullable
125  IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz);
126
127  /**
128   * Determine the bound class for the provided XML {@link QName}.
129   *
130   * @param rootQName
131   *          the root XML element's QName
132   * @return the bound class or {@code null} if not recognized
133   * @see IBindingContext#registerBindingMatcher(Class)
134   */
135  @Nullable
136  Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName);
137
138  /**
139   * Determine the bound class for the provided JSON/YAML property/item name using
140   * any registered matchers.
141   *
142   * @param rootName
143   *          the JSON/YAML property/item name
144   * @return the bound class or {@code null} if not recognized
145   * @see IBindingContext#registerBindingMatcher(Class)
146   */
147  @Nullable
148  Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName);
149
150  /**
151   * Get's the {@link IDataTypeAdapter} associated with the specified Java class,
152   * which is used to read and write XML, JSON, and YAML data to and from
153   * instances of that class. Thus, this adapter supports a direct binding between
154   * the Java class and structured data in one of the supported formats. Adapters
155   * are used to support bindings for simple data objects (e.g., {@link String},
156   * {@link BigInteger}, {@link ZonedDateTime}, etc).
157   *
158   * @param <TYPE>
159   *          the class type of the adapter
160   * @param clazz
161   *          the Java {@link Class} for the bound type
162   * @return the adapter instance or {@code null} if the provided class is not
163   *         bound
164   */
165  @Nullable
166  <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz);
167
168  /**
169   * Load a bound Metaschema module implemented by the provided class.
170   * <p>
171   * Also registers any associated bound classes.
172   * <p>
173   * Implementations are expected to return the same IModule instance for multiple
174   * calls to this method with the same class argument.
175   *
176   * @param clazz
177   *          the class implementing a bound Metaschema module
178   * @return the loaded module
179   */
180  @NonNull
181  IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz);
182
183  /**
184   * Generate, compile, and load a set of generated Module annotated Java classes
185   * based on the provided Module {@code module}.
186   *
187   * @param module
188   *          the Module module to generate classes for
189   * @param compilePath
190   *          the path to the directory to generate classes in
191   * @return this instance
192   * @throws IOException
193   *           if an error occurred while generating or loading the classes
194   */
195  @NonNull
196  IBindingContext registerModule(
197      @NonNull IModule module,
198      @NonNull Path compilePath) throws IOException;
199
200  /**
201   * Gets a data {@link ISerializer} which can be used to write Java instance data
202   * for the provided class in the requested format.
203   * <p>
204   * The provided class must be a bound Java class with a
205   * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
206   * {@link IBoundDefinitionModel} exists.
207   *
208   * @param <CLASS>
209   *          the Java type this serializer can write data from
210   * @param format
211   *          the format to serialize into
212   * @param clazz
213   *          the Java data object to serialize
214   * @return the serializer instance
215   * @throws NullPointerException
216   *           if any of the provided arguments, except the configuration, are
217   *           {@code null}
218   * @throws IllegalArgumentException
219   *           if the provided class is not bound to a Module assembly or field
220   * @throws UnsupportedOperationException
221   *           if the requested format is not supported by the implementation
222   * @see #getBoundDefinitionForClass(Class)
223   */
224  @NonNull
225  <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
226      @NonNull Format format,
227      @NonNull Class<CLASS> clazz);
228
229  /**
230   * Gets a data {@link IDeserializer} which can be used to read Java instance
231   * data for the provided class from the requested format.
232   * <p>
233   * The provided class must be a bound Java class with a
234   * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
235   * {@link IBoundDefinitionModel} exists.
236   *
237   * @param <CLASS>
238   *          the Java type this deserializer can read data into
239   * @param format
240   *          the format to serialize into
241   * @param clazz
242   *          the Java data type to serialize
243   * @return the deserializer instance
244   * @throws NullPointerException
245   *           if any of the provided arguments, except the configuration, are
246   *           {@code null}
247   * @throws IllegalArgumentException
248   *           if the provided class is not bound to a Module assembly or field
249   * @throws UnsupportedOperationException
250   *           if the requested format is not supported by the implementation
251   * @see #getBoundDefinitionForClass(Class)
252   */
253  @NonNull
254  <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
255      @NonNull Format format,
256      @NonNull Class<CLASS> clazz);
257
258  /**
259   * Get a new {@link IBoundLoader} instance.
260   *
261   * @return the instance
262   */
263  @NonNull
264  IBoundLoader newBoundLoader();
265
266  /**
267   * Create a deep copy of the provided bound object.
268   *
269   * @param <CLASS>
270   *          the bound object type
271   * @param other
272   *          the object to copy
273   * @param parentInstance
274   *          the object's parent or {@code null}
275   * @return a deep copy of the provided object
276   * @throws BindingException
277   *           if an error occurred copying content between java instances
278   * @throws NullPointerException
279   *           if the provided object is {@code null}
280   * @throws IllegalArgumentException
281   *           if the provided class is not bound to a Module assembly or field
282   */
283  @NonNull
284  <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
285      throws BindingException;
286
287  /**
288   * Get a new single use constraint validator.
289   *
290   * @param handler
291   *          the validation handler to use to process the validation results
292   * @param config
293   *          the validation configuration
294   *
295   * @return the validator
296   */
297  default IConstraintValidator newValidator(
298      @NonNull IConstraintValidationHandler handler,
299      @Nullable IConfiguration<ValidationFeature<?>> config) {
300    IBoundLoader loader = newBoundLoader();
301    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
302
303    DynamicContext context = new DynamicContext();
304    context.setDocumentLoader(loader);
305
306    DefaultConstraintValidator retval = new DefaultConstraintValidator(handler);
307    if (config != null) {
308      retval.applyConfiguration(config);
309    }
310    return retval;
311  }
312
313  /**
314   * Perform constraint validation on the provided bound object represented as an
315   * {@link IDocumentNodeItem}.
316   *
317   * @param nodeItem
318   *          the node item to validate
319   * @param loader
320   *          a module loader used to load and resolve referenced resources
321   * @param config
322   *          the validation configuration
323   * @return the validation result
324   * @throws IllegalArgumentException
325   *           if the provided class is not bound to a Module assembly or field
326   */
327  default IValidationResult validate(
328      @NonNull IDocumentNodeItem nodeItem,
329      @NonNull IBoundLoader loader,
330      @Nullable IConfiguration<ValidationFeature<?>> config) {
331    IRootAssemblyNodeItem root = nodeItem.getRootAssemblyNodeItem();
332    return validate(root, loader, config);
333  }
334
335  /**
336   * Perform constraint validation on the provided bound object represented as an
337   * {@link IDefinitionNodeItem}.
338   *
339   * @param nodeItem
340   *          the node item to validate
341   * @param loader
342   *          a module loader used to load and resolve referenced resources
343   * @param config
344   *          the validation configuration
345   * @return the validation result
346   * @throws IllegalArgumentException
347   *           if the provided class is not bound to a Module assembly or field
348   */
349  default IValidationResult validate(
350      @NonNull IDefinitionNodeItem<?, ?> nodeItem,
351      @NonNull IBoundLoader loader,
352      @Nullable IConfiguration<ValidationFeature<?>> config) {
353
354    FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
355    IConstraintValidator validator = newValidator(handler, config);
356
357    DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext());
358    dynamicContext.setDocumentLoader(loader);
359
360    validator.validate(nodeItem, dynamicContext);
361    validator.finalizeValidation(dynamicContext);
362    return handler;
363  }
364
365  /**
366   * Load and perform schema and constraint validation on the target. The
367   * constraint validation will only be performed if the schema validation passes.
368   *
369   * @param target
370   *          the target to validate
371   * @param asFormat
372   *          the schema format to use to validate the target
373   * @param schemaProvider
374   *          provides callbacks to get the appropriate schemas
375   * @param config
376   *          the validation configuration
377   * @return the validation result
378   * @throws IOException
379   *           if an error occurred while reading the target
380   */
381  default IValidationResult validate(
382      @NonNull URI target,
383      @NonNull Format asFormat,
384      @NonNull ISchemaValidationProvider schemaProvider,
385      @Nullable IConfiguration<ValidationFeature<?>> config) throws IOException {
386
387    IValidationResult retval = schemaProvider.validateWithSchema(target, asFormat);
388
389    if (retval.isPassing()) {
390      IValidationResult constraintValidationResult = validateWithConstraints(target, config);
391      retval = AggregateValidationResult.aggregate(retval, constraintValidationResult);
392    }
393    return retval;
394  }
395
396  /**
397   * Load and validate the provided {@code target} using the associated Module
398   * module constraints.
399   *
400   * @param target
401   *          the file to load and validate
402   * @param config
403   *          the validation configuration
404   * @return the validation results
405   * @throws IOException
406   *           if an error occurred while parsing the target
407   */
408  default IValidationResult validateWithConstraints(
409      @NonNull URI target,
410      @Nullable IConfiguration<ValidationFeature<?>> config)
411      throws IOException {
412    IBoundLoader loader = newBoundLoader();
413    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
414    IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);
415
416    return validate(nodeItem, loader, config);
417  }
418
419  interface IModuleLoaderStrategy {
420    /**
421     * Load the bound Metaschema module represented by the provided class.
422     * <p>
423     * Implementations are allowed to return a cached instance if the module has
424     * already been loaded.
425     *
426     * @param clazz
427     *          the Module class
428     * @return the module
429     * @throws IllegalStateException
430     *           if an error occurred while processing the associated module
431     *           information
432     */
433    @NonNull
434    IBoundModule loadModule(@NonNull Class<? extends IBoundModule> clazz);
435
436    /**
437     * Get the {@link IBoundDefinitionModel} instance associated with the provided
438     * Java class.
439     * <p>
440     * Typically the class will have a {@link MetaschemaAssembly} or
441     * {@link MetaschemaField} annotation.
442     *
443     * @param clazz
444     *          the class binding to load
445     * @return the associated class binding instance or {@code null} if the class is
446     *         not bound
447     */
448    @Nullable
449    IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz);
450  }
451
452  interface ISchemaValidationProvider {
453
454    @NonNull
455    default IValidationResult validateWithSchema(@NonNull URI target, @NonNull Format asFormat)
456        throws FileNotFoundException, IOException {
457      URL targetResource = ObjectUtils.notNull(target.toURL());
458
459      IValidationResult retval;
460      switch (asFormat) {
461      case JSON: {
462        JSONObject json;
463        try (@SuppressWarnings("resource") InputStream is
464            = new BufferedInputStream(ObjectUtils.notNull(targetResource.openStream()))) {
465          json = new JSONObject(new JSONTokener(is));
466        }
467        retval = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, target);
468        break;
469      }
470      case XML:
471        try {
472          List<Source> schemaSources = getXmlSchemas(targetResource);
473          retval = new XmlSchemaContentValidator(schemaSources).validate(target);
474        } catch (SAXException ex) {
475          throw new IOException(ex);
476        }
477        break;
478      case YAML: {
479        JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
480        assert json != null;
481        retval = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, ObjectUtils.notNull(target));
482        break;
483      }
484      default:
485        throw new UnsupportedOperationException("Unsupported format: " + asFormat.name());
486      }
487      return retval;
488    }
489
490    /**
491     * Get a JSON schema to use for content validation.
492     *
493     * @param json
494     *          the JSON content to validate
495     *
496     * @return the JSON schema
497     * @throws IOException
498     *           if an error occurred while loading the schema
499     */
500    @NonNull
501    JSONObject getJsonSchema(@NonNull JSONObject json) throws IOException;
502
503    /**
504     * Get a XML schema to use for content validation.
505     *
506     * @param targetResource
507     *          the URL for the XML content to validate
508     *
509     * @return the XML schema sources
510     * @throws IOException
511     *           if an error occurred while loading the schema
512     */
513    @NonNull
514    List<Source> getXmlSchemas(@NonNull URL targetResource) throws IOException;
515  }
516
517  /**
518   * Implementations of this interface provide a means by which a bound class can
519   * be found that corresponds to an XML element, JSON property, or YAML item
520   * name.
521   */
522  interface IBindingMatcher {
523    @SuppressWarnings("PMD.ShortMethodName")
524    @NonNull
525    static IBindingMatcher of(IBoundDefinitionModelAssembly assembly) {
526      if (!assembly.isRoot()) {
527        throw new IllegalArgumentException(
528            String.format("The provided class '%s' is not a root assembly.", assembly.getBoundClass().getName()));
529      }
530      return new RootAssemblyBindingMatcher(assembly);
531    }
532
533    /**
534     * Determine the bound class for the provided XML {@link QName}.
535     *
536     * @param rootQName
537     *          the root XML element's QName
538     * @return the bound class for the XML qualified name or {@code null} if not
539     *         recognized
540     */
541    Class<? extends IBoundObject> getBoundClassForXmlQName(QName rootQName);
542
543    /**
544     * Determine the bound class for the provided JSON/YAML property/item name.
545     *
546     * @param rootName
547     *          the JSON/YAML property/item name
548     * @return the bound class for the JSON property name or {@code null} if not
549     *         recognized
550     */
551    Class<? extends IBoundObject> getBoundClassForJsonName(String rootName);
552  }
553}