Coverage Summary for Class: NewspaperProcessesMigrator (org.kitodo.production.migration)

Class Class, % Method, % Line, %
NewspaperProcessesMigrator 100% (1/1) 83,3% (25/30) 84,7% (221/261)


 /*
  * (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.migration;
 
 
 
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import javax.naming.ConfigurationException;
 
 import org.apache.commons.collections4.iterators.PeekingIterator;
 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.dataeditor.rulesetmanagement.DatesSimpleMetadataViewInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.FunctionalMetadata;
 import org.kitodo.api.dataeditor.rulesetmanagement.RulesetManagementInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.SimpleMetadataViewInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.StructuralElementViewInterface;
 import org.kitodo.api.dataformat.LogicalDivision;
 import org.kitodo.api.dataformat.Workpiece;
 import org.kitodo.config.ConfigCore;
 import org.kitodo.config.enums.ParameterCore;
 import org.kitodo.data.database.beans.Batch;
 import org.kitodo.data.database.beans.Process;
 import org.kitodo.data.database.enums.BatchType;
 import org.kitodo.data.database.exceptions.DAOException;
 import org.kitodo.data.exceptions.DataException;
 import org.kitodo.exceptions.CommandException;
 import org.kitodo.exceptions.ProcessGenerationException;
 import org.kitodo.production.helper.tasks.NewspaperMigrationTask;
 import org.kitodo.production.helper.tasks.TaskManager;
 import org.kitodo.production.metadata.MetadataEditor;
 import org.kitodo.production.process.NewspaperProcessesGenerator;
 import org.kitodo.production.process.ProcessGenerator;
 import org.kitodo.production.services.ServiceManager;
 import org.kitodo.production.services.data.BatchService;
 import org.kitodo.production.services.data.ProcessService;
 import org.kitodo.production.services.dataeditor.DataEditorService;
 import org.kitodo.production.services.dataformat.MetsService;
 import org.kitodo.production.services.file.FileService;
 import org.kitodo.production.services.workflow.WorkflowControllerService;
 
 /**
  * Tool for converting newspaper processes from Production v. 2 format to
  * Production v. 3 format.
  */
 public class NewspaperProcessesMigrator {
     private static final Logger logger = LogManager.getLogger(NewspaperProcessesMigrator.class);
 
     /**
      * Metadata field in Production v. 2 where the displayed title is contained.
      */
     private static final String FIELD_TITLE = "TitleDocMain";
 
     /**
      * Metadata field in Production v. 2, in which the title is contained in
      * sorting form.
      */
     private static final String FIELD_TITLE_SORT = "TitleDocMainShort";
 
     /**
      * Regular expression to find (and remove) the individual part of the
      * process title, to get the base process title.
      */
     private static final String INDIVIDUAL_PART = "(?<=.)\\p{Punct}+(?:1[6-9]|20)\\d{2}\\p{Punct}?(?:0[1-9]|1[012]).*$";
 
     /**
      * A regular expression describing a four-digit year number or a double year
      * consisting of two four-digit year numbers, concatenated by a slash.
      */
     private static final String YEAR_OR_DOUBLE_YEAR = "\\d{4}(?:/\\d{4})?";
 
     /**
      * Acquisition stage of newspaper processes migrator.
      */
     private final String acquisitionStage = "";
 
     /**
      * The database index number of the newspaper batch.
      */
     private Integer batchNumber;
 
     /**
      * Service to read and write Batch objects in the database or search engine
      * index.
      */
     private static final BatchService batchService = ServiceManager.getBatchService();
 
     /**
      * Service that contains the meta-data editor.
      */
     private final DataEditorService dataEditorService = ServiceManager.getDataEditorService();
 
     /**
      * Ruleset setting where to store the day information.
      */
     private DatesSimpleMetadataViewInterface daySimpleMetadataView;
 
     /**
      * Service to access files on the storage.
      */
     private static final FileService fileService = ServiceManager.getFileService();
 
     /**
      * Service to read and write METS file format.
      */
     private final MetsService metsService = ServiceManager.getMetsService();
 
     /**
      * Ruleset setting where to store the month information.
      */
     private DatesSimpleMetadataViewInterface monthSimpleMetadataView;
 
     /**
      * Service to read and write Process objects in the database or search
      * engine index.
      */
     private static final ProcessService processService = ServiceManager.getProcessService();
 
     /**
      * List of processes.
      */
     private final List<Process> processes;
 
     /**
      * Record ID of the process template.
      */
     private int templateId;
 
     /**
      * The metadata of the newspaper as its whole.
      */
     private Collection<Metadata> overallMetadata = new ArrayList<>();
 
     /**
      * A process representing the newspaper as its whole.
      */
     private Process overallProcess;
 
     /**
      * The workpiece of the newspaper as its whole.
      */
     private Workpiece overallWorkpiece = new Workpiece();
 
     /**
      * Record ID of the project.
      */
     private int projectId;
 
     /**
      * The process title.
      */
     private String title;
 
     /**
      * Ruleset setting where to store the year information.
      */
     private DatesSimpleMetadataViewInterface yearSimpleMetadataView;
 
     /**
      * The years of the course of appearance of the newspaper with their
      * logical structures.
      */
     private Map<String, LogicalDivision> years = new TreeMap<>();
 
     /**
      * Process IDs of children (issue processes) to be added to the years in
      * question.
      */
     private Map<String, Collection<Integer>> yearsChildren = new HashMap<>();
 
     /**
      * Years iterator during creation of year processes.
      */
     private PeekingIterator<Entry<String, LogicalDivision>> yearsIterator;
 
     /**
      * The ruleset.
      */
     private RulesetManagementInterface rulesetManagement;
 
     /**
      * Creates a new process migrator.
      *
      * @param batch
      *            the batch to process
      */
     public NewspaperProcessesMigrator(Batch batch) {
         this.batchNumber = batch.getId();
         this.processes = batch.getProcesses();
     }
 
     /**
      * Returns all newspaper batches.
      *
      * @return all newspaper batches
      * @throws DAOException
      *             if a batch cannot be load from the database
      * @throws IOException
      *             if an I/O error occurs when accessing the file system
      */
     public static List<Batch> getNewspaperBatches() throws DAOException, IOException {
         List<Batch> newspaperBatches = new ArrayList<>();
         for (Batch batch : batchService.getAll()) {
             if (BatchType.NEWSPAPER.equals(batch.getType())) {
                 newspaperBatches.add(batch);
             }
         }
         return newspaperBatches;
     }
 
     /**
      * Creates a newspaper migration task for the given batch ID in the task
      * manager.
      *
      * @param batchId
      *            number of batch to migrate
      * @throws DAOException
      *             if a db error occurs
      */
     public static void initializeMigration(Integer batchId) throws DAOException {
         Batch batch = ServiceManager.getBatchService().getById(batchId);
         TaskManager.addTask(new NewspaperMigrationTask(batch));
     }
 
     /**
      * Initializes the newspaper processes migrator.
      *
      * @param process
      *            a process, to get basic information from
      * @param newspaperIncludedStructalElementDivision
      *            the ID of the newspaper division in the ruleset
      */
     private void initializeMigrator(Process process, String newspaperIncludedStructalElementDivision)
             throws IOException, ConfigurationException {
 
         title = generateNewspaperShortTitle(process.getTitle());
         logger.trace("Newspaper is: {}", title);
         projectId = process.getProject().getId();
         logger.trace("Project is: {} (ID {})", process.getProject().getTitle(), projectId);
         templateId = process.getTemplate().getId();
         logger.trace("Template is: {} (ID {})", process.getTemplate().getTitle(), templateId);
 
         rulesetManagement = ServiceManager.getRulesetService()
                 .openRuleset(process.getRuleset());
         StructuralElementViewInterface newspaperView = rulesetManagement.getStructuralElementView(
             newspaperIncludedStructalElementDivision, "", NewspaperProcessesGenerator.ENGLISH);
         StructuralElementViewInterface yearDivisionView = NewspaperProcessesGenerator.nextSubView(rulesetManagement,
             newspaperView, acquisitionStage);
         yearSimpleMetadataView = yearDivisionView.getDatesSimpleMetadata().orElseThrow(
             () -> new ConfigurationException(yearDivisionView.getId() + " has no dates metadata configuration!"));
         StructuralElementViewInterface monthDivisionView = NewspaperProcessesGenerator.nextSubView(rulesetManagement,
             yearDivisionView, acquisitionStage);
         monthSimpleMetadataView = monthDivisionView.getDatesSimpleMetadata().orElseThrow(
             () -> new ConfigurationException(monthDivisionView.getId() + " has no dates metadata configuration!"));
         StructuralElementViewInterface dayDivisionView = NewspaperProcessesGenerator.nextSubView(rulesetManagement,
             monthDivisionView, acquisitionStage);
         daySimpleMetadataView = dayDivisionView.getDatesSimpleMetadata().orElseThrow(
             () -> new ConfigurationException(dayDivisionView.getId() + " has no dates metadata configuration!"));
     }
 
     /**
      * Convert a newspaper like full title into its shorted version.
      *
      * @param newspaperFullTitle Newspaper like full title
      * @return Shorted newspaper like title
      */
     public String generateNewspaperShortTitle(String newspaperFullTitle) {
         return newspaperFullTitle.replaceFirst(INDIVIDUAL_PART, "");
     }
 
     /**
      * Converts one newspaper process.
      *
      * @param index
      *            index of process to convert in the processes object
      *            list passed to the constructor—<b>not</b> the process ID
      */
     public void convertProcess(int index) throws DAOException, IOException, ConfigurationException {
         final long begin = System.nanoTime();
         Integer processId = processes.get(index).getId();
         Process process = processService.getById(processId);
         String processTitle = process.getTitle();
         logger.info("Starting to convert process {} (ID {})...", processTitle, processId);
         URI metadataFilePath = fileService.getMetadataFilePath(process);
         URI anchorFilePath = fileService.createAnchorFile(metadataFilePath);
         URI yearFilePath = fileService.createYearFile(metadataFilePath);
         overallWorkpiece = metsService.loadWorkpiece(anchorFilePath);
 
         dataEditorService.readData(anchorFilePath);
         dataEditorService.readData(yearFilePath);
         dataEditorService.readData(metadataFilePath);
 
         Workpiece workpiece = metsService.loadWorkpiece(metadataFilePath);
         workpiece.setId(process.getId().toString());
         LogicalDivision newspaperLogicalDivision = workpiece.getLogicalStructure();
 
         if (Objects.isNull(title)) {
             initializeMigrator(process, newspaperLogicalDivision.getType());
         }
 
         LogicalDivision yearLogicalDivision = cutOffTopLevel(newspaperLogicalDivision);
         final String year = createLinkStructureAndCopyDates(process, yearFilePath, yearLogicalDivision);
 
         workpiece.setLogicalStructure(cutOffTopLevel(yearLogicalDivision));
         moveMetadataFromYearToIssue(process, processTitle, yearFilePath, workpiece);
         metsService.saveWorkpiece(workpiece, metadataFilePath);
 
         for (Metadata metadata : overallWorkpiece.getLogicalStructure().getMetadata()) {
             if (!overallMetadata.contains(metadata)) {
                 logger.debug("Adding metadata to newspaper {}: {}", title, metadata);
                 overallMetadata.add(metadata);
             }
         }
         yearsChildren.computeIfAbsent(year, each -> new ArrayList<>()).add(processId);
 
         ServiceManager.getFileService().renameFile(anchorFilePath, "meta_anchor.migrated");
         ServiceManager.getFileService().renameFile(yearFilePath, "meta_year.migrated");
 
         logger.info("Process {} (ID {}) successfully converted.", processTitle, processId);
         if (logger.isTraceEnabled()) {
             logger.trace("Converting {} took {} ms.", processTitle,
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     private void moveMetadataFromYearToIssue(Process process, String processTitle, URI yearFilePath,
             Workpiece workpiece) throws IOException {
         Workpiece yearWorkpiece = metsService.loadWorkpiece(yearFilePath);
         // Copy metadata from year to issue
         Collection<Metadata> processMetadataFromYear = new ArrayList<>(
                 yearWorkpiece.getLogicalStructure().getChildren().get(0).getMetadata());
         List<LogicalDivision> issuesIncludedStructuralElements = workpiece.getLogicalStructure().getChildren()
                 .get(0).getChildren();
         issuesIncludedStructuralElements.get(0).getMetadata().addAll(processMetadataFromYear);
 
         RulesetManagementInterface rulesetManagement = ServiceManager.getRulesetManagementService()
                 .getRulesetManagement();
 
         // find and load the ruleset file
         String rulesetDir = ConfigCore.getParameter(ParameterCore.DIR_RULESETS);
         String rulesetFullPath = Paths.get(rulesetDir, process.getRuleset().getFile()).toString();
         rulesetManagement.load(new File(rulesetFullPath));
         Collection<String> functionalKeys = rulesetManagement.getFunctionalKeys(FunctionalMetadata.PROCESS_TITLE);
         String titleKey = functionalKeys.isEmpty() ? FIELD_TITLE : functionalKeys.stream().findFirst().get();
 
         MetadataEntry titelMetadata = new MetadataEntry();
         titelMetadata.setValue(processTitle);
         titelMetadata.setKey(titleKey);
         titelMetadata.setDomain(MdSec.DMD_SEC);
         issuesIncludedStructuralElements.get(0).getMetadata().add(titelMetadata);
     }
 
     /**
      * Cuts the top level of a tree logical division.
      *
      * @param logicalDivision
      *            tree logical division to be cut
      * @return the new top level
      */
     private static LogicalDivision cutOffTopLevel(LogicalDivision logicalDivision) {
         List<LogicalDivision> children = logicalDivision.getChildren();
         int numberOfChildren = children.size();
         if (numberOfChildren == 0) {
             return null;
         }
         LogicalDivision firstChild = children.get(0);
         if (numberOfChildren > 1) {
             children.subList(1, numberOfChildren).stream()
                     .flatMap(theLogicalDivision -> theLogicalDivision.getChildren().stream())
                     .forEachOrdered(firstChild.getChildren()::add);
             String firstOrderlabel = firstChild.getOrderlabel();
             String lastOrderlabel = children.get(children.size() - 1).getOrderlabel();
             if (Objects.nonNull(firstOrderlabel) && !firstOrderlabel.equals(lastOrderlabel)) {
                 firstChild.setOrderlabel(firstOrderlabel + '/' + lastOrderlabel);
             }
         }
         return firstChild;
     }
 
     /**
      * Creates or complements the logical root levels of the annual level.
      *
      * @param process
      *            process ID of the current process (on issue level)
      * @param yearMetadata
      *            Production v. 2 year metadata file
      * @param metaFileYearLogicalDivision
      *            year logical division of the processes’ metadata file
      * @throws IOException
      *             if an error occurs in the disk drive
      */
     private String createLinkStructureAndCopyDates(Process process, URI yearMetadata,
             LogicalDivision metaFileYearLogicalDivision)
             throws IOException, ConfigurationException {
 
         LogicalDivision yearFileYearLogicalDivision = metsService.loadWorkpiece(yearMetadata)
                 .getLogicalStructure().getChildren().get(0);
         String year = MetadataEditor.getMetadataValue(yearFileYearLogicalDivision, FIELD_TITLE_SORT);
         if (Objects.isNull(year) || !year.matches(YEAR_OR_DOUBLE_YEAR)) {
             logger.debug("\"{}\" is not a year number. Falling back to {}.", year, FIELD_TITLE);
             year = MetadataEditor.getMetadataValue(yearFileYearLogicalDivision, FIELD_TITLE);
         }
         LogicalDivision processYearLogicalDivision = years.computeIfAbsent(year, theYear -> {
             // remove existing layers in the year
             yearFileYearLogicalDivision.getChildren().get(0).getChildren().get(0).getChildren().clear();
             MetadataEditor.writeMetadataEntry(yearFileYearLogicalDivision, yearSimpleMetadataView, theYear);
             return yearFileYearLogicalDivision;
         });
 
         createLinkStructureAndCopyMonths(process, metaFileYearLogicalDivision, yearFileYearLogicalDivision, year,
             processYearLogicalDivision);
         return year;
     }
 
     private void createLinkStructureAndCopyMonths(Process process,
             LogicalDivision metaFileYearLogicalDivision, LogicalDivision yearFileYearLogicalDivision, String year,
             LogicalDivision processYearLogicalDivision) throws ConfigurationException {
 
         // Add types to month and day
         StructuralElementViewInterface newspaperView = rulesetManagement.getStructuralElementView(
             overallWorkpiece.getLogicalStructure().getType(), acquisitionStage, Locale.LanguageRange.parse("en"));
         StructuralElementViewInterface yearDivisionView = nextSubView(rulesetManagement, newspaperView,
             acquisitionStage);
         yearSimpleMetadataView = yearDivisionView.getDatesSimpleMetadata().orElseThrow(ConfigurationException::new);
         StructuralElementViewInterface monthDivisionView = nextSubView(rulesetManagement, yearDivisionView,
             acquisitionStage);
         monthSimpleMetadataView = monthDivisionView.getDatesSimpleMetadata().orElseThrow(ConfigurationException::new);
         String monthType = monthDivisionView.getId();
         StructuralElementViewInterface dayDivisionView = nextSubView(rulesetManagement, monthDivisionView,
             acquisitionStage);
         daySimpleMetadataView = dayDivisionView.getDatesSimpleMetadata().orElseThrow(ConfigurationException::new);
         String dayType = dayDivisionView.getId();
 
         for (Iterator<LogicalDivision> yearFileMonthLogicalDivisionsIterator = yearFileYearLogicalDivision
                 .getChildren()
                 .iterator(), metaFileMonthLogicalDivisionsIterator = metaFileYearLogicalDivision
                         .getChildren().iterator(); yearFileMonthLogicalDivisionsIterator.hasNext()
                                 && metaFileMonthLogicalDivisionsIterator.hasNext();) {
             LogicalDivision yearFileMonthLogicalDivision = yearFileMonthLogicalDivisionsIterator
                     .next();
             LogicalDivision metaFileMonthLogicalDivision = metaFileMonthLogicalDivisionsIterator
                     .next();
             String month = getCompletedDate(yearFileMonthLogicalDivision, year);
             LogicalDivision processMonthLogicalDivision = computeIfAbsent(
                 processYearLogicalDivision, monthSimpleMetadataView, month, monthType);
             MetadataEditor.writeMetadataEntry(metaFileMonthLogicalDivision, monthSimpleMetadataView, month);
 
             createLinkStructureAndCopyDays(process, yearFileMonthLogicalDivision,
                 metaFileMonthLogicalDivision, month, dayType, processMonthLogicalDivision);
         }
     }
 
     private static StructuralElementViewInterface nextSubView(RulesetManagementInterface ruleset,
                                                              StructuralElementViewInterface superiorView, String acquisitionStage) {
 
         Map<String, String> allowedSubstructuralElements = superiorView.getAllowedSubstructuralElements();
         String subType = allowedSubstructuralElements.entrySet().iterator().next().getKey();
         return ruleset.getStructuralElementView(subType, acquisitionStage, Locale.LanguageRange.parse("en"));
     }
 
     private void createLinkStructureAndCopyDays(Process process,
             LogicalDivision yearFileMonthLogicalDivision,
             LogicalDivision metaFileMonthLogicalDivision, String month, String dayType,
             LogicalDivision processMonthLogicalDivision) {
 
         for (Iterator<LogicalDivision> yearFileDayLogicalDivisionsIterator = yearFileMonthLogicalDivision
                 .getChildren()
                 .iterator(), metaFileDayLogicalDivisionsIterator = metaFileMonthLogicalDivision
                         .getChildren().iterator(); yearFileDayLogicalDivisionsIterator.hasNext()
                                 && metaFileDayLogicalDivisionsIterator.hasNext();) {
             LogicalDivision yearFileDayLogicalDivision = yearFileDayLogicalDivisionsIterator
                     .next();
             LogicalDivision metaFileDayLogicalDivision = metaFileDayLogicalDivisionsIterator
                     .next();
             String day = getCompletedDate(yearFileDayLogicalDivision, month);
             LogicalDivision processDayLogicalDivision = computeIfAbsent(
                 processMonthLogicalDivision, daySimpleMetadataView, day, dayType);
             MetadataEditor.writeMetadataEntry(metaFileDayLogicalDivision, daySimpleMetadataView, day);
             createLinkStructureOfIssues(process, yearFileDayLogicalDivision,
                 processDayLogicalDivision);
         }
     }
 
     private void createLinkStructureOfIssues(Process process,
             LogicalDivision yearFileDayLogicalDivision,
             LogicalDivision processDayLogicalDivision) {
 
         MetadataEditor.addLink(processDayLogicalDivision, process.getId());
     }
 
     /**
      * Finds the logical division with the specified label, if it
      * exists, otherwise it creates.
      *
      * @param logicalDivision
      *            parent logical division
      * @param simpleMetadataView
      *            indication which metadata value is used to store the value
      * @param value
      *            the value
      * @return child with value
      */
     private static LogicalDivision computeIfAbsent(LogicalDivision logicalDivision,
             SimpleMetadataViewInterface simpleMetadataView, String value, String type) {
 
         int index = 0;
         for (LogicalDivision child : logicalDivision.getChildren()) {
             String firstSimpleMetadataValue = MetadataEditor.readSimpleMetadataValues(child, simpleMetadataView).get(0);
             int comparison = firstSimpleMetadataValue.compareTo(value);
             if (comparison <= -1) {
                 index++;
             } else if (comparison == 0) {
                 return child;
             } else {
                 break;
             }
         }
         LogicalDivision computed = new LogicalDivision();
         computed.setType(type);
         MetadataEditor.writeMetadataEntry(computed, simpleMetadataView, value);
         logicalDivision.getChildren().add(index, computed);
         return computed;
     }
 
     /**
      * Adds a date to get a complete date. In Production versions before 2.2,
      * the date is stored incompletely (as an integer). This is supplemented to
      * ISO if found. Otherwise just returns the date.
      *
      * @param logicalDivision
      *            the logical division that contains the date
      * @param previousLevel
      *            previous part of date
      * @return ISO date
      */
     private static String getCompletedDate(LogicalDivision logicalDivision, String previousLevel) {
         String date = MetadataEditor.getMetadataValue(logicalDivision, FIELD_TITLE_SORT);
         if (!date.matches("\\d{1,2}")) {
             return date;
         }
         logger.debug("Found integer date value ({}), supplementing to ISO date", date);
         StringBuilder composedDate = new StringBuilder();
         composedDate.append(previousLevel);
         composedDate.append('-');
         if (date.length() < 2) {
             composedDate.append('0');
         }
         composedDate.append(date);
         return composedDate.toString();
     }
 
     /**
      * Creates an overall process as a representation of the newspaper as a
      * whole.
      *
      * @throws ProcessGenerationException
      *             An error occurred while creating the process.
      * @throws IOException
      *             An error has occurred in the disk drive.
      */
     public void createOverallProcess() throws ProcessGenerationException, IOException, DAOException,
             CommandException {
         final long begin = System.nanoTime();
         logger.info("Creating overall process {}...", title);
         overallWorkpiece.getLogicalStructure().getChildren().clear();
 
         ProcessGenerator processGenerator = new ProcessGenerator();
         processGenerator.generateProcess(templateId, projectId);
         overallProcess = processGenerator.getGeneratedProcess();
         overallProcess.setTitle(getTitle());
         ProcessService.checkTasks(overallProcess, overallWorkpiece.getLogicalStructure().getType());
         processService.saveToDatabase(overallProcess);
         ServiceManager.getFileService().createProcessLocation(overallProcess);
         overallWorkpiece.setId(overallProcess.getId().toString());
         overallWorkpiece.getLogicalStructure().getMetadata().addAll(overallMetadata);
         addToBatch(overallProcess);
 
         logger.info("Process {} (ID {}) successfully created.", overallProcess.getTitle(), overallProcess.getId());
         if (logger.isTraceEnabled()) {
             logger.trace("Creating {} took {} ms.", overallProcess.getTitle(),
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     /**
      * Creates the next year process.
      *
      * @throws ProcessGenerationException
      *             if the process cannot be generated
      * @throws IOException
      *             if an I/O error occurs when accessing the file system
      * @throws DataException
      *             if there is an error saving the process
      * @throws DAOException
      *             if a process cannot be load from the database
      */
     public void createNextYearProcess() throws ProcessGenerationException, IOException, DAOException,
             CommandException {
         final long begin = System.nanoTime();
         Entry<String, LogicalDivision> yearToCreate = yearsIterator.next();
         String yearTitle = getYearTitle(yearToCreate.getKey());
         logger.info("Creating process for year {}, {}...", yearToCreate.getKey(), yearTitle);
         ProcessGenerator processGenerator = new ProcessGenerator();
         processGenerator.generateProcess(templateId, projectId);
         Process yearProcess = processGenerator.getGeneratedProcess();
         yearProcess.setTitle(yearTitle);
         ProcessService.checkTasks(yearProcess, yearToCreate.getValue().getType());
         // remove metadata from year (which originally relates to issue and was copied there)
         yearToCreate.getValue().getMetadata().clear();
         processService.saveToDatabase(yearProcess);
 
         MetadataEditor.addLink(overallWorkpiece.getLogicalStructure(), yearProcess.getId());
         if (!yearsIterator.hasNext()) {
             metsService.saveWorkpiece(overallWorkpiece, fileService.getMetadataFilePath(overallProcess, false, false));
         }
 
         yearProcess.setParent(overallProcess);
         overallProcess.getChildren().add(yearProcess);
         processService.saveToDatabase(yearProcess);
 
         ServiceManager.getFileService().createProcessLocation(yearProcess);
 
         createYearWorkpiece(yearToCreate, yearTitle, yearProcess);
 
         Collection<Integer> childIds = yearsChildren.get(yearToCreate.getKey());
         for (Integer childId : childIds) {
             Process child = processService.getById(childId);
             child.setParent(yearProcess);
             yearProcess.getChildren().add(child);
             processService.saveToDatabase(child);
         }
         if (WorkflowControllerService.allChildrenClosed(yearProcess)) {
             yearProcess.setSortHelperStatus("100000000");
         }
         processService.saveToDatabase(yearProcess);
         addToBatch(yearProcess);
 
         logger.info("Process {} (ID {}) successfully created.", yearProcess.getTitle(), yearProcess.getId());
         if (logger.isTraceEnabled()) {
             logger.trace("Creating {} took {} ms.", yearProcess.getTitle(),
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     private void createYearWorkpiece(Entry<String, LogicalDivision> yearToCreate, String yearTitle,
                                      Process yearProcess) throws IOException {
         Workpiece yearWorkpiece = new Workpiece();
         yearWorkpiece.setId(yearProcess.getId().toString());
         yearWorkpiece.setLogicalStructure(yearToCreate.getValue());
         StructuralElementViewInterface newspaperView = rulesetManagement.getStructuralElementView(
             yearWorkpiece.getLogicalStructure().getType(), acquisitionStage, Locale.LanguageRange.parse("de"));
         final Collection<String> processTitleKeys = rulesetManagement.getFunctionalKeys(FunctionalMetadata.PROCESS_TITLE);
         newspaperView.getAllowedMetadata().parallelStream().filter(SimpleMetadataViewInterface.class::isInstance)
                 .map(SimpleMetadataViewInterface.class::cast)
                 .filter(metadataView -> processTitleKeys.contains(metadataView.getId())).collect(Collectors.toList())
                 .forEach(yearView -> MetadataEditor.writeMetadataEntry(yearWorkpiece.getLogicalStructure(), yearView, yearTitle));
         metsService.saveWorkpiece(yearWorkpiece, fileService.getMetadataFilePath(yearProcess, false, false));
     }
 
     /**
      * Add the process to the newspaper batch.
      *
      * @param process
      *            process to be added
      */
     private void addToBatch(Process process) throws DAOException {
         Batch batch = batchService.getById(batchNumber);
         process.getBatches().add(batch);
         batch.getProcesses().add(process);
         processService.saveToDatabase(process);
         batchService.saveToDatabase(batch);
     }
 
     /**
      * Returns the number of years that have issues referenced.
      *
      * @return the number of years
      */
     public int getNumberOfYears() {
         return years.size();
     }
 
     /**
      * Returns the title of the year process to create next.
      *
      * @return the title of the year process to create next
      */
     public String getPendingYearTitle() {
         return getYearTitle(yearsIterator.peek().getKey());
     }
 
     /**
      * Returns the process title of the process with the given transfer index.
      *
      * @param transferIndex
      *            index of process in transfer object list
      * @return the process title
      */
     public String getProcessTitle(int transferIndex) {
         return processes.get(transferIndex).getTitle();
     }
 
     /**
      * Returns the title of the overall process.
      *
      * @return the title
      */
     public String getTitle() {
         return title;
     }
 
     /**
      * Returns the title of a year process.
      *
      * @param year
      *            year to return title
      * @return the title
      */
     private String getYearTitle(String year) {
         return title + '-' + year.split("/")[0];
     }
 
     /**
      * Returns whether there are more years to create.
      *
      * @return whether there are more years
      */
     public boolean hasNextYear() {
         if (Objects.isNull(yearsIterator)) {
             yearsIterator = new PeekingIterator(years.entrySet().iterator());
         }
         return yearsIterator.hasNext();
     }
 }