MetadataField.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License
 * at:
 *
 *   http://opensource.org/licenses/ecl2.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package org.opencastproject.metadata.dublincore;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Map;
import java.util.TimeZone;

/**
 * This is a generic and very abstract view of a certain field/property in a metadata catalog. The main purpose of this
 * class is to have a generic access to the variety of information stored in metadata catalogs.
 */
public class MetadataField {

  private static final Logger logger = LoggerFactory.getLogger(MetadataField.class);

  /** Keys for the different values in the configuration file */
  public static final String CONFIG_COLLECTION_ID_KEY = "collectionID";
  private static final String CONFIG_PATTERN_KEY = "pattern";
  private static final String CONFIG_DELIMITER_KEY = "delimiter";
  public static final String CONFIG_INPUT_ID_KEY = "inputID";
  public static final String CONFIG_LABEL_KEY = "label";
  public static final String CONFIG_LIST_PROVIDER_KEY = "listprovider";
  private static final String CONFIG_NAMESPACE_KEY = "namespace";
  private static final String CONFIG_ORDER_KEY = "order";
  private static final String CONFIG_OUTPUT_ID_KEY = "outputID";
  public static final String CONFIG_PROPERTY_PREFIX = "property";
  public static final String CONFIG_READ_ONLY_KEY = "readOnly";
  public static final String CONFIG_REQUIRED_KEY = "required";
  public static final String CONFIG_TYPE_KEY = "type";

  /**
   * Possible types for the metadata field. The types are used in the frontend and backend to know how the metadata
   * fields should be formatted (if needed).
   */
  public enum Type {
    BOOLEAN, DATE, DURATION, ITERABLE_TEXT, MIXED_TEXT, ORDERED_TEXT, LONG, START_DATE, START_TIME, TEXT, TEXT_LONG
  }

  /** The id of a collection to validate values against. */
  private String collectionID;
  /** The format to use for temporal date properties. */
  private String pattern;
  /** The delimiter used to display and parse list values. */
  private String delimiter;
  /** The id of the field used to identify it in the dublin core. */
  private final String inputID;
  /** The i18n id for the label to show the property. */
  private final String label;
  /** The provider to populate the property with. */
  private final String listprovider;
  /** The optional namespace of the field used if a field can be found in more than one namespace */
  private final String namespace;
  /**
   * In the order of properties where this property should be oriented in the UI i.e. 0 means the property should come
   * first, 1 means it should come second etc.
   */
  private final Integer order;
  /** The optional id of the field used to output for the ui, if not present will assume the same as the inputID. */
  private final String outputID;
  /** Whether the property should not be edited. */
  private boolean readOnly;
  /** Whether the property is required to update the metadata. */
  private final boolean required;
  /** The type of the metadata for example text, date etc. */
  private Type type;

  private Object value;
  private Boolean translatable;
  private boolean updated = false;
  private Map<String, String> collection;

  // this can only be true if the metadata field is representing multiple events with different values
  private Boolean hasDifferentValues = null;

  /**
   * Copy constructor
   *
   * @param other
   *          Other metadata field
   */
  public MetadataField(final MetadataField other) {
    this.inputID = other.inputID;
    this.outputID = other.outputID;
    this.label = other.label;
    this.readOnly = other.readOnly;
    this.required = other.required;
    this.value = other.value;
    this.translatable = other.translatable;
    this.hasDifferentValues = other.hasDifferentValues;
    this.type = other.type;
    this.collection = other.collection;
    this.collectionID = other.collectionID;
    this.order = other.order;
    this.namespace = other.namespace;
    this.updated = other.updated;
    this.pattern = other.pattern;
    this.delimiter = other.delimiter;
    this.listprovider = other.listprovider;
  }

