Coverage Summary for Class: NewspaperProcessesGenerator (org.kitodo.production.process)

Class Class, % Method, % Line, %
NewspaperProcessesGenerator 100% (1/1) 95,8% (23/24) 86,5% (275/318)


 /*
  * (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.process;
 
 import de.unigoettingen.sub.search.opac.ConfigOpac;
 import de.unigoettingen.sub.search.opac.ConfigOpacDoctype;
 
 import java.io.IOException;
 import java.net.URI;
 import java.time.LocalDate;
 import java.time.MonthDay;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale.LanguageRange;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import javax.naming.ConfigurationException;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.kitodo.api.Metadata;
 import org.kitodo.api.MetadataEntry;
 import org.kitodo.api.dataeditor.rulesetmanagement.ComplexMetadataViewInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.DatesSimpleMetadataViewInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.FunctionalMetadata;
 import org.kitodo.api.dataeditor.rulesetmanagement.MetadataViewInterface;
 import org.kitodo.api.dataeditor.rulesetmanagement.MetadataViewWithValuesInterface;
 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.api.dataformat.mets.LinkedMetsResource;
 import org.kitodo.config.ConfigProject;
 import org.kitodo.data.database.beans.Process;
 import org.kitodo.data.database.exceptions.DAOException;
 import org.kitodo.data.exceptions.DataException;
 import org.kitodo.exceptions.CommandException;
 import org.kitodo.exceptions.DoctypeMissingException;
 import org.kitodo.exceptions.ProcessGenerationException;
 import org.kitodo.production.forms.createprocess.ProcessFieldedMetadata;
 import org.kitodo.production.helper.Helper;
 import org.kitodo.production.metadata.MetadataEditor;
 import org.kitodo.production.model.bibliography.course.Course;
 import org.kitodo.production.model.bibliography.course.IndividualIssue;
 import org.kitodo.production.process.field.AdditionalField;
 import org.kitodo.production.services.ServiceManager;
 import org.kitodo.production.services.data.ProcessService;
 import org.kitodo.production.services.data.RulesetService;
 import org.kitodo.production.services.dataformat.MetsService;
 import org.kitodo.production.services.file.FileService;
 
 /**
  * A generator for newspaper processes.
  */
 public class NewspaperProcessesGenerator extends ProcessGenerator {
     private static final Logger logger = LogManager.getLogger(NewspaperProcessesGenerator.class);
 
     /**
      * Language for the ruleset. Since we run headless here, English only.
      */
     public static final List<LanguageRange> ENGLISH = LanguageRange.parse("en");
 
     /**
      * Number of steps the long-running task has to go through for
      * initialization.
      */
     private static final int NUMBER_OF_INIT_STEPS = 1;
 
     /**
      * Number of steps the long-running task must complete to complete.
      */
     private static final int NUMBER_OF_COMPLETION_STEPS = 1;
 
     /**
      * Date format pattern indicating a double year. A double year is a time
      * span with the length of one year, starting on a day different from
      * January 1ˢᵗ and spanning two complementary parts of two subsequent
      * calendar years.
      */
     private static final String PATTERN_DOUBLE_YEAR = "yyyy/yyyy";
 
     /**
      * Acquisition stage of newspaper generator.
      */
     private final String acquisitionStage = "";
 
     /**
      * This class requires service for files.
      */
     private final FileService fileService = ServiceManager.getFileService();
 
     /**
      * This class requires service for METS.
      */
     private final MetsService metsService = ServiceManager.getMetsService();
 
     /**
      * This class requires service for process.
      */
     private final ProcessService processService = ServiceManager.getProcessService();
 
     /**
      * This class requires service for rule definitions.
      */
     private final RulesetService rulesetService = ServiceManager.getRulesetService();
 
     /**
      * This is the supreme process of the newspaper.
      */
     private final Process overallProcess;
 
     /**
      * The appearance history for which operations are to be created.
      */
     private final Course course;
 
     /**
      * The current step. This class operates step by step and the long running
      * task can always be paused between two steps in Task Manager.
      */
     private int currentStep = 0;
 
     /**
      * Specifies which year is currently being processed.
      */
     private String currentYear;
 
     /**
      * An interface view for simple metadata that maps the date of the day.
      */
     private DatesSimpleMetadataViewInterface daySimpleMetadataView;
 
     /**
      * Which logical division type are the days.
      */
     private String dayType;
 
     /**
      * Information from the rule set about the issue.
      */
     StructuralElementViewInterface issueDivisionView;
 
     /**
      * Views of metadata to add process title to issue processes.
      */
     private Collection<SimpleMetadataViewInterface> issueProcessTitleViews;
 
     /**
      * An interface view for simple metadata that maps the date of the month.
      */
     private DatesSimpleMetadataViewInterface monthSimpleMetadataView;
 
     /**
      * Which logical division type are the months.
      */
     private String monthType;
 
     /**
      * Views of metadata to add process title to the overall newspaper process.
      */
     private Collection<SimpleMetadataViewInterface> newspaperProcessTitleViews;
 
     /**
      * Uniform resource identifier of the location of the serialization of the
      * overall media presentation description.
      */
     private URI overallMetadataFileUri;
 
     /**
      * Object model of the overall media presentation description.
      */
     private Workpiece overallWorkpiece;
 
     /**
      * List of processes to be created. A process is characterized here only by
      * the issues contained therein.
      */
     private List<List<IndividualIssue>> processesToCreate;
 
     /**
      * Build statements for the process title, which can be interpreted by the
      * title generator.
      */
     private Optional<String> yearTitleDefinition;
 
     /**
      * The title generator is used to create the process titles.
      */
     private TitleGenerator titleGenerator;
 
     /**
      * Uniform resource identifier of the location of the serialization of the
      * annual media presentation description.
      */
     private URI yearMetadataFileUri;
 
     /**
      * This is the annual process which is currently being processed.
      */
     private Process yearProcess;
 
     /**
      * Views of metadata to add process title to year processes.
      */
     private Collection<SimpleMetadataViewInterface> yearProcessTitleViews;
 
     /**
      * An interface view for simple metadata that maps the date of the year.
      */
     private DatesSimpleMetadataViewInterface yearSimpleMetadataView;
 
     /**
      * Which logical division type are the years.
      */
     private String yearType;
 
     /**
      * Object model of the year media presentation description.
      */
     private Workpiece yearWorkpiece;
 
     /**
      * Creates a new newspaper process generator.
      *
      * @param overallProcess
      *            Process that represents the entirety of the newspaper
      * @param course
      *            object model of the course of the issue
      */
     public NewspaperProcessesGenerator(Process overallProcess, Course course) {
         this.overallProcess = overallProcess;
         this.course = course;
     }
 
     /**
      * Returns the progress of the newspaper processes generator.
      *
      * @return the progress
      */
     public int getProgress() {
         return currentStep;
     }
 
     /**
      * Returns the number of steps of the newspaper processes generator. Note
      * that the number of steps may change in the future and needs to be
      * updated.
      *
      * @return the number of steps
      */
     public int getNumberOfSteps() {
         return NUMBER_OF_INIT_STEPS + (Objects.isNull(processesToCreate) ? 0 : processesToCreate.size())
                 + NUMBER_OF_COMPLETION_STEPS;
     }
 
     /**
      * Works the next step of the long-running task.
      *
      * @throws ConfigurationException
      *             if the configuration is wrong
      * @throws DAOException
      *             if an error occurs while saving in the database
      * @throws DataException
      *             if an error occurs while saving in the database
      * @throws IOException
      *             if something goes wrong when reading or writing one of the
      *             affected files
      * @throws ProcessGenerationException
      *             if there is a "CurrentNo" item in the projects configuration,
      *             but its value cannot be evaluated to an integer
      */
     public boolean nextStep() throws ConfigurationException, DAOException, DataException, IOException,
             ProcessGenerationException, DoctypeMissingException, CommandException {
 
         if (currentStep == 0) {
             initialize();
             if (isDuplicatedTitles()) {
                 Helper.setErrorMessage("duplicatedTitles");
                 return false;
             }
         } else if (currentStep - NUMBER_OF_INIT_STEPS < processesToCreate.size()) {
             createProcess(currentStep - NUMBER_OF_INIT_STEPS);
         } else {
             finish();
         }
         currentStep++;
         return true;
     }
 
     /**
      * Initializes the newspaper process generator.
      *
      * @throws ConfigurationException
      *             if the configuration is wrong
      * @throws IOException
      *             if something goes wrong when reading or writing one of the
      *             affected files
      */
     public void initialize() throws ConfigurationException, IOException, DoctypeMissingException {
         final long begin = System.nanoTime();
 
         overallMetadataFileUri = processService.getMetadataFileUri(overallProcess);
         overallWorkpiece = metsService.loadWorkpiece(overallMetadataFileUri);
 
         initializeRulesetFields(overallWorkpiece.getLogicalStructure().getType());
 
         ConfigProject configProject = new ConfigProject(overallProcess.getProject().getTitle());
 
         Collection<MetadataViewInterface> allowedMetadata = rulesetService.openRuleset(overallProcess.getRuleset())
                 .getStructuralElementView(overallWorkpiece.getLogicalStructure().getType(), acquisitionStage, ENGLISH)
                 .getAllowedMetadata();
 
         titleGenerator = initializeTitleGenerator(configProject, overallWorkpiece, allowedMetadata);
 
         processesToCreate = course.getProcesses();
 
         if (logger.isTraceEnabled()) {
             logger.trace("Initialization took {} ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     /**
      * Initializes the class fields related that hold information to be obtained
      * from the ruleset.
      *
      * @param newspaperType
      *            ruleset type of the overall newspaper process
      * @throws ConfigurationException
      *             if the configuration is wrong
      * @throws IOException
      *             if something goes wrong when reading or writing one of the
      *             affected files
      */
     private void initializeRulesetFields(String newspaperType) throws ConfigurationException, IOException {
         RulesetManagementInterface ruleset = rulesetService.openRuleset(overallProcess.getRuleset());
         StructuralElementViewInterface newspaperView = ruleset.getStructuralElementView(newspaperType, acquisitionStage, ENGLISH);
         StructuralElementViewInterface yearDivisionView = nextSubView(ruleset, newspaperView, acquisitionStage);
         yearSimpleMetadataView = yearDivisionView.getDatesSimpleMetadata().orElseThrow(ConfigurationException::new);
         yearTitleDefinition = yearDivisionView.getProcessTitle();
         yearType = yearDivisionView.getId();
         StructuralElementViewInterface monthDivisionView = nextSubView(ruleset, yearDivisionView, acquisitionStage);
         monthSimpleMetadataView = monthDivisionView.getDatesSimpleMetadata().orElseThrow(ConfigurationException::new);
         monthType = monthDivisionView.getId();
         StructuralElementViewInterface dayDivisionView = nextSubView(ruleset, monthDivisionView, acquisitionStage);
         daySimpleMetadataView = dayDivisionView.getDatesSimpleMetadata().orElseThrow(ConfigurationException::new);
         dayType = dayDivisionView.getId();
         issueDivisionView = nextSubView(ruleset, dayDivisionView, acquisitionStage);
 
         final Collection<String> processTitleKeys = ruleset.getFunctionalKeys(FunctionalMetadata.PROCESS_TITLE);
         newspaperProcessTitleViews = newspaperView
                 .getAllowedMetadata()
                 .parallelStream().filter(SimpleMetadataViewInterface.class::isInstance)
                 .map(SimpleMetadataViewInterface.class::cast)
                 .filter(metadataView -> processTitleKeys.contains(metadataView.getId())).collect(Collectors.toList());
         yearProcessTitleViews = yearDivisionView.getAllowedMetadata()
                 .parallelStream().filter(SimpleMetadataViewInterface.class::isInstance)
                 .map(SimpleMetadataViewInterface.class::cast)
                 .filter(metadataView -> processTitleKeys.contains(metadataView.getId())).collect(Collectors.toList());
         issueProcessTitleViews = issueDivisionView.getAllowedMetadata()
                 .parallelStream().filter(SimpleMetadataViewInterface.class::isInstance)
                 .map(SimpleMetadataViewInterface.class::cast)
                 .filter(metadataView -> processTitleKeys.contains(metadataView.getId())).collect(Collectors.toList());
     }
 
     /**
      * Returns the next sub-view relative to given view from the ruleset.
      *
      * @param ruleset
      *            ruleset to return the sub-view from
      * @param superiorView
      *            relative superior view
      * @return the sub-view
      */
     public 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, ENGLISH);
     }
 
     /**
      * Initializes the title generator.
      *
      * @param configProject
      *            the config project
      * @param allowedMetadata
      *            allowed Metadata views
      * @return the initialized title generator
      */
     private static TitleGenerator initializeTitleGenerator(ConfigProject configProject, Workpiece workpiece,
             Collection<MetadataViewInterface> allowedMetadata)
             throws DoctypeMissingException {
 
         LogicalDivision logicalStructure = workpiece.getLogicalStructure();
         Map<String, Map<String, String>> metadata = new HashMap<>(4);
         Map<String, String> topstruct = getMetadataEntries(logicalStructure.getMetadata());
         metadata.put("topstruct", topstruct);
         List<LogicalDivision> children = logicalStructure.getChildren();
         metadata.put("firstchild",
             children.isEmpty() ? Collections.emptyMap() : getMetadataEntries(children.get(0).getMetadata()));
         metadata.put("physSequence", getMetadataEntries(workpiece.getPhysicalStructure().getMetadata()));
 
         String docType = null;
         for (ConfigOpacDoctype configOpacDoctype : ConfigOpac.getAllDoctypes()) {
             if (configOpacDoctype.getRulesetType().equals(logicalStructure.getType())) {
                 docType = configOpacDoctype.getTitle();
                 break;
             }
         }
 
         List<AdditionalField> projectAdditionalFields = configProject.getAdditionalFields();
         ProcessFieldedMetadata table = new ProcessFieldedMetadata();
         for (AdditionalField additionalField : projectAdditionalFields) {
             if (isDocTypeAndNotIsNotDoctype(additionalField, docType)) {
                 String value = metadata.getOrDefault(additionalField.getDocStruct(), Collections.emptyMap())
                         .get(additionalField.getMetadata());
                 List<MetadataViewInterface> filteredViews = allowedMetadata
                         .stream()
                         .filter(v -> v.getId().equals(additionalField.getMetadata()))
                         .collect(Collectors.toList());
                 if (!filteredViews.isEmpty()) {
                     MetadataEntry metadataEntry = new MetadataEntry();
                     metadataEntry.setValue(value);
                     if (filteredViews.get(0).isComplex()) {
                         table.createMetadataGroupPanel((ComplexMetadataViewInterface) filteredViews.get(0),
                             Collections.singletonList(metadataEntry));
                     } else {
                         table.createMetadataEntryEdit((SimpleMetadataViewInterface) filteredViews.get(0),
                             Collections.singletonList(metadataEntry));
                     }
                 }
             }
         }
         return new TitleGenerator(topstruct.getOrDefault("TSL_ATS", ""), table.getRows());
     }
 
     /**
      * Returns whether an additional field is assigned to the doc type and not
      * excluded from it. The {@code isDocType}s and {@code isNotDoctype}s are a
      * list as a string, separated by a horizontal line ({@code |}, U+007C).
      *
      * @param additionalField
      *            the field in question
      * @param docType
      *            the doc type used
      * @return whether the field is assigned and not excluded
      */
     private static boolean isDocTypeAndNotIsNotDoctype(AdditionalField additionalField, String docType) {
         boolean isDocType = false;
         boolean isNotDoctype = false;
         String isDocTypes = additionalField.getIsDocType();
         if (Objects.nonNull(isDocTypes)) {
             for (String isDocTypeOption : isDocTypes.split("\\|")) {
                 if (isDocTypeOption.equals(docType)) {
                     isDocType = true;
                     break;
                 }
             }
         }
         String isNotDoctypes = additionalField.getIsNotDoctype();
         if (Objects.nonNull(isNotDoctypes)) {
             for (String isNotDoctypeOption : isNotDoctypes.split("\\|")) {
                 if (isNotDoctypeOption.equals(docType)) {
                     isNotDoctype = true;
                     break;
                 }
             }
         }
         return isDocType ^ !isNotDoctype;
     }
 
     /**
      * Reduces the metadata to metadata entries and returns them as a map.
      *
      * @param metadata
      *            all the metadata
      * @return the metadata entries as map
      */
     private static Map<String, String> getMetadataEntries(Collection<Metadata> metadata) {
         return metadata.parallelStream().filter(MetadataEntry.class::isInstance)
                 .map(MetadataEntry.class::cast).collect(Collectors.toMap(Metadata::getKey, MetadataEntry::getValue,
                     (one, another) -> one + ", " + another));
     }
 
     private void createProcess(int index) throws DAOException, DataException, IOException, ProcessGenerationException,
             CommandException {
         final long begin = System.nanoTime();
 
         List<IndividualIssue> individualIssuesForProcess = processesToCreate.get(index);
         if (individualIssuesForProcess.isEmpty()) {
             return;
         }
 
         IndividualIssue firstIssue = individualIssuesForProcess.get(0);
         Map<String, String> genericFields = firstIssue.getGenericFields();
         prepareTheAppropriateYearProcess(dateMark(yearSimpleMetadataView.getScheme(), firstIssue.getDate()),
             genericFields);
 
         generateProcess(overallProcess.getTemplate().getId(), overallProcess.getProject().getId());
 
         String title = makeTitle(issueDivisionView.getProcessTitle().orElse("+'_'+#YEAR+#MONTH+#DAY+#ISSU"), genericFields);
         getGeneratedProcess().setTitle(title);
         getGeneratedProcess().setParent(yearProcess);
         yearProcess.getChildren().add(getGeneratedProcess());
         processService.save(getGeneratedProcess(), true);
         createMetadataFileForProcess(individualIssuesForProcess, title);
 
         if (logger.isTraceEnabled()) {
             logger.trace("Creating newspaper process {} took {} ms", title,
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     /**
      * Generate process title.
      * @param definition as String
      * @param genericFields a map with generic fields that can be configured for process
      * @return process title as String
      */
     public String makeTitle(String definition, Map<String, String> genericFields) throws ProcessGenerationException {
         String title;
         boolean prefixWithProcessTitle = definition.startsWith("+");
         if (prefixWithProcessTitle) {
             definition = definition.substring(1);
         }
         title = titleGenerator.generateTitle(definition, genericFields);
         if (prefixWithProcessTitle) {
             title = overallProcess.getTitle().concat(title);
         }
         return title;
     }
 
     private void createMetadataFileForProcess(List<IndividualIssue> individualIssues, String title)
             throws IOException, CommandException {
 
         LogicalDivision logicalStructure = new LogicalDivision();
         MetadataEntry dateMetadataEntry = new MetadataEntry();
         dateMetadataEntry.setKey(monthSimpleMetadataView.getId());
         dateMetadataEntry.setValue(dateMark(monthSimpleMetadataView.getScheme(), individualIssues.get(0).getDate()));
         logicalStructure.getMetadata().add(dateMetadataEntry);
 
         for (IndividualIssue individualIssue : individualIssues) {
 
             String monthMark = dateMark(monthSimpleMetadataView.getScheme(), individualIssue.getDate());
             LogicalDivision yearMonth = getOrCreateLogicalDivision(yearWorkpiece.getLogicalStructure(),
                 monthType, monthSimpleMetadataView, monthMark);
             String dayMark = dateMark(daySimpleMetadataView.getScheme(), individualIssue.getDate());
             final LogicalDivision processDay = getOrCreateLogicalDivision(logicalStructure, null,
                 daySimpleMetadataView, dayMark);
             final LogicalDivision yearDay = getOrCreateLogicalDivision(yearMonth, dayType,
                 daySimpleMetadataView, dayMark);
 
             LogicalDivision processIssue = new LogicalDivision();
             processIssue.setType(issueDivisionView.getId());
             for (SimpleMetadataViewInterface issueProcessTitleView : issueProcessTitleViews) {
                 MetadataEditor.writeMetadataEntry(processIssue, issueProcessTitleView, title);
             }
             addCustomMetadata(individualIssue, processIssue);
             processDay.getChildren().add(processIssue);
 
             LogicalDivision yearIssue = new LogicalDivision();
             LinkedMetsResource linkToProcess = new LinkedMetsResource();
             linkToProcess.setLoctype("Kitodo.Production");
             linkToProcess.setUri(processService.getProcessURI(getGeneratedProcess()));
             yearIssue.setLink(linkToProcess);
             yearDay.getChildren().add(yearIssue);
         }
 
         Workpiece workpiece = new Workpiece();
         workpiece.setLogicalStructure(logicalStructure);
         workpiece.setId(getGeneratedProcess().getId().toString());
         fileService.createProcessLocation(getGeneratedProcess());
         final URI metadataFileUri = processService.getMetadataFileUri(getGeneratedProcess());
         metsService.saveWorkpiece(workpiece, metadataFileUri);
     }
 
     private void addCustomMetadata(IndividualIssue definition, LogicalDivision issue) {
         Collection<Metadata> entered = new ArrayList<>();
         MonthDay yearBegin = yearSimpleMetadataView.getYearBegin();
         for (Metadata metadata : definition.getMetadata(yearBegin.getMonthValue(),
             yearBegin.getDayOfMonth())) {
             entered.add(metadata);
         }
         List<MetadataViewWithValuesInterface> viewsWithValues = issueDivisionView
                 .getSortedVisibleMetadata(entered, Collections.emptyList());
         for (MetadataViewWithValuesInterface viewWithValues : viewsWithValues) {
             for (Metadata metadata : viewWithValues.getValues()) {
                 if (viewWithValues.getMetadata().isPresent()) {
                     MetadataViewInterface view = viewWithValues.getMetadata().get();
                     if (metadata instanceof MetadataEntry && view instanceof SimpleMetadataViewInterface) {
                         MetadataEditor.writeMetadataEntry(issue, (SimpleMetadataViewInterface) view,
                             ((MetadataEntry) metadata).getValue());
                     } else {
                         logger.warn("Cannot add metadata value \"{}\" of type {} to {}: {} is a metadata group",
                             ((MetadataEntry) metadata).getValue(), metadata.getKey(), issue.getType(), view.getId());
                     }
                 } else {
                     logger.warn(
                         "Cannot add metadata value \"{}\" of type {} to NewspaperIssue: type is hidden in acquisition stage \"{}\".",
                         ((MetadataEntry) metadata).getValue(), metadata.getKey(), issue.getType(), acquisitionStage);
                 }
             }
         }
     }
 
     /**
      * Creates a date indicator according to the specified schema for the given
      * date. Normally, the date indicator is generated with the date formatter.
      * Exception is the indication of a double year. In this case, the
      * information depends on the beginning of the year and must first be
      * determined.
      *
      * @param scheme
      *            scheme for the date indicator
      * @param date
      *            date for the date mark
      * @return the formatted date indicator
      */
     private String dateMark(String scheme, LocalDate date) {
         if (PATTERN_DOUBLE_YEAR.equals(scheme)) {
             int firstYear = date.getYear();
             MonthDay yearBegin = yearSimpleMetadataView.getYearBegin();
             LocalDate yearStartThisYear = LocalDate.of(firstYear, yearBegin.getMonth(), yearBegin.getDayOfMonth());
             if (date.isBefore(yearStartThisYear)) {
                 firstYear--;
             }
             return String.format("%04d/%04d", firstYear, firstYear + 1);
         } else {
             DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern(scheme);
             return yearFormatter.format(date);
         }
     }
 
     private void prepareTheAppropriateYearProcess(String yearMark, Map<String, String> genericFields)
             throws DAOException, DataException, ProcessGenerationException, IOException, CommandException {
 
         if (yearMark.equals(currentYear)) {
             return;
         } else if (Objects.nonNull(currentYear)) {
             saveAndCloseCurrentYearProcess();
         }
         if (!openExistingYearProcess(yearMark)) {
             createNewYearProcess(yearMark, genericFields);
         }
     }
 
     private void saveAndCloseCurrentYearProcess() throws DataException, IOException {
         final long begin = System.nanoTime();
 
         metsService.saveWorkpiece(yearWorkpiece, yearMetadataFileUri);
         ProcessService.checkTasks(yearProcess, yearWorkpiece.getLogicalStructure().getType());
         processService.save(yearProcess, true);
 
         this.yearProcess = null;
         this.yearWorkpiece = null;
         this.yearMetadataFileUri = null;
         String year = currentYear;
         this.currentYear = null;
 
         if (logger.isTraceEnabled()) {
             logger.trace("Saving year process for {} took {} ms", year,
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     private boolean openExistingYearProcess(String yearMark)
             throws DAOException, IOException {
         final long begin = System.nanoTime();
 
         boolean couldOpenExistingProcess = false;
         for (LogicalDivision firstLevelChild : overallWorkpiece.getLogicalStructure().getChildren()) {
             LinkedMetsResource firstLevelChildLink = firstLevelChild.getLink();
             if (Objects.isNull(firstLevelChildLink)) {
                 continue;
             }
             Process linkedProcess = processService
                     .getById(processService.processIdFromUri(firstLevelChildLink.getUri()));
             URI metadataFileUri = processService.getMetadataFileUri(linkedProcess);
             Workpiece workpiece = metsService.loadWorkpiece(metadataFileUri);
             String yearMetadataEntry = null;
             if (yearSimpleMetadataView.getId().equals("ORDERLABEL")) {
                 yearMetadataEntry = workpiece.getLogicalStructure().getOrderlabel();
             }
             for (Metadata metadata : workpiece.getLogicalStructure().getMetadata()) {
                 if (metadata.getKey().equals(yearSimpleMetadataView.getId()) && metadata instanceof MetadataEntry) {
                     yearMetadataEntry = ((MetadataEntry) metadata).getValue();
                     break;
                 }
             }
             if (Objects.isNull(yearMetadataEntry)) {
                 continue;
             }
             couldOpenExistingProcess = yearMetadataEntry.equals(yearMark);
             if (couldOpenExistingProcess) {
                 this.yearProcess = linkedProcess;
                 this.yearWorkpiece = workpiece;
                 this.yearMetadataFileUri = metadataFileUri;
                 this.currentYear = yearMark;
                 break;
             }
         }
         if (logger.isTraceEnabled()) {
             logger.trace("Searching year process for {} took {} ms", yearMark,
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
         return couldOpenExistingProcess;
     }
 
     private void createNewYearProcess(String yearMark, Map<String, String> genericFields)
             throws ProcessGenerationException, DataException, IOException, CommandException {
         final long begin = System.nanoTime();
 
         generateProcess(overallProcess.getTemplate().getId(), overallProcess.getProject().getId());
 
         String title = makeTitle(yearTitleDefinition.orElse("+'_'+#YEAR"), genericFields);
         getGeneratedProcess().setTitle(title);
         ProcessService.checkTasks(getGeneratedProcess(), yearType);
         processService.save(getGeneratedProcess(), true);
 
         getGeneratedProcess().setParent(overallProcess);
         overallProcess.getChildren().add(getGeneratedProcess());
         processService.save(getGeneratedProcess(), true);
 
         fileService.createProcessLocation(getGeneratedProcess());
         final URI metadataFileUri = processService.getMetadataFileUri(getGeneratedProcess());
 
         LogicalDivision newYearChild = new LogicalDivision();
         LinkedMetsResource link = new LinkedMetsResource();
         link.setLoctype("Kitodo.Production");
         link.setUri(processService.getProcessURI(getGeneratedProcess()));
         newYearChild.setLink(link);
         overallWorkpiece.getLogicalStructure().getChildren().add(newYearChild);
 
         LogicalDivision logicalStructure = new LogicalDivision();
         logicalStructure.setType(yearType);
         MetadataEditor.writeMetadataEntry(logicalStructure, yearSimpleMetadataView, yearMark);
         for (SimpleMetadataViewInterface yearProcessTitleView : yearProcessTitleViews) {
             MetadataEditor.writeMetadataEntry(logicalStructure, yearProcessTitleView, title);
         }
         Workpiece workpiece = new Workpiece();
         workpiece.setLogicalStructure(logicalStructure);
         workpiece.setId(getGeneratedProcess().getId().toString());
 
         this.yearProcess = getGeneratedProcess();
         this.yearWorkpiece = workpiece;
         this.yearMetadataFileUri = metadataFileUri;
         this.currentYear = yearMark;
 
         if (logger.isTraceEnabled()) {
             logger.trace("Creating year process for {} took {} ms", yearMark,
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     private LogicalDivision getOrCreateLogicalDivision(
             LogicalDivision logicalDivision, String childType,
             SimpleMetadataViewInterface identifierMetadata,
             String identifierMetadataValue) {
 
         for (LogicalDivision child : logicalDivision.getChildren()) {
             if (MetadataEditor.readSimpleMetadataValues(child, identifierMetadata).contains(identifierMetadataValue)) {
                 return child;
             }
         }
 
         LogicalDivision createdChild = new LogicalDivision();
         createdChild.setType(childType);
         MetadataEditor.writeMetadataEntry(createdChild, identifierMetadata, identifierMetadataValue);
         logicalDivision.getChildren().add(createdChild);
         return createdChild;
     }
 
     private void finish() throws DataException, IOException {
         final long begin = System.nanoTime();
 
         saveAndCloseCurrentYearProcess();
         for (SimpleMetadataViewInterface newspaperProcessTitleView : newspaperProcessTitleViews) {
             MetadataEditor.writeMetadataEntry(overallWorkpiece.getLogicalStructure(), newspaperProcessTitleView,
                 overallProcess.getTitle());
         }
         metsService.saveWorkpiece(overallWorkpiece, overallMetadataFileUri);
         ProcessService.checkTasks(overallProcess, overallWorkpiece.getLogicalStructure().getType());
         processService.save(overallProcess,true);
 
         if (logger.isTraceEnabled()) {
             logger.trace("Finish took {} ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     /**
      * Check if process with the same processtitle already exists.
      * @return 'true' if Duplicated titles are found and 'false' if not
      */
     public boolean isDuplicatedTitles() throws ProcessGenerationException, DataException {
         List<List<IndividualIssue>> processes = course.getProcesses();
         List<String> issueTitles = new ArrayList<>();
         boolean check = false;
         for (List<IndividualIssue> individualProcess : processes) {
             for (IndividualIssue individualIssue : individualProcess) {
                 Map<String, String> genericFields = individualIssue.getGenericFields();
                 String title = makeTitle(issueDivisionView.getProcessTitle().orElse("+'_'+#YEAR+#MONTH+#DAY+#ISSU"),
                     genericFields);
                 if (!ServiceManager.getProcessService().findByTitle(title).isEmpty() || issueTitles.contains(title)) {
                     Helper.setErrorMessage("duplicatedTitles", individualIssue.toString());
                     check = true;
                 }
                 issueTitles.add(title);
             }
         }
         return check;
     }
 }