ConstraintFactory.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.model;

import gov.nist.secauto.metaschema.core.datatype.DataTypeService;
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.MetapathExpression;
import gov.nist.secauto.metaschema.core.model.constraint.AbstractConstraint.AbstractConstraintBuilder;
import gov.nist.secauto.metaschema.core.model.constraint.AbstractKeyConstraint.AbstractKeyConstraintBuilder;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultAllowedValue;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultAllowedValuesConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultCardinalityConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultExpectConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultIndexConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultIndexHasKeyConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultKeyField;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultMatchesConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultUniqueConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraint;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.ISource;
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.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.NullJavaTypeAdapter;
import gov.nist.secauto.metaschema.databind.model.annotations.Property;

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

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import javax.xml.namespace.QName;

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

final class ConstraintFactory {
  private static final Logger LOGGER = LogManager.getLogger(ConstraintFactory.class);

  private ConstraintFactory() {
    // disable
  }

  static MarkupMultiline toRemarks(@NonNull String remarks) {
    return remarks.isBlank() ? null : MarkupMultiline.fromMarkdown(remarks);
  }

  @NonNull
  static MetapathExpression toMetapath(@NonNull String metapath) {
    String path = metapath;
    if (path.startsWith("/")) {
      String newPath = "." + path;

      if (LOGGER.isInfoEnabled()) {
        StringBuilder builder = new StringBuilder(80)
            .append("The path '")
            .append(path)
            .append("' is not properly contextualized using '.'. Using '")
            .append(newPath)
            .append("' instead.");
        LOGGER.atInfo().log(builder.toString());
      }
      path = newPath;
    }

    return path.isBlank() ? IConstraint.DEFAULT_TARGET : MetapathExpression.compile(path);
  }

  @NonNull
  static <T extends AbstractConstraintBuilder<T, ?>> T applyId(@NonNull T builder, @NonNull String id) {
    if (!id.isBlank()) {
      builder.identifier(id);
    }
    return builder;
  }

  @NonNull
  static <T extends AbstractConstraintBuilder<T, ?>> T applyFormalName(@NonNull T builder, @NonNull String name) {
    if (!name.isBlank()) {
      builder.formalName(name);
    }
    return builder;
  }

  @NonNull
  static <T extends AbstractConstraintBuilder<T, ?>> T applyDescription(@NonNull T builder, @NonNull String value) {
    if (!value.isBlank()) {
      builder.description(MarkupLine.fromMarkdown(value));
    }
    return builder;
  }

  @NonNull
  static <T extends AbstractConstraintBuilder<T, ?>> T applyTarget(@NonNull T builder, @NonNull String target) {
    builder.target(toMetapath(target));
    return builder;
  }

  @NonNull
  static <T extends AbstractConstraintBuilder<T, ?>> T applyProperties(
      @NonNull T builder,
      @Nullable Property... properties) {
    if (properties != null) {
      for (Property property : properties) {
        String name = property.name();
        String namespace = property.namespace();
        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // ok
        QName qname = new QName(namespace, name);

        String[] values = property.values();
        List<String> valueList = Arrays.asList(values);
        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // ok
        Set<String> valueSet = new LinkedHashSet<>(valueList);
        builder.property(qname, valueSet);
      }
    }
    return builder;
  }

  static <T extends AbstractConstraintBuilder<T, ?>> T applyRemarks(@NonNull T builder, @NonNull String remarks) {
    if (!remarks.isBlank()) {
      builder.remarks(MarkupMultiline.fromMarkdown(remarks));
    }
    return builder;
  }

  @NonNull
  static DefaultAllowedValuesConstraint.Builder applyAllowedValues(
      @NonNull DefaultAllowedValuesConstraint.Builder builder,
      @NonNull AllowedValues constraint) {
    for (AllowedValue value : constraint.values()) {
      DefaultAllowedValue allowedValue
          = new DefaultAllowedValue(value.value(), MarkupLine.fromMarkdown(value.description())); // NOPMD - intentional
      builder.allowedValue(allowedValue);
    }
    return builder;
  }

  @Nullable
  static Pattern toPattern(@NonNull String pattern) {
    return pattern.isBlank() ? null : Pattern.compile(pattern);
  }

