Coverage Summary for Class: MetadataEditor (org.kitodo.production.metadata)

Class Method, % Line, %
MetadataEditor 60,7% (17/28) 41,8% (100/239)
MetadataEditor$1 100% (1/1) 100% (2/2)
Total 62,1% (18/29) 42,3% (102/241)


 /*
  * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
  *
  * This file is part of the Kitodo project.
  *
  * It is licensed under GNU General Public License version 3 or later.
  *
  * For the full copyright and license information, please read the
  * GPL3-License.txt file that was distributed with this source code.
  */
 
 package org.kitodo.production.metadata;
 
 import java.io.IOException;
 import java.net.URI;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.OptionalInt;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.kitodo.api.MdSec;
 import org.kitodo.api.Metadata;
 import org.kitodo.api.MetadataEntry;
 import org.kitodo.api.MetadataGroup;
 import org.kitodo.api.dataeditor.rulesetmanagement.Domain;
 import org.kitodo.api.dataeditor.rulesetmanagement.MetadataViewInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.SimpleMetadataViewInterface;
 import org.kitodo.api.dataformat.Division;
 import org.kitodo.api.dataformat.LogicalDivision;
 import org.kitodo.api.dataformat.PhysicalDivision;
 import org.kitodo.api.dataformat.View;
 import org.kitodo.api.dataformat.Workpiece;
 import org.kitodo.api.dataformat.mets.LinkedMetsResource;
 import org.kitodo.data.database.beans.Process;
 import org.kitodo.production.helper.Helper;
 import org.kitodo.production.services.ServiceManager;
 
 /**
  * This class contains some methods to handle metadata (semi) automatically.
  */
 public class MetadataEditor {
     private static final Logger logger = LogManager.getLogger(MetadataEditor.class);
 
     /**
      * Separator for specifying an insertion position.
      */
     public static final String INSERTION_POSITION_SEPARATOR = ",";
 
     /**
      * LOCTYPE used for internal links.
      */
     private static final String INTERNAL_LOCTYPE = "Kitodo.Production";
 
     /**
      * Connects two processes by means of a link. The link is sorted as a linked
      * logical division in a logical division of the parent process. The order
      * is based on the order number specified by the user. This method does not
      * create a link between the two processes in the database, this must and
      * can only happen when saving.
      *
      * @param process
      *            the parent process in which the link is to be added
      * @param insertionPosition
      *            at which point the link is to be inserted
      * @param childProcessId
      *            Database ID of the child process to be linked
      * @throws IOException
      *             if the METS file cannot be read or written
      */
     public static void addLink(Process process, String insertionPosition, int childProcessId) throws IOException {
         URI metadataFileUri = ServiceManager.getProcessService().getMetadataFileUri(process);
         Workpiece workpiece = ServiceManager.getMetsService().loadWorkpiece(metadataFileUri);
         List<String> indices = Arrays.asList(insertionPosition.split(Pattern.quote(INSERTION_POSITION_SEPARATOR)));
         LogicalDivision logicalDivision = workpiece.getLogicalStructure();
         for (int index = 0; index < indices.size(); index++) {
             if (index < indices.size() - 1) {
                 logicalDivision = logicalDivision.getChildren().get(Integer.parseInt(indices.get(index)));
             } else {
                 addLink(logicalDivision, Integer.parseInt(indices.get(index)), childProcessId);
             }
         }
         ServiceManager.getFileService().createBackupFile(process);
         ServiceManager.getMetsService().saveWorkpiece(workpiece, metadataFileUri);
     }
 
     /**
      * Connects two processes by means of a link. The link is sorted as a linked
      * logical division in a logical division of the parent process. The order
      * is based on the order number specified by the user. This method does not
      * create a link between the two processes in the database, this must and
      * can only happen when saving.
      *
      * @param parentLogicalDivision
      *            document logical division of the parent process in which the
      *            link is to be added
      * @param childProcessId
      *            Database ID of the child process to be linked
      */
     public static void addLink(LogicalDivision parentLogicalDivision, int childProcessId) {
         addLink(parentLogicalDivision, -1, childProcessId);
     }
 
     private static void addLink(LogicalDivision parentLogicalDivision, int index, int childProcessId) {
 
         LinkedMetsResource link = new LinkedMetsResource();
         link.setLoctype(INTERNAL_LOCTYPE);
         URI uri = ServiceManager.getProcessService().getProcessURI(childProcessId);
         link.setUri(uri);
         LogicalDivision logicalDivision = new LogicalDivision();
         logicalDivision.setLink(link);
         List<LogicalDivision> children = parentLogicalDivision.getChildren();
         if (index < 0) {
             children.add(logicalDivision);
         } else {
             children.add(index, logicalDivision);
         }
     }
 
     /**
      * Remove link to process with ID 'childProcessId' from workpiece of Process
      * 'parentProcess'.
      *
      * @param parentProcess
      *            Process from which link is removed
      * @param childProcessId
      *            ID of process whose link will be remove from workpiece of
      *            parent process
      * @throws IOException
      *             thrown if meta.xml could not be loaded
      */
     public static void removeLink(Process parentProcess, int childProcessId) throws IOException {
         URI metadataFileUri = ServiceManager.getProcessService().getMetadataFileUri(parentProcess);
         Workpiece workpiece = ServiceManager.getMetsService().loadWorkpiece(metadataFileUri);
         if (removeLinkRecursive(workpiece.getLogicalStructure(), childProcessId)) {
             ServiceManager.getFileService().createBackupFile(parentProcess);
             ServiceManager.getMetsService().saveWorkpiece(workpiece, metadataFileUri);
         } else {
             Helper.setErrorMessage("errorDeleting", new Object[] {Helper.getTranslation("link") });
         }
     }
 
     private static boolean removeLinkRecursive(LogicalDivision element, int childId) {
         LogicalDivision parentElement = null;
         LogicalDivision linkElement = null;
         for (LogicalDivision structuralElement : element.getChildren()) {
             if (Objects.nonNull(structuralElement.getLink()) && Objects.nonNull(structuralElement.getLink().getUri())
                     && structuralElement.getLink().getUri().toString().endsWith("process.id=" + childId)) {
                 parentElement = element;
                 linkElement = structuralElement;
                 break;
             } else {
                 if (removeLinkRecursive(structuralElement, childId)) {
                     return true;
                 }
             }
         }
         // no need to check if 'linkElement' is Null since it is set in the same
         // place as 'parentElement'!
         if (Objects.nonNull(parentElement)) {
             parentElement.getChildren().remove(linkElement);
             return true;
         }
         return false;
     }
 
     private static void addMultipleStructuresWithMetadataEntry(int number, String type, Workpiece workpiece, LogicalDivision structure,
             InsertionPosition position, String metadataKey, String metadataValue) {
         for (int i = 0; i < number; i++) {
             LogicalDivision newStructure = addLogicalDivision(type, workpiece, structure, position,
                 Collections.emptyList());
             if (Objects.isNull(newStructure) || metadataKey.isEmpty()) {
                 continue;
             }
             if (!metadataKey.isEmpty()) {
                 MetadataEntry metadataEntry = new MetadataEntry();
                 metadataEntry.setKey(metadataKey);
                 metadataEntry.setValue(metadataValue + " " + (number - i));
                 newStructure.getMetadata().add(metadataEntry);
             }
         }
     }
 
     private static void addMultipleStructuresWithMetadataGroup(int number, String type, Workpiece workpiece, LogicalDivision structure,
             InsertionPosition position, String metadataKey) {
 
         for (int i = 0; i < number; i++) {
             LogicalDivision newStructure = addLogicalDivision(type, workpiece, structure, position,
                 Collections.emptyList());
             if (Objects.isNull(newStructure) || metadataKey.isEmpty() || metadataKey == null) {
                 continue;
             }
             MetadataGroup metadataGroup = new MetadataGroup();
             metadataGroup.setKey(metadataKey);
             newStructure.getMetadata().add(metadataGroup);
         }
     }
 
     /**
      * Creates a given number of new structures and inserts them into the
      * workpiece. The insertion position is given relative to an existing
      * structure. In addition, you can specify metadata, which is assigned to
      * the structures consecutively with a counter.
      *
      * @param number
      *            number of structures to create
      * @param type
      *            type of new structure
      * @param workpiece
      *            workpiece to which the new structure is to be added
      * @param structure
      *            structure relative to which the new structure is to be
      *            inserted
      * @param position
      *            relative insertion position
      * @param metadataViewInterface
      *            interface of the metadata to be added
      * @param metadataValue
      *            value of the first metadata entry
      */
     public static void addMultipleStructuresWithMetadata(int number, String type, Workpiece workpiece, LogicalDivision structure,
             InsertionPosition position, MetadataViewInterface metadataViewInterface, String metadataValue) {
 
         String metadataKey = metadataViewInterface.getId();
         
         if (metadataViewInterface.isComplex()) {
             addMultipleStructuresWithMetadataGroup(number, type, workpiece, structure, position, metadataKey);
         } else {
             addMultipleStructuresWithMetadataEntry(number, type, workpiece, structure, position, metadataKey, metadataValue);
         }
     }
     
     /**
      * Creates a given number of new structures and inserts them into the
      * workpiece. The insertion position is given relative to an existing
      * structure.
      *
      * @param number
      *            number of structures to create
      * @param type
      *            type of new structure
      * @param workpiece
      *            workpiece to which the new structure is to be added
      * @param structure
      *            structure relative to which the new structure is to be
      *            inserted
      * @param position
      *            relative insertion position
      */
     public static void addMultipleStructures(int number, String type, Workpiece workpiece, LogicalDivision structure,
             InsertionPosition position) {
         for (int i = 0; i < number; i++) {
             LogicalDivision newStructure = addLogicalDivision(type, workpiece, structure, position,
                 Collections.emptyList());
         }
     }
 
     /**
      * Creates a new structure and inserts it into a workpiece. The insertion
      * position is determined by the specified structure and mode. The given
      * views are assigned to the structure and all its parent structures.
      *
      * @param type
      *            type of new structure
      * @param workpiece
      *            workpiece to which the new structure is to be added
      * @param logicalDivision
      *            structure relative to which the new structure is to be
      *            inserted
      * @param position
      *            relative insertion position
      * @param viewsToAdd
      *            views to be assigned to the structure
      * @return the newly created structure
      */
     public static LogicalDivision addLogicalDivision(String type, Workpiece workpiece, LogicalDivision logicalDivision,
             InsertionPosition position, List<View> viewsToAdd) {
         LinkedList<LogicalDivision> parents = getAncestorsOfLogicalDivision(logicalDivision,
             workpiece.getLogicalStructure());
         List<LogicalDivision> siblings = new LinkedList<>();
         if (parents.isEmpty()) {
             if (position.equals(InsertionPosition.AFTER_CURRENT_ELEMENT)
                     || position.equals(InsertionPosition.BEFORE_CURRENT_ELEMENT)
                     || position.equals(InsertionPosition.PARENT_OF_CURRENT_ELEMENT)) {
                 Helper.setErrorMessage("No parent found for currently selected structure to which new structure can be appended!");
                 return null;
             }
         } else {
             siblings = parents.getLast().getChildren();
         }
         LogicalDivision newStructure = new LogicalDivision();
         newStructure.setType(type);
 
         handlePosition(workpiece, logicalDivision, position, viewsToAdd, parents, siblings, newStructure);
 
         if (Objects.nonNull(viewsToAdd) && !viewsToAdd.isEmpty()) {
             handleViewsToAdd(viewsToAdd, newStructure);
         }
         return newStructure;
     }
 
     private static void handlePosition(Workpiece workpiece, LogicalDivision logicalDivision, InsertionPosition position,
                                        List<View> viewsToAdd, LinkedList<LogicalDivision> parents,
                                        List<LogicalDivision> siblings, LogicalDivision newStructure) {
         switch (position) {
             case AFTER_CURRENT_ELEMENT:
                 siblings.add(siblings.indexOf(logicalDivision) + 1, newStructure);
                 break;
             case BEFORE_CURRENT_ELEMENT:
                 siblings.add(siblings.indexOf(logicalDivision), newStructure);
                 break;
             case CURRENT_POSITION:
                 OptionalInt minOrder = viewsToAdd.stream().mapToInt(v -> v.getPhysicalDivision().getOrder()).min();
                 if (minOrder.isPresent()) {
                     int structureOrder = minOrder.getAsInt();
                     // new structure ORDER must be set to same min ORDER value of contained physical divisions
                     newStructure.setOrder(structureOrder);
                     List<Integer> siblingOrderValues = Stream.concat(logicalDivision.getChildren().stream()
                             .map(Division::getOrder), Stream.of(structureOrder)).sorted().collect(Collectors.toList());
 
                     // new order must be set at correction location between existing siblings
                     logicalDivision.getChildren().add(siblingOrderValues.indexOf(structureOrder), newStructure);
                 }
                 break;
             case FIRST_CHILD_OF_CURRENT_ELEMENT:
                 logicalDivision.getChildren().add(0, newStructure);
                 break;
             case LAST_CHILD_OF_CURRENT_ELEMENT:
                 logicalDivision.getChildren().add(newStructure);
                 break;
             case PARENT_OF_CURRENT_ELEMENT:
                 newStructure.getChildren().add(logicalDivision);
                 if (parents.isEmpty()) {
                     workpiece.setLogicalStructure(newStructure);
                 } else {
                     siblings.set(siblings.indexOf(logicalDivision), newStructure);
                 }
                 break;
             default:
                 throw new IllegalStateException("complete switch");
         }
     }
 
     private static void handleViewsToAdd(List<View> viewsToAdd, LogicalDivision newStructure) {
         for (View viewToAdd : viewsToAdd) {
             List<LogicalDivision> logicalDivisions = viewToAdd.getPhysicalDivision().getLogicalDivisions();
             for (LogicalDivision elementToUnassign : logicalDivisions) {
                 elementToUnassign.getViews().remove(viewToAdd);
             }
             logicalDivisions.clear();
             logicalDivisions.add(newStructure);
         }
         newStructure.getViews().addAll(viewsToAdd);
     }
 
     /**
      * Create a new PhysicalDivision and insert it into the passed workpiece. The position of insertion
      * is determined by the passed parent and position.
      * @param type type of new PhysicalDivision
      * @param workpiece workpiece from which the root physical division is retrieved
      * @param parent parent of the new PhysicalDivision
      * @param position position relative to the parent element
      */
     public static PhysicalDivision addPhysicalDivision(String type, Workpiece workpiece, PhysicalDivision parent,
             InsertionPosition position) {
         LinkedList<PhysicalDivision> grandparents = getAncestorsOfPhysicalDivision(parent, workpiece.getPhysicalStructure());
         List<PhysicalDivision> siblings = new LinkedList<>();
         if (grandparents.isEmpty()) {
             if (position.equals(InsertionPosition.AFTER_CURRENT_ELEMENT)
                     || position.equals(InsertionPosition.BEFORE_CURRENT_ELEMENT)) {
                 Helper.setErrorMessage("No parent found for currently selected physical "
                         + "division to which new physical division can be appended!");
             }
         } else {
             siblings = grandparents.getLast().getChildren();
         }
         PhysicalDivision newPhysicalDivision = new PhysicalDivision();
         newPhysicalDivision.setType(type);
         switch (position) {
             case AFTER_CURRENT_ELEMENT:
                 siblings.add(siblings.indexOf(parent) + 1, newPhysicalDivision);
                 break;
             case BEFORE_CURRENT_ELEMENT:
                 siblings.add(siblings.indexOf(parent), newPhysicalDivision);
                 break;
             case FIRST_CHILD_OF_CURRENT_ELEMENT:
                 parent.getChildren().add(0, newPhysicalDivision);
                 break;
             case LAST_CHILD_OF_CURRENT_ELEMENT:
                 parent.getChildren().add(newPhysicalDivision);
                 break;
             default:
                 throw new IllegalStateException("Used InsertionPosition not allowed.");
         }
         return newPhysicalDivision;
     }
 
     /**
      * Assigns all views of all children to the specified included structural
      * element.
      *
      * @param structure
      *            structure to add all views of all children to
      */
     public static void assignViewsFromChildren(LogicalDivision structure) {
         Collection<View> viewsToAdd = getViewsFromChildrenRecursive(structure);
         Collection<View> assignedViews = structure.getViews();
         viewsToAdd.removeAll(assignedViews);
         List<View> sortedViews = Stream.concat(assignedViews.stream(), viewsToAdd.stream())
                 .sorted(Comparator.comparing(view -> view.getPhysicalDivision().getOrder()))
                 .collect(Collectors.toList());
         assignedViews.clear();
         assignedViews.addAll(sortedViews);
     }
 
     private static Collection<View> getViewsFromChildrenRecursive(LogicalDivision structure) {
         Set<View> viewsFromChildren = new LinkedHashSet<>(structure.getViews());
         for (LogicalDivision child : structure.getChildren()) {
             viewsFromChildren.addAll(getViewsFromChildrenRecursive(child));
         }
         return viewsFromChildren;
     }
 
     /**
      * Creates a view on a physical division that is not further restricted; that is,
      * the entire physical division is displayed.
      *
      * @param physicalDivision
      *            physical division on which a view is to be formed
      * @return the created physical division
      */
     public static View createUnrestrictedViewOn(PhysicalDivision physicalDivision) {
         View unrestrictedView = new View();
         unrestrictedView.setPhysicalDivision(physicalDivision);
         return unrestrictedView;
     }
 
     /**
      * Determines the path to the logical division of the child. For each level
      * of the logical structure, the recursion is run through once, that is for
      * a newspaper year process tree times (year, month, day).
      *
      * @param logicalDivision
      *            logical division of the level stage of recursion (starting
      *            from the top)
      * @param number
      *            number of the record of the process of the child
      *
      */
     public static List<LogicalDivision> determineLogicalDivisionPathToChild(
             LogicalDivision logicalDivision, int number) {
 
         if (Objects.nonNull(logicalDivision.getLink())) {
             try {
                 if (ServiceManager.getProcessService()
                         .processIdFromUri(logicalDivision.getLink().getUri()) == number) {
                     LinkedList<LogicalDivision> linkedLogicalDivisions = new LinkedList<>();
                     linkedLogicalDivisions.add(logicalDivision);
                     return linkedLogicalDivisions;
                 }
             } catch (IllegalArgumentException | ClassCastException | SecurityException e) {
                 logger.catching(Level.TRACE, e);
             }
         }
         for (LogicalDivision logicalDivisionChild : logicalDivision.getChildren()) {
             List<LogicalDivision> logicalDivisionList = determineLogicalDivisionPathToChild(
                 logicalDivisionChild, number);
             if (!logicalDivisionList.isEmpty()) {
                 logicalDivisionList.add(0, logicalDivision);
                 return logicalDivisionList;
             }
         }
         return Collections.emptyList();
     }
 
     /**
      * Transforms a {@code Domain} specifying object from the ruleset into an
      * {@code MdSec} specifier for metadata in internal data format. Note that
      * there is no {@code MdSec} for {@code Domain.METS_DIV}; that has to be
      * treated differently.
      *
      * @param domain
      *            domain to transform
      * @return {@code MdSec} is returned
      * @throws IllegalArgumentException
      *             if the {@code Domain} is {@code mets:div}
      */
     public static MdSec domainToMdSec(Domain domain) {
         switch (domain) {
             case DESCRIPTION:
                 return MdSec.DMD_SEC;
             case DIGITAL_PROVENANCE:
                 return MdSec.DIGIPROV_MD;
             case RIGHTS:
                 return MdSec.RIGHTS_MD;
             case SOURCE:
                 return MdSec.SOURCE_MD;
             case TECHNICAL:
                 return MdSec.TECH_MD;
             default:
                 throw new IllegalArgumentException(domain.name());
         }
     }
 
     /**
      * Determines the ancestors of a tree node.
      *
      * @param searched
      *            node whose ancestor nodes are to be found
      * @param position
      *            node to be searched recursively
      * @return the parent nodes (maybe empty)
      */
     public static LinkedList<LogicalDivision> getAncestorsOfLogicalDivision(LogicalDivision searched,
                                                                                 LogicalDivision position) {
         return getAncestorsRecursive(searched, position, null)
                 .stream()
                 .map(parent -> (LogicalDivision) parent)
                 .collect(Collectors.toCollection(LinkedList::new));
     }
 
     /**
      * Determines the ancestors of a tree node.
      *
      * @param searched
      *            node whose ancestor nodes are to be found
      * @param position
      *            node to be searched recursively
      * @return the parent nodes (maybe empty)
      */
     public static LinkedList<PhysicalDivision> getAncestorsOfPhysicalDivision(PhysicalDivision searched, PhysicalDivision position) {
         return getAncestorsRecursive(searched, position, null)
                 .stream()
                 .map(parent -> (PhysicalDivision) parent)
                 .collect(Collectors.toCollection(LinkedList::new));
     }
 
     private static <T extends Division<T>> LinkedList<Division<T>> getAncestorsRecursive(Division<T> searched,
             Division<T> position, Division<T> parent) {
 
         if (position.equals(searched)) {
             if (Objects.isNull(parent)) {
                 return new LinkedList<>();
             }
             LinkedList<Division<T>> ancestors = new LinkedList<>();
             ancestors.add(parent);
             return ancestors;
         }
         for (Division<T> child : position.getChildren()) {
             LinkedList<Division<T>> maybeFound = getAncestorsRecursive(searched, child, position);
             if (!maybeFound.isEmpty()) {
                 if (Objects.nonNull(parent)) {
                     maybeFound.addFirst(parent);
                 }
                 return maybeFound;
             }
         }
         return new LinkedList<>();
     }
 
     /**
      * Returns the value of the specified metadata entry.
      *
      * @param logicalDivision
      *            logical division from whose metadata the value is
      *            to be retrieved
      * @param key
      *            key of the metadata to be determined
      * @return the value of the metadata entry, otherwise {@code null}
      */
     public static String getMetadataValue(LogicalDivision logicalDivision, String key) {
         for (Metadata metadata : logicalDivision.getMetadata()) {
             if (metadata.getKey().equals(key) && metadata instanceof MetadataEntry) {
                 return ((MetadataEntry) metadata).getValue();
             }
         }
         return null;
     }
 
     /**
      * Get the first view the given PhysicalDivision is assigned to.
      * @param physicalDivision PhysicalDivision to get the view for
      * @return View or null if no View was found
      */
     public static View getFirstViewForPhysicalDivision(PhysicalDivision physicalDivision) {
         List<LogicalDivision> logicalDivisions = physicalDivision.getLogicalDivisions();
         if (!logicalDivisions.isEmpty() && Objects.nonNull(logicalDivisions.get(0))) {
             for (View view : logicalDivisions.get(0).getViews()) {
                 if (Objects.nonNull(view) && Objects.equals(view.getPhysicalDivision(), physicalDivision)) {
                     return view;
                 }
             }
         }
         return null;
     }
 
     /**
      * Reads the simple metadata from a logical division defined by
      * the simple metadata view interface, including {@code mets:div} metadata.
      *
      * @param division
      *            logical division from which the metadata should be
      *            read
      * @param simpleMetadataView
      *            simple metadata view interface which formally describes the
      *            methadata to be read
      * @return metadata which corresponds to the formal description
      */
     public static List<String> readSimpleMetadataValues(LogicalDivision division,
             SimpleMetadataViewInterface simpleMetadataView) {
         Domain domain = simpleMetadataView.getDomain().orElse(Domain.DESCRIPTION);
         if (domain.equals(Domain.METS_DIV)) {
             switch (simpleMetadataView.getId().toLowerCase()) {
                 case "label":
                     return Collections.singletonList(division.getLabel());
                 case "orderlabel":
                     return Collections.singletonList(division.getOrderlabel());
                 case "type":
                     return Collections.singletonList(division.getType());
                 default:
                     throw new IllegalArgumentException(division.getClass().getSimpleName() + " has no field '"
                             + simpleMetadataView.getId() + "'.");
             }
         } else {
             return division.getMetadata().parallelStream()
                     .filter(metadata -> metadata.getKey().equals(simpleMetadataView.getId()))
                     .filter(MetadataEntry.class::isInstance).map(MetadataEntry.class::cast).map(MetadataEntry::getValue)
                     .collect(Collectors.toList());
         }
     }
 
     /**
      * Removes all metadata of the given key from the given included structural
      * element.
      *
      * @param logicalDivision
      *            logical division to remove metadata from
      * @param key
      *            key of metadata to remove
      */
     public static void removeAllMetadata(LogicalDivision logicalDivision, String key) {
         logicalDivision.getMetadata().removeIf(metadata -> key.equals(metadata.getKey()));
     }
 
     /**
      * Writes a metadata entry as defined by the ruleset. The ruleset allows the
      * domain {@code mets:div}. If a metadata entry must be written with the
      * {@code mets:div} domain, this must set the internal value on the object
      * model. Otherwise, however, a metadata entry is written in metadata area.
      *
      * @param division
      *            logical division at which the metadata entry is to
      *            be written
      * @param simpleMetadataView
      *            properties of the metadata entry as defined in the ruleset
      * @param value
      *            worth writing
      * @throws IllegalArgumentException
      *             when trying to write a value to the structure, there is no
      *             field for it. This is when the domain is {@code mets:div},
      *             and the value is different from either {@code label} or
      *             {@code orderlabel}.
      */
     public static void writeMetadataEntry(LogicalDivision division,
             SimpleMetadataViewInterface simpleMetadataView, String value) {
 
         Domain domain = simpleMetadataView.getDomain().orElse(Domain.DESCRIPTION);
         if (domain.equals(Domain.METS_DIV)) {
             switch (simpleMetadataView.getId().toLowerCase()) {
                 case "label":
                     division.setLabel(value);
                     break;
                 case "orderlabel":
                     division.setOrderlabel(value);
                     break;
                 case "type":
                     throw new IllegalArgumentException(
                             "'" + simpleMetadataView.getId() + "' is reserved for the key ID.");
                 default:
                     throw new IllegalArgumentException(division.getClass().getSimpleName() + " has no field '"
                             + simpleMetadataView.getId() + "'.");
             }
         } else {
             MetadataEntry metadata = new MetadataEntry();
             metadata.setKey(simpleMetadataView.getId());
             metadata.setDomain(domainToMdSec(domain));
             metadata.setValue(value);
             division.getMetadata().add(metadata);
         }
     }
 }