BagitProfileDeserializer.java

/*
 * Copyright (C) 2023 DANS - Data Archiving and Networked Services (info@dans.knaw.nl)
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 nl.knaw.dans.bagit.conformance.profile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

/**
 * Deserialize bagit profile json to a {@link BagitProfile}
 */
public class BagitProfileDeserializer extends StdDeserializer<BagitProfile> {

  private static final long serialVersionUID = 1L;
  private static final Logger logger = LoggerFactory.getLogger(BagitProfileDeserializer.class);
  private static final ResourceBundle messages = ResourceBundle.getBundle("MessageBundle");

  public BagitProfileDeserializer() {
    this(null);
  }

  public BagitProfileDeserializer(final Class<?> vc) {
    super(vc);
  }

  @Override
  public BagitProfile deserialize(final JsonParser p, final DeserializationContext ctxt)
          throws IOException, JsonProcessingException {
    final BagitProfile profile = new BagitProfile();
    final JsonNode node = p.getCodec().readTree(p);

    parseBagitProfileInfo(node, profile);

    profile.setBagInfoRequirements(parseBagInfo(node));

    profile.getManifestTypesRequired().addAll(parseManifestTypesRequired(node));

    profile.setFetchFileAllowed(node.get("Allow-Fetch.txt").asBoolean());
    logger.debug(messages.getString("fetch_allowed"), profile.isFetchFileAllowed());

    profile.setSerialization(Serialization.valueOf(node.get("Serialization").asText()));
    logger.debug(messages.getString("serialization_allowed"), profile.getSerialization());

    profile.getAcceptableMIMESerializationTypes().addAll(parseAcceptableSerializationFormats(node));

    profile.getTagManifestTypesRequired().addAll(parseRequiredTagmanifestTypes(node));

    profile.getTagFilesRequired().addAll(parseRequiredTagFiles(node));

    profile.getAcceptableBagitVersions().addAll(parseAcceptableVersions(node));

    return profile;
  }

  private static void parseBagitProfileInfo(final JsonNode node, final BagitProfile profile) {
    logger.debug(messages.getString("parsing_bagit_profile_info_section"));
    
    final JsonNode bagitProfileInfoNode = node.get("BagIt-Profile-Info");
    parseMandatoryTagsOfBagitProfileInfo(bagitProfileInfoNode, profile);
    parseOptionalTagsOfBagitProfileInfo(bagitProfileInfoNode, profile);
  }

  /**
   * Parse required tags due to specification defined at <a href="https://github.com/bagit-profiles/bagit-profiles">bagit profiles</a>
   * Note: If one of the tags is missing, a NullPointerException is thrown.
   *
   * @param bagitProfileInfoNode Root node of the bagit profile info section.
   * @param profile Representation of bagit profile.
   */
  private static void parseMandatoryTagsOfBagitProfileInfo(final JsonNode bagitProfileInfoNode, final BagitProfile profile) {
    logger.debug(messages.getString("parsing_mandatory_tags_of_bagit_profile_info_section"));
    
    final String profileIdentifier = bagitProfileInfoNode.get("BagIt-Profile-Identifier").asText();
    logger.debug(messages.getString("identifier"), profileIdentifier);
    profile.setBagitProfileIdentifier(profileIdentifier);

    final String sourceOrg = bagitProfileInfoNode.get("Source-Organization").asText();
    logger.debug(messages.getString("source_organization"), sourceOrg);
    profile.setSourceOrganization(sourceOrg);

    final String extDescript = bagitProfileInfoNode.get("External-Description").asText();
    logger.debug(messages.getString("external_description"), extDescript);
    profile.setExternalDescription(extDescript);

    final String version = bagitProfileInfoNode.get("Version").asText();
    logger.debug(messages.getString("version"), version);
    profile.setVersion(version);
  }