  /**
   * Metadata field constructor
   *
   * @param inputID
   *          The identifier of the new metadata field
   * @param label
   *          the label of the field. The string displayed next to the field value on the frontend. This is usually be a
   *          translation key
   * @param readOnly
   *          Define if the new metadata field can be or not edited
   * @param required
   *          Define if the new metadata field is or not required
   * @param value
   *          The metadata field value
   * @param type
   *          The metadata field type @ EventMetadata.Type}
   * @param collection
   *          If the field has a limited list of possible value, the option should contain this one. Otherwise it should
   *          be none. This is also possible to use the collectionId parameter for that.
   * @param collectionID
   *          The id of the limit list of possible value that should be get through the resource endpoint.
   * @param listprovider An optional list provider ID
   * @param pattern Pattern for time/date fields
   * @param delimiter Delimiter
   * @throws IllegalArgumentException
   *           if the id, label, type parameters is/are null
   */
  public MetadataField(
          final String inputID,
          final String outputID,
          final String label,
          final boolean readOnly,
          final boolean required,
          final Object value,
          final Boolean translatable,
          final Type type,
          final Map<String, String> collection,
          final String collectionID,
          final Integer order,
          final String namespace,
          final String listprovider,
          final String pattern,
          final String delimiter) throws IllegalArgumentException {
    if (StringUtils.isBlank(inputID)) {
      throw new IllegalArgumentException("The metadata input id must not be null.");
    }
    if (StringUtils.isBlank(label)) {
      throw new IllegalArgumentException("The metadata label must not be null.");
    }
    if (type == null) {
      throw new IllegalArgumentException("The metadata type must not be null.");
    }
    this.inputID = inputID;
    this.outputID = outputID;
    this.label = label;
    this.readOnly = readOnly;
    this.required = required;
    this.value = value;
    this.translatable = translatable;
    this.type = type;
    this.collection = collection;
    this.collectionID = collectionID;
    this.order = order;
    this.namespace = namespace;
    this.listprovider = listprovider;
    this.pattern = pattern;
    this.delimiter = delimiter;
  }

  /**
   * Set the option of a limited list of possible values.
   *
   * @param collection
   *          The option of a limited list of possible values
   */
  public void setCollection(final Map<String, String> collection) {
    this.collection = collection;
  }

  public Map<String, String> getCollection() {
    return collection;
  }

  public Object getValue() {
    return value;
  }

  public Boolean isTranslatable() {
    return translatable;
  }

  public boolean isUpdated() {
    return updated;
  }

  public void setValue(final Object value) {
    setValue(value, true);
  }

  public void setValue(final Object value, final boolean setUpdated) {
    this.value = value;

    if (setUpdated) {
      this.updated = true;
    }
  }

  public void setIsTranslatable(final Boolean translatable) {
    this.translatable = translatable;
  }

  public static SimpleDateFormat getSimpleDateFormatter(final String pattern) {
    final SimpleDateFormat dateFormat;
    if (StringUtils.isNotBlank(pattern)) {
      dateFormat = new SimpleDateFormat(pattern);
    } else {
      dateFormat = new SimpleDateFormat();
    }
    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    return dateFormat;
  }

