AnnotationGenerator.java

/*
 * Portions of this software was developed by employees of the National Institute
 * of Standards and Technology (NIST), an agency of the Federal Government and is
 * being made available as a public service. Pursuant to title 17 United States
 * Code Section 105, works of NIST employees are not subject to copyright
 * protection in the United States. This software may be subject to foreign
 * copyright. Permission in the United States and in foreign countries, to the
 * extent that NIST may hold copyright, to use, copy, modify, create derivative
 * works, and distribute this software and its documentation without fee is hereby
 * granted on a non-exclusive basis, provided that this notice and disclaimer
 * of warranty appears in all copies.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
 */

package gov.nist.secauto.metaschema.databind.codegen;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.AnnotationSpec.Builder;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.TypeSpec;

import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.ISequence;
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.core.metapath.StaticContext;
import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
import gov.nist.secauto.metaschema.core.model.IFlagContainer;
import gov.nist.secauto.metaschema.core.model.INamedInstance;
import gov.nist.secauto.metaschema.core.model.INamedModelInstance;
import gov.nist.secauto.metaschema.core.model.IValuedDefinition;
import gov.nist.secauto.metaschema.core.model.constraint.IAllowedValue;
import gov.nist.secauto.metaschema.core.model.constraint.IAllowedValuesConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.ICardinalityConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IExpectConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IIndexConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IIndexHasKeyConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IKeyField;
import gov.nist.secauto.metaschema.core.model.constraint.IMatchesConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IUniqueConstraint;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.model.annotations.AllowedValue;
import gov.nist.secauto.metaschema.databind.model.annotations.AllowedValues;
import gov.nist.secauto.metaschema.databind.model.annotations.AssemblyConstraints;
import gov.nist.secauto.metaschema.databind.model.annotations.Expect;
import gov.nist.secauto.metaschema.databind.model.annotations.HasCardinality;
import gov.nist.secauto.metaschema.databind.model.annotations.Index;
import gov.nist.secauto.metaschema.databind.model.annotations.IndexHasKey;
import gov.nist.secauto.metaschema.databind.model.annotations.IsUnique;
import gov.nist.secauto.metaschema.databind.model.annotations.KeyField;
import gov.nist.secauto.metaschema.databind.model.annotations.Matches;
import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints;

import org.apache.logging.log4j.LogBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.lang.reflect.Method;
import java.util.List;
import java.util.regex.Pattern;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
 * A variety of utility functions for creating Module annotations.
 */
@SuppressWarnings({
    "PMD.GodClass" // utility class
})
final class AnnotationGenerator {
  private static final Logger LOGGER = LogManager.getLogger(AnnotationGenerator.class);

  private AnnotationGenerator() {
    // disable construction
  }

  public static Object getDefaultValue(Class<?> annotation, String member) {
    Method method;
    try {
      method = annotation.getDeclaredMethod(member);
    } catch (NoSuchMethodException ex) {
      throw new IllegalArgumentException(ex);
    }
    Object retval;
    try {
      retval = method.getDefaultValue();
    } catch (TypeNotPresentException ex) {
      retval = null; // NOPMD readability
    }
    return retval;
  }

  private static void buildConstraint(Class<?> annotationType, AnnotationSpec.Builder annotation,
      IConstraint constraint) {
    String id = constraint.getId();
    if (id != null) {
      annotation.addMember("id", "$S", id);
    }

    String formalName = constraint.getFormalName();
    if (formalName != null) {
      annotation.addMember("formalName", "$S", formalName);
    }

    MarkupLine description = constraint.getDescription();
    if (description != null) {
      annotation.addMember("description", "$S", description.toMarkdown());
    }

    annotation.addMember("level", "$T.$L", IConstraint.Level.class, constraint.getLevel());

    MetapathExpression target = constraint.getTarget();
    String path = target.getPath();
    if (!path.equals(getDefaultValue(annotationType, "target"))) {
      annotation.addMember("target", "$S", path);
    }
  }

