DublinCoreMetadataCollection.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.opencastproject.list.api.DefaultResourceListQuery;
import org.opencastproject.list.api.ListProviderException;
import org.opencastproject.list.api.ListProvidersService;
import org.opencastproject.list.api.ResourceListQuery;

import com.google.common.collect.Iterables;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class DublinCoreMetadataCollection {
  private static final Logger logger = LoggerFactory.getLogger(DublinCoreMetadataCollection.class);

  /** The list containing all the metadata */
  private List<MetadataField> fieldsInOrder = new ArrayList<>();
  private final Map<String, MetadataField> outputFields = new HashMap<>();

  public DublinCoreMetadataCollection() {
    this(Collections.emptyList());
  }

  public DublinCoreMetadataCollection(final Iterable<MetadataField> fields) {
    for (final MetadataField field : fields) {
      addField(field);
    }
  }

  public DublinCoreMetadataCollection(final DublinCoreMetadataCollection c) {
    this(c.fieldsInOrder);
  }

  public DublinCoreMetadataCollection readOnlyCopy() {
    return new DublinCoreMetadataCollection(this.fieldsInOrder.stream().map(MetadataField::readOnlyCopy)
            .collect(Collectors.toList()));
  }

  public Map<String, MetadataField> getOutputFields() {
    return outputFields;
  }

  public void addField(final MetadataField metadata) {
    if (metadata == null) {
      throw new IllegalArgumentException("The metadata must not be null.");
    }
    addFieldInOrder(metadata);
    this.outputFields.put(metadata.getOutputID(), metadata);
  }

  /**
   * Adds a field in ui order to the collection. If no order is specified it will be added to the end.
   *
   * @param metadata
   *          The metadata to add to the collection.
   */
  private void addFieldInOrder(final MetadataField metadata) {
    removeFieldIfExists(metadata);

    // Find all of the ordered or unordered elements.
    final ArrayList<MetadataField> orderedFields = new ArrayList<>();
    final ArrayList<MetadataField> unorderedFields = new ArrayList<>();
    for (final MetadataField field : fieldsInOrder) {
      if (field.getOrder() != null) {
        orderedFields.add(field);
      } else {
        unorderedFields.add(field);
      }
    }

    // Add the new field to either the ordered fields or the unordered fields.
    if (metadata.getOrder() != null) {
      orderedFields.add(metadata);
    } else {
      unorderedFields.add(metadata);
    }

    // Sort the ordered elements so that early entries don't push later entries to the right
    orderedFields.sort(Comparator.comparingInt(MetadataField::getOrder));

    // Add all the non-ordered elements to the collection
    fieldsInOrder = new ArrayList<>(unorderedFields);

    // Add all of the fields that have an index to their location starting at the lowest value.
    for (final MetadataField orderedField : orderedFields) {
      final int index = orderedField.getOrder() < fieldsInOrder.size() ? orderedField.getOrder()
              : fieldsInOrder.size();
      fieldsInOrder.add(index, orderedField);
    }
  }

  public void addEmptyField(final MetadataField metadataField, final ListProvidersService listProvidersService) {
    addField(metadataField, Collections.emptyList(), Optional.empty(), listProvidersService);
  }

  public void addField(final MetadataField metadataField, final String value, final ListProvidersService
          listProvidersService) {
    addField(metadataField, Collections.singletonList(value), Optional.empty(), listProvidersService);
  }

  public void addField(final MetadataField metadataField, final Optional<String> valueOpt, final
      Optional<ResourceListQuery> collectionQueryOverrideOpt, final ListProvidersService listProvidersService) {
    if (valueOpt.isPresent()) {
      addField(metadataField, Collections.singletonList(valueOpt.get()), collectionQueryOverrideOpt,
              listProvidersService);
    } else {
      addField(metadataField, Collections.emptyList(), collectionQueryOverrideOpt, listProvidersService);
    }
  }

  /**
   * Set value to a metadata field of unknown type
   */
  public static void setValueFromDCCatalog(
          final List<String> filteredValues,
          final MetadataField metadataField) {
    if (filteredValues.isEmpty()) {
      throw new IllegalArgumentException("Values cannot be empty");
    }

    if (filteredValues.size() > 1
            && metadataField.getType() != MetadataField.Type.MIXED_TEXT
            && metadataField.getType() != MetadataField.Type.ITERABLE_TEXT) {
      logger.warn("Cannot put multiple values into a single-value field, only the last value is used. {}",
              Arrays.toString(filteredValues.toArray()));
    }

    switch (metadataField.getType()) {
      case BOOLEAN:
        metadataField.setValue(Boolean.parseBoolean(Iterables.getLast(filteredValues)), false);
        break;
      case DATE:
        if (metadataField.getPattern() == null) {
          metadataField.setPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        }
        metadataField.setValue(EncodingSchemeUtils.decodeDate(Iterables.getLast(filteredValues)), false);
        break;
      case DURATION:
        final String value = Iterables.getLast(filteredValues);
        final DCMIPeriod period = EncodingSchemeUtils.decodePeriod(value);
        if (period == null) {
          throw new IllegalArgumentException("period couldn't be parsed: " + value);
        }
        final long longValue = period.getEnd().getTime() - period.getStart().getTime();
        metadataField.setValue(Long.toString(longValue), false);
        break;
      case ITERABLE_TEXT:
      case MIXED_TEXT:
        metadataField.setValue(filteredValues, false);
        break;
      case LONG:
        metadataField.setValue(Long.parseLong(Iterables.getLast(filteredValues)), false);
        break;
      case START_DATE:
        if (metadataField.getPattern() == null) {
          metadataField.setPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        }
        metadataField.setValue(Iterables.getLast(filteredValues), false);
        break;
      case TEXT:
      case ORDERED_TEXT:
      case TEXT_LONG:
        metadataField.setValue(Iterables.getLast(filteredValues), false);
        break;
      default:
        throw new IllegalArgumentException("Unknown metadata type! " + metadataField.getType());
    }
  }

  public void addField(final MetadataField metadataField, final List<String> values, final ListProvidersService
          listProvidersService) {
    addField(metadataField, values, Optional.empty(), listProvidersService);
  }

  public void addField(final MetadataField metadataField, final List<String> values, final Optional<ResourceListQuery>
          collectionQueryOverrideOpt, final ListProvidersService listProvidersService) {
    final List<String> filteredValues = values.stream().filter(StringUtils::isNotBlank).collect(Collectors.toList());

    if (!filteredValues.isEmpty()) {
      setValueFromDCCatalog(filteredValues, metadataField);
    }

    metadataField.setIsTranslatable(getCollectionIsTranslatable(metadataField, listProvidersService));
    metadataField.setCollection(getCollection(metadataField, listProvidersService, collectionQueryOverrideOpt));

    addField(metadataField);
  }

  private static Boolean getCollectionIsTranslatable(
          final MetadataField metadataField,
          final ListProvidersService listProvidersService) {
    if (listProvidersService != null && metadataField.getListprovider() != null) {
      try {
        return listProvidersService.isTranslatable(metadataField.getListprovider());
      } catch (final ListProviderException ex) {
        // failed to get is-translatable property on list-provider-service
        // as this field is optional, it is fine to pass here
      }
    }
    return null;
  }

  private static Map<String, String> getCollection(final MetadataField metadataField, final ListProvidersService
          listProvidersService, final Optional<ResourceListQuery> collectionQueryOverrideOpt) {
    try {
      if (listProvidersService != null && metadataField.getListprovider() != null) {

        // use collection query override?
        ResourceListQuery resourceListQuery;
        if (collectionQueryOverrideOpt.isPresent()) {
          resourceListQuery = collectionQueryOverrideOpt.get();

          // shortcut: don't query list provider if limit is set to 0
          Optional<Integer> limit = resourceListQuery.getLimit();
          if (limit.isPresent() && limit.get() == 0) {
            return Collections.emptyMap();
          }
        } else {
          resourceListQuery = new DefaultResourceListQuery();
        }

        return listProvidersService.getList(metadataField.getListprovider(), resourceListQuery, true);
      }
      return null;
    } catch (final ListProviderException e) {
      logger.warn("Unable to set collection on metadata because", e);
      return null;
    }
  }

  /**
   * Removes a {@link MetadataField} if it already exists in the ordered collection.
   *
   * @param metadata
   *          The field to remove.
   */
  private void removeFieldIfExists(final MetadataField metadata) {
    int index = -1;
    for (final MetadataField field : fieldsInOrder) {
      if (field.getInputID().equalsIgnoreCase(metadata.getInputID())
              && field.getOutputID().equalsIgnoreCase(metadata.getOutputID())) {
        index = fieldsInOrder.indexOf(field);
      }
    }

    if (index >= 0) {
      fieldsInOrder.remove(index);
    }
  }

  public void removeField(final MetadataField metadata) {
    if (metadata == null) {
      throw new IllegalArgumentException("The metadata must not be null.");
    }
    this.fieldsInOrder.remove(metadata);
    this.outputFields.remove(metadata.getOutputID());
  }

  public List<MetadataField> getFields() {
    return this.fieldsInOrder;
  }

  public void updateStringField(final MetadataField current, final String value) {
    if (current.getValue() != null && !(current.getValue() instanceof String)) {
      throw new IllegalArgumentException("Unable to update a field to a different type than String with this method!");
    }
    removeField(current);
    final MetadataField field = new MetadataField(current);
    field.setValue(value);
    addField(field);
  }

  public boolean isUpdated() {
    for (final MetadataField field : fieldsInOrder) {
      if (field.isUpdated()) {
        return true;
      }
    }
    return false;
  }

}