  public static MetadataField createMetadataField(final Map<String,String> configuration) {
    final String inputID = configuration.get(CONFIG_INPUT_ID_KEY);
    final String label = configuration.get(CONFIG_LABEL_KEY);

    final String collectionID = configuration.get(CONFIG_COLLECTION_ID_KEY);
    final String delimiter = configuration.get(CONFIG_DELIMITER_KEY);
    final String outputID = configuration.get(CONFIG_OUTPUT_ID_KEY);
    final String listprovider = configuration.get(CONFIG_LIST_PROVIDER_KEY);
    final String namespace = configuration.get(CONFIG_NAMESPACE_KEY);

    final Type type = configuration.containsKey(CONFIG_TYPE_KEY)
            ? Type.valueOf(configuration.get(CONFIG_TYPE_KEY).toUpperCase()) : null;
    final boolean required = configuration.containsKey(CONFIG_REQUIRED_KEY) && Boolean
            .parseBoolean(configuration.get(CONFIG_REQUIRED_KEY).toUpperCase());
    final boolean readOnly = configuration.containsKey(CONFIG_READ_ONLY_KEY) && Boolean
            .parseBoolean(configuration.get(CONFIG_READ_ONLY_KEY).toUpperCase());

    final String pattern = configuration.getOrDefault(CONFIG_PATTERN_KEY, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

    Integer order = null;
    if (configuration.containsKey(CONFIG_ORDER_KEY)) {
      try {
        order = Integer.parseInt(configuration.get(CONFIG_ORDER_KEY));
      } catch (final NumberFormatException e) {
        logger.warn("Unable to parse order value {} of metadata field {}", configuration.get(CONFIG_ORDER_KEY),
                inputID, e);
      }
    }

    if (type == null) {
      throw new IllegalArgumentException("type is null");
    }

    switch (type) {
      case BOOLEAN:
        return new MetadataField(
                inputID,
                outputID,
                label,
                readOnly,
                required,
                null,
                null,
                type,
                null,
                null,
                order,
                namespace,
                listprovider,
                null,
                null);
      case DATE:
        return new MetadataField(
                inputID,
                outputID,
                label,
                readOnly,
                required,
                null,
                null,
                type,
                null,
                null,
                order,
                namespace,
                listprovider,
                StringUtils.isNotBlank(pattern) ? pattern : null,
                null);
      case DURATION:
      case TEXT:
      case ORDERED_TEXT:
      case TEXT_LONG:
        return new MetadataField(
                inputID,
                outputID,
                label,
                readOnly,
                required,
                "",
                null,
                type,
                null,
                collectionID,
                order,
                namespace,
                listprovider,
                null,
                null);
      case ITERABLE_TEXT:
      case MIXED_TEXT:
        return new MetadataField(
                inputID,
                outputID,
                label,
                readOnly,
                required,
                new ArrayList<>(),
                null,
                type,
                null,
                collectionID,
                order,
                namespace,
                listprovider,
                null,
                delimiter);
      case LONG:
        return new MetadataField(
                inputID,
                outputID,
                label,
                readOnly,
                required,
                0L,
                null,
                Type.LONG,
                null,
                collectionID,
                order,
                namespace,
                listprovider,
                null, null);
      case START_DATE:
      case START_TIME:
        if (StringUtils.isBlank(pattern)) {
          throw new IllegalArgumentException(
                  "For temporal metadata field " + inputID + " of type " + type + " there needs to be a pattern.");
        }

        return new MetadataField(
                inputID,
                outputID,
                label,
                readOnly,
                required,
                null,
                null,
                type,
                null,
                null,
                order,
                namespace,
                listprovider,
                pattern,
                null);
      default:
        throw new IllegalArgumentException("Unknown metadata type! " + type);
    }
  }

  public String getCollectionID() {
    return collectionID;
  }

  public void setCollectionID(final String collectionID) {
    this.collectionID = collectionID;
  }

  public String getInputID() {
    return inputID;
  }

  public String getLabel() {
    return label;
  }

  public String getListprovider() {
    return listprovider;
  }

  public String getNamespace() {
    return namespace;
  }

  public Integer getOrder() {
    return order;
  }

  /**
   * @return The outputID if available, inputID if it is missing.
   */
  public String getOutputID() {
    if (outputID != null) {
      return outputID;
    }
    return inputID;
  }

  public String getPattern() {
    return pattern;
  }

  public void setPattern(final String pattern) {
    this.pattern = pattern;
  }

  public String getDelimiter() {
    return delimiter;
  }

  public void setDelimiter(final String delimiter) {
    this.delimiter = delimiter;
  }

  public void setReadOnly(final boolean readOnly) {
    this.readOnly = readOnly;
  }

  public boolean isReadOnly() {
    return readOnly;
  }

  public boolean isRequired() {
    return required;
  }

  public void setUpdated(final boolean updated) {
    this.updated = updated;
  }

  public Type getType() {
    return type;
  }

  public void setType(final Type type) {
    this.type = type;
  }

  public void setDifferentValues() {
    value = null;
    hasDifferentValues = true;
  }

  public Boolean hasDifferentValues() {
    return hasDifferentValues;
  }

  public MetadataField readOnlyCopy() {
    final MetadataField metadataField = new MetadataField(this);
    metadataField.setReadOnly(true);
    return metadataField;
  }
}