  public static void buildValueConstraints(
      @NonNull FieldSpec.Builder builder,
      @NonNull IValuedDefinition definition) {
    if (!definition.getConstraints().isEmpty()) {
      AnnotationSpec.Builder valueConstraintsAnnotation = AnnotationSpec.builder(ValueConstraints.class);

      applyAllowedValuesConstraints(valueConstraintsAnnotation, definition.getAllowedValuesConstraints());
      applyIndexHasKeyConstraints(valueConstraintsAnnotation, definition.getIndexHasKeyConstraints());
      applyMatchesConstraints(valueConstraintsAnnotation, definition.getMatchesConstraints());
      applyExpectConstraints(valueConstraintsAnnotation, definition.getExpectConstraints());

      builder.addAnnotation(valueConstraintsAnnotation.build());
    }
  }

  public static void buildValueConstraints(
      @NonNull TypeSpec.Builder builder,
      @NonNull IFlagContainer definition) {

    List<? extends IAllowedValuesConstraint> allowedValues = definition.getAllowedValuesConstraints();
    List<? extends IIndexHasKeyConstraint> indexHasKey = definition.getIndexHasKeyConstraints();
    List<? extends IMatchesConstraint> matches = definition.getMatchesConstraints();
    List<? extends IExpectConstraint> expects = definition.getExpectConstraints();

    if (!allowedValues.isEmpty() || !indexHasKey.isEmpty() || !matches.isEmpty() || !expects.isEmpty()) {
      AnnotationSpec.Builder annotation = AnnotationSpec.builder(ValueConstraints.class);

      applyAllowedValuesConstraints(annotation, allowedValues);
      applyIndexHasKeyConstraints(annotation, indexHasKey);
      applyMatchesConstraints(annotation, matches);
      applyExpectConstraints(annotation, expects);

      builder.addAnnotation(annotation.build());
    }
  }

  public static void buildAssemblyConstraints(
      @NonNull TypeSpec.Builder builder,
      @NonNull IAssemblyDefinition definition) {

    List<? extends IIndexConstraint> index = definition.getIndexConstraints();
    List<? extends IUniqueConstraint> unique = definition.getUniqueConstraints();
    List<? extends ICardinalityConstraint> cardinality = definition.getHasCardinalityConstraints();

    if (!index.isEmpty() || !unique.isEmpty() || !cardinality.isEmpty()) {
      AnnotationSpec.Builder annotation = ObjectUtils.notNull(AnnotationSpec.builder(AssemblyConstraints.class));

      applyIndexConstraints(annotation, index);
      applyUniqueConstraints(annotation, unique);
      applyHasCardinalityConstraints(definition, annotation, cardinality);

      builder.addAnnotation(annotation.build());
    }
  }

