Coverage Summary for Class: HierarchyMigrationTask (org.kitodo.production.helper.tasks)

Class Class, % Method, % Line, %
HierarchyMigrationTask 100% (1/1) 76,5% (13/17) 71% (93/131)


 /*
  * (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.helper.tasks;
 
 import java.io.IOException;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 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.dataformat.LogicalDivision;
 import org.kitodo.api.dataformat.Workpiece;
 import org.kitodo.api.dataformat.mets.LinkedMetsResource;
 import org.kitodo.data.database.beans.Process;
 import org.kitodo.data.database.beans.Project;
 import org.kitodo.data.database.exceptions.DAOException;
 import org.kitodo.exceptions.CommandException;
 import org.kitodo.exceptions.ProcessGenerationException;
 import org.kitodo.production.helper.Helper;
 import org.kitodo.production.metadata.MetadataEditor;
 import org.kitodo.production.process.ProcessGenerator;
 import org.kitodo.production.services.ServiceManager;
 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;
 
 public class HierarchyMigrationTask extends EmptyTask {
     private static final Logger logger = LogManager.getLogger(HierarchyMigrationTask.class);
 
     /**
      * Service that contains the meta-data editor.
      */
     private static final DataEditorService dataEditorService = ServiceManager.getDataEditorService();
 
     /**
      * Service to access files on the storage.
      */
     private static final FileService fileService = ServiceManager.getFileService();
 
     /**
      * Service to read and write METS file format.
      */
     private static final MetsService metsService = ServiceManager.getMetsService();
 
     /**
      * Service to generate processes.
      */
     private final ProcessGenerator processGenerator = new ProcessGenerator();
 
     /**
      * Service to read and write Process objects in the database or search
      * engine index.
      */
     private static final ProcessService processService = ServiceManager.getProcessService();
 
     /**
      * This map contains information about parent processes that have already
      * been created. Key is the identifier, the value is the process ID, then
      * the current numbers of child links already inserted. The current number
      * should not be confused with the process ID. It is not the process ID, but
      * a sort criterion that is read from the metadata. The background is that
      * during the migration the issues or volumes are found in any order, but
      * should be linked in ascending order according to their current number in
      * the parent process. Therefore, the sequential numbers of the already
      * linked children must be stored temporarily during the migration in order
      * to be able to determine the correct insertion position of another link.
      */
     private Map<String, List<Integer>> parentProcesses = new HashMap<>();
 
     /**
      * List of all processes to migrate.
      */
     private List<Integer> processesList;
 
     /**
      * All processes belong to a project.
      */
     private Collection<Project> projects;
 
     /**
      * The progress, for the progress bar.
      */
     private int progress = 0;
 
     public HierarchyMigrationTask(Collection<Project> projects) {
         super(projects.stream().map(Project::getTitle).collect(Collectors.joining(", ")));
         this.projects = projects;
     }
 
     /**
      * Clone constructor. Provides the ability to restart the task if it was
      * previously interrupted.
      *
      * @param source
      *            terminated thread
      */
     private HierarchyMigrationTask(HierarchyMigrationTask source) {
         super(source);
         this.processesList = source.processesList;
         this.projects = source.projects;
         this.progress = source.progress;
     }
 
     /**
      * Defines the display name of the task in the task manager. Usually this is
      * the class name, but here it is different from the class name in lower
      * case.
      */
     @Override
     public String getDisplayName() {
         return Helper.getTranslation(getClass().getSimpleName().toLowerCase());
     }
 
     /**
      * The {@code run()} method is called when the thread starts. It initializes
      * the process list (if this has not already been done), processes it and
      * updates the progress display in the screen output.
      */
     @Override
     public void run() {
         try {
             if (Objects.isNull(processesList)) {
                 processesList = projects.parallelStream().flatMap(project -> project.getProcesses().parallelStream())
                         .map(Process::getId).collect(Collectors.toList());
             }
             while (progress < processesList.size()) {
                 Process process = processService.getById(processesList.get(progress));
                 if (fileService.processOwnsAnchorXML(process) && !fileService.processOwnsYearXML(process)) {
                     setWorkDetail(process.getTitle());
                     migrate(process);
                 }
                 super.setProgress(100 * ++progress / processesList.size());
                 if (Thread.currentThread().isInterrupted()) {
                     return;
                 }
             }
         } catch (IOException | DAOException | ProcessGenerationException | CommandException e) {
             setException(e);
         }
     }
 
     /**
      * This function does the actual work and migrates exactly one process.
      *
      * @param process
      *            process to migrate
      */
     void migrate(Process process) throws IOException, ProcessGenerationException, DAOException, CommandException {
         logger.info("Starting to convert process {} (ID {})...", process.getTitle(), process.getId());
         long begin = System.nanoTime();
         migrateMetadataFiles(process);
         Optional<String> parentId = getParentRecordId(process);
         if (parentId.isPresent()) {
             if (parentProcesses.containsKey(parentId.get())) {
                 linkProcessInParent(process, parentProcesses.get(parentId.get()));
             } else {
                 parentProcesses.put(parentId.get(), createParentProcess(process));
             }
             renameAnchorFile(process);
         } else {
             logger.warn("Process {} (ID {}): Parent has no identifier! Cannot create parent process.",
                 process.getTitle(), process.getId());
         }
         if (logger.isTraceEnabled()) {
             logger.trace("Converting {} took {} ms.", process.getTitle(),
                 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
     }
 
     /**
      * The metadata file and the anchor file are converted to the new internal
      * format using XSLT.
      *
      * @param process
      *            process to migrate
      */
     private static void migrateMetadataFiles(Process process) throws IOException {
         URI metadataFilePath = fileService.getMetadataFilePath(process, true, true);
         dataEditorService.readData(metadataFilePath);
         URI anchorFilePath = fileService.createAnchorFile(metadataFilePath);
         dataEditorService.readData(anchorFilePath);
     }
 
     /**
      * Reads the parent record identifier from the anchor file.
      */
     private static Optional<String> getParentRecordId(Process process) throws IOException {
         URI metadataFilePath = fileService.getMetadataFilePath(process);
         URI anchorFilePath = fileService.createAnchorFile(metadataFilePath);
         Workpiece anchorWorkpiece = metsService.loadWorkpiece(anchorFilePath);
         Optional<String> parentRecordId = anchorWorkpiece.getLogicalStructure().getMetadata().parallelStream()
                 .filter(metadata -> metadata.getKey().equals("CatalogIDDigital"))
                 .filter(MetadataEntry.class::isInstance).map(MetadataEntry.class::cast).map(MetadataEntry::getValue)
                 .findFirst();
         return parentRecordId;
     }
 
     /**
      * Creates a new parent process. The process is created in the database, the
      * process folder is created on the file system and the METS file is
      * written. The METS file of the child process is migrated and a link
      * between the processes is written in the database.
      *
      * @param childProcess
      *            process to migrate
      * @return a data object with the ID of the created parent process and the
      *         current number of the child process
      */
     private List<Integer> createParentProcess(Process childProcess)
             throws ProcessGenerationException, IOException, CommandException, DAOException {
 
         processGenerator.generateProcess(childProcess.getTemplate().getId(), childProcess.getProject().getId());
         Process parentProcess = processGenerator.getGeneratedProcess();
         processService.saveToDatabase(parentProcess);
         fileService.createProcessLocation(parentProcess);
         createParentMetsFile(childProcess);
         checkTaskAndId(parentProcess);
         processService.saveToDatabase(parentProcess);
         parentProcess = ServiceManager.getProcessService().getById(parentProcess.getId());
         ArrayList<Integer> parentData = new ArrayList<>();
         parentData.add(parentProcess.getId());
         URI metadataFilePath = fileService.getMetadataFilePath(childProcess);
         parentData.add(convertChildMetsFile(metadataFilePath));
         linkParentProcessWithChildProcess(parentProcess, childProcess);
         return parentData;
     }
 
     private void checkTaskAndId(Process parentProcess) throws IOException {
         URI parentMetadataFilePath = fileService.getMetadataFilePath(parentProcess, true, true);
         Workpiece workpiece = ServiceManager.getMetsService().loadWorkpiece(parentMetadataFilePath);
         ProcessService.checkTasks(parentProcess, workpiece.getLogicalStructure().getType());
         Collection<Metadata> metadata = workpiece.getLogicalStructure().getMetadata();
         String shortedTitle = "";
         String catalogIdentifier = "";
         for (Metadata metadatum : metadata) {
             if (metadatum.getKey().equals("TSL_ATS")) {
                 shortedTitle = ((MetadataEntry) metadatum).getValue();
             }
             if (metadatum.getKey().equals("CatalogIDDigital")) {
                 catalogIdentifier = ((MetadataEntry) metadatum).getValue();
             }
         }
         String title = "";
         if (!shortedTitle.isEmpty()) {
             title += shortedTitle + '_';
         }
         if (!catalogIdentifier.isEmpty()) {
             title += catalogIdentifier;
         }
         parentProcess.setTitle(title);
         workpiece.setId(parentProcess.getId().toString());
         ServiceManager.getMetsService().saveWorkpiece(workpiece,parentMetadataFilePath);
         if (WorkflowControllerService.allChildrenClosed(parentProcess)) {
             parentProcess.setSortHelperStatus("100000000");
         }
     }
 
     /**
      * Links parent process and child process in the database. The processes are
      * saved.
      *
      * @param parentProcess
      *            parent process to link
      * @param childProcess
      *            child process to link
      */
     private static void linkParentProcessWithChildProcess(Process parentProcess, Process childProcess)
             throws DAOException {
 
         parentProcess.getChildren().add(childProcess);
         childProcess.setParent(parentProcess);
         processService.saveToDatabase(childProcess);
         processService.saveToDatabase(parentProcess);
     }
 
     /**
      * Generates the METS file for the parent process from the process anchor
      * file.
      *
      * @param process
      *            process to migrate
      */
     private void createParentMetsFile(Process process) throws IOException {
         URI metadataFileUri = fileService.getMetadataFilePath(process);
         URI anchorFileUri = fileService.createAnchorFile(metadataFileUri);
         Workpiece workpiece = metsService.loadWorkpiece(anchorFileUri);
         LogicalDivision firstChild = workpiece.getLogicalStructure().getChildren().get(0);
         firstChild.setType(null);
         LinkedMetsResource link = firstChild.getLink();
         link.setLoctype("Kitodo.Production");
         link.setUri(processService.getProcessURI(process));
         URI parentMetadataFileUri = fileService.getMetadataFilePath(processGenerator.getGeneratedProcess(), false,
             false);
         metsService.saveWorkpiece(workpiece, parentMetadataFileUri);
     }
 
     /**
      * Changes the METS file of the child process.
      *
      * @param metadataFilePath
      *            URI of the metadata file
      * @return the current number, may be {@code null}
      */
     private static Integer convertChildMetsFile(URI metadataFilePath) throws IOException {
         Workpiece workpiece = metsService.loadWorkpiece(metadataFilePath);
         LogicalDivision childStructureRoot = workpiece.getLogicalStructure().getChildren().get(0);
         workpiece.setLogicalStructure(childStructureRoot);
         metsService.saveWorkpiece(workpiece, metadataFilePath);
         return getCurrentNoSorting(childStructureRoot);
     }
 
     /**
      * Extracts the CurrentNo from the metadata. The current number is an
      * integer sorting criterion, which specifies the order of the subordinate
      * units within their superordinate entirety. In case of journal issues,
      * this can be the same as the issue number if the issue number continues to
      * be counted at the turn of the year. In the case of multi-volume works,
      * this number can correspond to the part number, or (for lexica, for
      * example) it is counted up according to the alphabetical order of the
      * volumes, supplementary volumes are counted on afterwards (thus, in the
      * order in which the books are usually placed on a shelf).
      *
      * @param logicalDivision
      *            outline element with metadata
      * @return the CurrentNo, or {@code null}
      */
     private static Integer getCurrentNoSorting(LogicalDivision logicalDivision) {
         Integer currentNo = logicalDivision.getMetadata().parallelStream()
                 .filter(metadata -> metadata.getKey().equals("CurrentNoSorting")).filter(MetadataEntry.class::isInstance)
                 .map(MetadataEntry.class::cast).map(MetadataEntry::getValue).filter(value -> value.matches("\\d+"))
                 .map(Integer::valueOf).findFirst().orElse(null);
         return currentNo;
     }
 
     /**
      * Links a child process in an existing parent process.
      *
      * @param childProcess
      *            child process to link
      * @param parentData
      *            a data object with the ID of the parent process and the
      *            current numbers of the child processes already linked with the
      *            parent
      */
     private static void linkProcessInParent(Process childProcess, List<Integer> parentData)
             throws IOException, DAOException {
 
         URI metadataFilePath = fileService.getMetadataFilePath(childProcess);
         Integer currentNo = convertChildMetsFile(metadataFilePath);
         Process parentProcess = processService.getById(parentData.get(0));
         int insertionPosition = calculateInsertionPosition(parentData, currentNo);
         MetadataEditor.addLink(parentProcess, Integer.toString(insertionPosition), childProcess.getId());
         parentData.add(insertionPosition + 1, currentNo);
         linkParentProcessWithChildProcess(parentProcess, childProcess);
     }
 
     /**
      * Calculates the point at which the child process must be inserted in the
      * parent hierarchy.
      *
      * @param parentData
      *            a data object with the ID of the parent process (here unused)
      *            and the current numbers of the child processes already linked
      *            with the parent, which may be {@code null}
      * @param currentNo
      *            the current number of the child process to link, may be
      *            {@code null}
      * @return the insertion position
      */
     private static int calculateInsertionPosition(List<Integer> parentData, Integer currentNo) {
         int currentNumber = Objects.isNull(currentNo) ? Integer.MIN_VALUE : currentNo;
         int insertionPosition = 0;
         for (int index = 1; index < parentData.size(); index++) {
             int comparee = Objects.isNull(parentData.get(index)) ? Integer.MIN_VALUE : parentData.get(index);
             if (currentNumber >= comparee) {
                 insertionPosition++;
             } else {
                 break;
             }
         }
         return insertionPosition;
     }
 
     /**
      * Renames the anchor file. It is clear that this has been migrated.
      *
      * @param process
      *            process to migrate
      */
     private static void renameAnchorFile(Process process) throws IOException {
         URI anchorFile = fileService.createAnchorFile(fileService.getMetadataFilePath(process));
         fileService.renameFile(anchorFile, "meta_anchor.migrated");
     }
 }