  /**
   * Parse optional tags due to specification defined at <a href="https://github.com/bagit-profiles/bagit-profiles">bagit profiles</a>
   *
   * @param bagitProfileInfoNode Root node of the bagit profile info section.
   * @param profile Representation of bagit profile .
   */
  private static void parseOptionalTagsOfBagitProfileInfo(final JsonNode bagitProfileInfoNode, final BagitProfile profile) {
    logger.debug(messages.getString("parsing_optional_tags_of_bagit_profile_info_section"));

    final JsonNode contactNameNode = bagitProfileInfoNode.get("Contact-Name");
    if (contactNameNode != null) {
      final String contactName = contactNameNode.asText();
      logger.debug(messages.getString("contact_name"), contactName);
      profile.setContactName(contactName);
    }

    final JsonNode contactEmailNode = bagitProfileInfoNode.get("Contact-Email");
    if (contactEmailNode != null) {
      final String contactEmail = contactEmailNode.asText();
      logger.debug(messages.getString("contact_email"), contactEmail);
      profile.setContactEmail(contactEmail);
    }

    final JsonNode contactPhoneNode = bagitProfileInfoNode.get("Contact-Phone");
    if (contactPhoneNode != null) {
      final String contactPhone = contactPhoneNode.asText();
      logger.debug(messages.getString("contact_phone"), contactPhone);
      profile.setContactPhone(contactPhone);
    }
  }

  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
  private static Map<String, BagInfoRequirement> parseBagInfo(final JsonNode rootNode) {
    final JsonNode bagInfoNode = rootNode.get("Bag-Info");
    logger.debug(messages.getString("parsing_bag_info"));
    final Map<String, BagInfoRequirement> bagInfo = new HashMap<>();

    final Iterator<Entry<String, JsonNode>> nodes = bagInfoNode.fields(); //stuck in java 6...

    while (nodes.hasNext()) {
      final Entry<String, JsonNode> node = nodes.next();

      final BagInfoRequirement entry = new BagInfoRequirement();
      // due to specification required is false by default.
      final JsonNode requiredNode = node.getValue().get("required");
      if (requiredNode != null) {
        entry.setRequired(requiredNode.asBoolean());
      }

      final JsonNode valuesNode = node.getValue().get("values");
      if (valuesNode != null) {
        for (final JsonNode value : valuesNode) {
          entry.getAcceptableValues().add(value.asText());
        }
      }

      final JsonNode repeatableNode = node.getValue().get("repeatable");
      if (repeatableNode != null) {
        entry.setRepeatable(repeatableNode.asBoolean());
      }

      logger.debug("{}: {}", node.getKey(), entry);
      bagInfo.put(node.getKey(), entry);
    }

    return bagInfo;
  }

  private static List<String> parseManifestTypesRequired(final JsonNode node) {
    final JsonNode manifests = node.get("Manifests-Required");

    final List<String> manifestTypes = new ArrayList<>();

    for (final JsonNode manifestName : manifests) {
      manifestTypes.add(manifestName.asText());
    }

    logger.debug(messages.getString("required_manifest_types"), manifestTypes);

    return manifestTypes;
  }

  private static List<String> parseAcceptableSerializationFormats(final JsonNode node) {
    final JsonNode serialiationFormats = node.get("Accept-Serialization");
    final List<String> serialTypes = new ArrayList<>();

    for (final JsonNode serialiationFormat : serialiationFormats) {
      serialTypes.add(serialiationFormat.asText());
    }
    logger.debug(messages.getString("acceptable_serialization_mime_types"), serialTypes);

    return serialTypes;
  }

  private static List<String> parseRequiredTagmanifestTypes(final JsonNode node) {
    final JsonNode tagManifestsRequiredNodes = node.get("Tag-Manifests-Required");
    final List<String> requiredTagmanifestTypes = new ArrayList<>();
    if (tagManifestsRequiredNodes != null) {
      for (final JsonNode tagManifestsRequiredNode : tagManifestsRequiredNodes) {
        requiredTagmanifestTypes.add(tagManifestsRequiredNode.asText());
      }
    }
    logger.debug(messages.getString("required_tagmanifest_types"), requiredTagmanifestTypes);

    return requiredTagmanifestTypes;
  }

  private static List<String> parseRequiredTagFiles(final JsonNode node) {
    final JsonNode tagFilesRequiredNodes = node.get("Tag-Files-Required");
    final List<String> requiredTagFiles = new ArrayList<>();

    if (tagFilesRequiredNodes != null) {
      for (final JsonNode tagFilesRequiredNode : tagFilesRequiredNodes) {
        requiredTagFiles.add(tagFilesRequiredNode.asText());
      }
    }
    logger.debug(messages.getString("tag_files_required"), requiredTagFiles);

    return requiredTagFiles;
  }

  private static List<String> parseAcceptableVersions(final JsonNode node) {
    final JsonNode acceptableVersionsNodes = node.get("Accept-BagIt-Version");
    final List<String> acceptableVersions = new ArrayList<>();

    for (final JsonNode acceptableVersionsNode : acceptableVersionsNodes) {
      acceptableVersions.add(acceptableVersionsNode.asText());
    }
    logger.debug(messages.getString("acceptable_bagit_versions"), acceptableVersions);

    return acceptableVersions;
  }
}