  private static void applyAllowedValuesConstraints(AnnotationSpec.Builder annotation,
      List<? extends IAllowedValuesConstraint> constraints) {
    for (IAllowedValuesConstraint constraint : constraints) {
      AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(AllowedValues.class);
      buildConstraint(AllowedValues.class, constraintAnnotation, constraint);

      boolean isAllowedOther = constraint.isAllowedOther();
      if (!Boolean.valueOf(isAllowedOther).equals(getDefaultValue(AllowedValues.class, "allowOthers"))) {
        constraintAnnotation.addMember("allowOthers", "$L", isAllowedOther);
      }

      for (IAllowedValue value : constraint.getAllowedValues().values()) {
        AnnotationSpec.Builder valueAnnotation = AnnotationSpec.builder(AllowedValue.class);

        valueAnnotation.addMember("value", "$S", value.getValue());
        valueAnnotation.addMember("description", "$S", value.getDescription().toMarkdown());

        constraintAnnotation.addMember("values", "$L", valueAnnotation.build());
      }

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }
      annotation.addMember("allowedValues", "$L", constraintAnnotation.build());
    }
  }

  private static void applyIndexHasKeyConstraints(AnnotationSpec.Builder annotation,
      List<? extends IIndexHasKeyConstraint> constraints) {
    for (IIndexHasKeyConstraint constraint : constraints) {
      AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(IndexHasKey.class);
      buildConstraint(IndexHasKey.class, constraintAnnotation, constraint);

      constraintAnnotation.addMember("indexName", "$S", constraint.getIndexName());

      buildKeyFields(constraintAnnotation, constraint.getKeyFields());

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }

      annotation.addMember("indexHasKey", "$L", constraintAnnotation.build());
    }
  }

  private static void buildKeyFields(@NonNull Builder constraintAnnotation,
      @NonNull List<? extends IKeyField> keyFields) {
    for (IKeyField key : keyFields) {
      AnnotationSpec.Builder keyAnnotation = AnnotationSpec.builder(KeyField.class);

      MetapathExpression target = key.getTarget();
      String path = target.getPath();
      if (!path.equals(getDefaultValue(KeyField.class, "target"))) {
        keyAnnotation.addMember("target", "$S", path);
      }

      Pattern pattern = key.getPattern();
      if (pattern != null) {
        keyAnnotation.addMember("pattern", "$S", pattern.pattern());
      }

      MarkupMultiline remarks = key.getRemarks();
      if (remarks != null) {
        keyAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }

      constraintAnnotation.addMember("keyFields", "$L", keyAnnotation.build());
    }
  }

  private static void applyMatchesConstraints(AnnotationSpec.Builder annotation,
      List<? extends IMatchesConstraint> constraints) {
    for (IMatchesConstraint constraint : constraints) {
      AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Matches.class);
      buildConstraint(Matches.class, constraintAnnotation, constraint);

      Pattern pattern = constraint.getPattern();
      if (pattern != null) {
        constraintAnnotation.addMember("pattern", "$S", pattern.pattern());
      }

      IDataTypeAdapter<?> dataType = constraint.getDataType();
      if (dataType != null) {
        constraintAnnotation.addMember("typeAdapter", "$T.class", dataType.getClass());
      }

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }
      annotation.addMember("matches", "$L", constraintAnnotation.build());
    }
  }

  private static void applyExpectConstraints(AnnotationSpec.Builder annotation,
      List<? extends IExpectConstraint> constraints) {
    for (IExpectConstraint constraint : constraints) {
      AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Expect.class);

      buildConstraint(Expect.class, constraintAnnotation, constraint);

      MetapathExpression test = constraint.getTest();
      constraintAnnotation.addMember("test", "$S", test.getPath());

      if (constraint.getMessage() != null) {
        constraintAnnotation.addMember("message", "$S", constraint.getMessage());
      }

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }

      annotation.addMember("expect", "$L", constraintAnnotation.build());
    }
  }

  private static void applyIndexConstraints(AnnotationSpec.Builder annotation,
      List<? extends IIndexConstraint> constraints) {
    for (IIndexConstraint constraint : constraints) {
      AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Index.class);

      buildConstraint(Index.class, constraintAnnotation, constraint);

      constraintAnnotation.addMember("name", "$S", constraint.getName());

      buildKeyFields(constraintAnnotation, constraint.getKeyFields());

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }

      annotation.addMember("index", "$L", constraintAnnotation.build());
    }
  }

  private static void applyUniqueConstraints(AnnotationSpec.Builder annotation,
      List<? extends IUniqueConstraint> constraints) {
    for (IUniqueConstraint constraint : constraints) {
      AnnotationSpec.Builder constraintAnnotation = ObjectUtils.notNull(AnnotationSpec.builder(IsUnique.class));

      buildConstraint(IsUnique.class, constraintAnnotation, constraint);

      buildKeyFields(constraintAnnotation, constraint.getKeyFields());

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }

      annotation.addMember("isUnique", "$L", constraintAnnotation.build());
    }
  }

  @SuppressWarnings({
      "PMD.GuardLogStatement" // guarded in outer calls
  })
  private static void checkCardinalities(
      @NonNull IAssemblyDefinition definition,
      @NonNull ICardinalityConstraint constraint,
      @NonNull ISequence<? extends IDefinitionNodeItem<?, ?>> instanceSet,
      @NonNull LogBuilder logBuilder) {

    LogBuilder warn = LOGGER.atWarn();
    for (IDefinitionNodeItem<?, ?> item : instanceSet.asList()) {
      INamedInstance instance = item.getInstance();
      if (instance instanceof INamedModelInstance) {
        INamedModelInstance modelInstance = (INamedModelInstance) instance;

        checkMinOccurs(definition, constraint, modelInstance, logBuilder);
        checkMaxOccurs(definition, constraint, modelInstance, logBuilder);
      } else {
        warn.log(String.format(
            "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that is not a model instance",
            definition.getName(), constraint.getMinOccurs(), constraint.getTarget().getPath()));
      }
    }
  }

  @SuppressWarnings({
      "PMD.GuardLogStatement" // guarded in outer calls
  })
  private static void checkMinOccurs(
      @NonNull IAssemblyDefinition definition,
      @NonNull ICardinalityConstraint constraint,
      @NonNull INamedModelInstance modelInstance,
      @NonNull LogBuilder logBuilder) {
    Integer minOccurs = constraint.getMinOccurs();
    if (minOccurs != null) {
      if (minOccurs == modelInstance.getMinOccurs()) {
        logBuilder.log(String.format(
            "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that is redundant with a"
                + " targeted instance named '%s' that requires min-occurs=%d",
            definition.getName(), minOccurs, constraint.getTarget().getPath(),
            modelInstance.getName(),
            modelInstance.getMinOccurs()));
      } else if (minOccurs < modelInstance.getMinOccurs()) {
        logBuilder.log(String.format(
            "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that conflicts with a"
                + " targeted instance named '%s' that requires min-occurs=%d",
            definition.getName(), minOccurs, constraint.getTarget().getPath(),
            modelInstance.getName(),
            modelInstance.getMinOccurs()));
      }
    }
  }

  @SuppressWarnings({
      "PMD.GuardLogStatement" // guarded in outer calls
  })
  private static void checkMaxOccurs(
      @NonNull IAssemblyDefinition definition,
      @NonNull ICardinalityConstraint constraint,
      @NonNull INamedModelInstance modelInstance,
      @NonNull LogBuilder logBuilder) {
    Integer maxOccurs = constraint.getMaxOccurs();
    if (maxOccurs != null) {
      if (maxOccurs == modelInstance.getMaxOccurs()) {
        logBuilder.log(String.format(
            "Definition '%s' has max-occurs=%d cardinality constraint targeting '%s' that is redundant with a"
                + " targeted instance named '%s' that requires max-occurs=%d",
            definition.getName(), maxOccurs, constraint.getTarget().getPath(),
            modelInstance.getName(),
            modelInstance.getMaxOccurs()));
      } else if (maxOccurs < modelInstance.getMaxOccurs()) {
        logBuilder.log(String.format(
            "Definition '%s' has max-occurs=%d cardinality constraint targeting '%s' that conflicts with a"
                + " targeted instance named '%s' that requires max-occurs=%d",
            definition.getName(), maxOccurs, constraint.getTarget().getPath(),
            modelInstance.getName(),
            modelInstance.getMaxOccurs()));
      }
    }
  }

  private static void applyHasCardinalityConstraints(
      @NonNull IAssemblyDefinition definition,
      @NonNull AnnotationSpec.Builder annotation,
      @NonNull List<? extends ICardinalityConstraint> constraints) {

    DynamicContext dynamicContext = StaticContext.newInstance().newDynamicContext();
    dynamicContext.disablePredicateEvaluation();

    for (ICardinalityConstraint constraint : constraints) {

      IAssemblyNodeItem definitionNodeItem
          = INodeItemFactory.instance().newAssemblyNodeItem(definition);

      ISequence<? extends IDefinitionNodeItem<?, ?>> instanceSet
          = constraint.matchTargets(definitionNodeItem, dynamicContext);

      if (LOGGER.isWarnEnabled()) {
        checkCardinalities(definition, constraint, instanceSet, ObjectUtils.notNull(LOGGER.atWarn()));
      }

      AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(HasCardinality.class);

      buildConstraint(HasCardinality.class, constraintAnnotation, constraint);

      Integer minOccurs = constraint.getMinOccurs();
      if (minOccurs != null && !minOccurs.equals(getDefaultValue(HasCardinality.class, "minOccurs"))) {
        constraintAnnotation.addMember("minOccurs", "$L", minOccurs);
      }

      Integer maxOccurs = constraint.getMaxOccurs();
      if (maxOccurs != null && !maxOccurs.equals(getDefaultValue(HasCardinality.class, "maxOccurs"))) {
        constraintAnnotation.addMember("maxOccurs", "$L", maxOccurs);
      }

      annotation.addMember("hasCardinality", "$L", constraintAnnotation.build());

      MarkupMultiline remarks = constraint.getRemarks();
      if (remarks != null) {
        constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
      }
    }
  }
}