  @Nullable
  static String toMessage(@NonNull String message) {
    return message.isBlank() ? null : message;
  }

  @Nullable
  static IDataTypeAdapter<?> toDataType(@NonNull Class<? extends IDataTypeAdapter<?>> adapterClass) {
    return adapterClass.isAssignableFrom(NullJavaTypeAdapter.class) ? null
        : DataTypeService.getInstance().getJavaTypeAdapterByClass(adapterClass);
  }

  @NonNull
  static DefaultAllowedValuesConstraint newAllowedValuesConstraint(
      @NonNull AllowedValues constraint,
      @NonNull ISource source) {
    DefaultAllowedValuesConstraint.Builder builder = DefaultAllowedValuesConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    applyAllowedValues(builder, constraint);
    builder.allowedOther(constraint.allowOthers());
    builder.extensible(constraint.extensible());

    return builder.build();
  }

  @NonNull
  static DefaultMatchesConstraint newMatchesConstraint(Matches constraint, @NonNull ISource source) {
    DefaultMatchesConstraint.Builder builder = DefaultMatchesConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    Pattern pattern = toPattern(constraint.pattern());
    if (pattern != null) {
      builder.regex(pattern);
    }

    IDataTypeAdapter<?> dataType = toDataType(constraint.typeAdapter());
    if (dataType != null) {
      builder.datatype(dataType);
    }

    return builder.build();
  }

  @NonNull
  static <T extends AbstractKeyConstraintBuilder<T, ?>> T applyKeyFields(@NonNull T builder,
      @NonNull KeyField... keyFields) {
    for (KeyField keyField : keyFields) {
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // ok
      DefaultKeyField field = new DefaultKeyField(
          toMetapath(keyField.target()),
          toPattern(keyField.pattern()),
          toRemarks(keyField.remarks()));
      builder.keyField(field);
    }
    return builder;
  }

  @NonNull
  static DefaultUniqueConstraint newUniqueConstraint(@NonNull IsUnique constraint, @NonNull ISource source) {
    DefaultUniqueConstraint.Builder builder = DefaultUniqueConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    applyKeyFields(builder, constraint.keyFields());

    return builder.build();
  }

  @NonNull
  static DefaultIndexConstraint newIndexConstraint(@NonNull Index constraint, @NonNull ISource source) {
    DefaultIndexConstraint.Builder builder = DefaultIndexConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    builder.name(constraint.name());
    applyKeyFields(builder, constraint.keyFields());

    return builder.build();
  }

  @NonNull
  static DefaultIndexHasKeyConstraint newIndexHasKeyConstraint(@NonNull IndexHasKey constraint,
      @NonNull ISource source) {
    DefaultIndexHasKeyConstraint.Builder builder = DefaultIndexHasKeyConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    builder.name(constraint.indexName());
    applyKeyFields(builder, constraint.keyFields());

    return builder.build();
  }

  @NonNull
  static DefaultExpectConstraint newExpectConstraint(@NonNull Expect constraint, @NonNull ISource source) {
    DefaultExpectConstraint.Builder builder = DefaultExpectConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    builder.test(toMetapath(constraint.test()));

    String message = constraint.message();
    if (!message.isBlank()) {
      builder.message(message);
    }

    return builder.build();
  }

  @Nullable
  static Integer toCardinality(int value) {
    return value < 0 ? null : value;
  }

  @NonNull
  static DefaultCardinalityConstraint newCardinalityConstraint(@NonNull HasCardinality constraint,
      @NonNull ISource source) {
    DefaultCardinalityConstraint.Builder builder = DefaultCardinalityConstraint.builder();
    applyId(builder, constraint.id());
    applyFormalName(builder, constraint.formalName());
    applyDescription(builder, constraint.description());
    builder
        .source(source)
        .level(constraint.level());
    applyTarget(builder, constraint.target());
    applyProperties(builder, constraint.properties());
    applyRemarks(builder, constraint.remarks());

    Integer min = toCardinality(constraint.minOccurs());
    if (min != null) {
      builder.minOccurs(min);
    }
    Integer max = toCardinality(constraint.maxOccurs());
    if (max != null) {
      builder.maxOccurs(max);
    }

    return builder.build();
  }
}