Coverage Summary for Class: FileService (org.kitodo.production.services.file)

Class Class, % Method, % Line, %
FileService 100% (1/1) 65,9% (56/85) 42,3% (165/390)


 /*
  * (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.services.file;
 
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.FileSystems;
 import java.nio.file.Paths;
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 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.regex.Pattern;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.kitodo.api.command.CommandResult;
 import org.kitodo.api.dataformat.LogicalDivision;
 import org.kitodo.api.dataformat.MediaVariant;
 import org.kitodo.api.dataformat.PhysicalDivision;
 import org.kitodo.api.dataformat.View;
 import org.kitodo.api.dataformat.Workpiece;
 import org.kitodo.api.filemanagement.FileManagementInterface;
 import org.kitodo.api.filemanagement.ProcessSubType;
 import org.kitodo.config.ConfigCore;
 import org.kitodo.config.enums.ParameterCore;
 import org.kitodo.data.database.beans.Folder;
 import org.kitodo.data.database.beans.Process;
 import org.kitodo.data.database.beans.Ruleset;
 import org.kitodo.data.database.beans.User;
 import org.kitodo.data.database.exceptions.DAOException;
 import org.kitodo.exceptions.CommandException;
 import org.kitodo.exceptions.InvalidImagesException;
 import org.kitodo.exceptions.MediaNotFoundException;
 import org.kitodo.production.dto.ProcessDTO;
 import org.kitodo.production.file.BackupFileRotation;
 import org.kitodo.production.helper.Helper;
 import org.kitodo.production.helper.metadata.ImageHelper;
 import org.kitodo.production.helper.metadata.legacytypeimplementations.LegacyMetsModsDigitalDocumentHelper;
 import org.kitodo.production.helper.metadata.pagination.Paginator;
 import org.kitodo.production.metadata.MetadataEditor;
 import org.kitodo.production.model.Subfolder;
 import org.kitodo.production.services.ServiceManager;
 import org.kitodo.production.services.command.CommandService;
 import org.kitodo.production.services.data.RulesetService;
 import org.kitodo.serviceloader.KitodoServiceLoader;
 
 public class FileService {
 
     private static final Logger logger = LogManager.getLogger(FileService.class);
 
     private final MetadataImageComparator metadataImageComparator = new MetadataImageComparator(this);
 
     /**
      * Attachment to filename for the overall anchor file in Production v. 2.
      */
     private static final String APPENDIX_ANCOR = "_anchor";
     /**
      * Attachment to filename for the year anchor file in Production v. 2.
      */
     private static final String APPENDIX_YEAR = "_year";
     private static final String TEMPORARY_FILENAME_PREFIX = "temporary_";
     private final FileManagementInterface fileManagementModule = new KitodoServiceLoader<>(
             FileManagementInterface.class).loadModule();
 
     private static final String ARABIC = "arabic";
     private static final String ROMAN = "roman";
     private static final String ARABIC_DEFAULT_VALUE = "1";
     private static final String ROMAN_DEFAULT_VALUE = "I";
     private static final String UNCOUNTED_DEFAULT_VALUE = " - ";
 
 
     /**
      * Adds a slash to a URI to mark it as a directory, if it does not already
      * end with one.
      *
      * @param uri
      *            URI that you know to name a directory
      * @return URI, which definitely ends in a slash
      */
     public URI asDirectory(URI uri) {
         String uriString = uri.toString();
         return uriString.endsWith("/") ? uri : URI.create(uriString.concat("/"));
     }
 
     /**
      * Creates a MetaDirectory.
      *
      * @param parentFolderUri
      *            The URI, where the
      * @param directoryName
      *            the name of the directory
      * @return true or false
      * @throws IOException
      *             an IOException
      */
     URI createMetaDirectory(URI parentFolderUri, String directoryName) throws IOException, CommandException {
         String encodedPath = "";
         try {
             encodedPath = (new URI(null, null, directoryName, null)).toString();
         } catch (URISyntaxException e) {
             throw new IllegalArgumentException(e.getMessage(), e);
         }
         URI directoryUri = asDirectory(parentFolderUri).resolve(encodedPath);
         if (fileExist(directoryUri)) {
             logger.info("Metadata directory: {} already existed! No new directory was created", directoryName);
         } else {
             CommandService commandService = ServiceManager.getCommandService();
             String path = FileSystems.getDefault()
                     .getPath(ConfigCore.getKitodoDataDirectory(), parentFolderUri.getRawPath(), directoryName)
                     .normalize().toAbsolutePath().toString();
             List<String> commandParameter = Collections.singletonList(path);
             File script = new File(ConfigCore.getParameter(ParameterCore.SCRIPT_CREATE_DIR_META));
             if (!script.exists()) {
                 throw new CommandException(Helper.getTranslation("fileNotFound", script.getName()));
             }
             CommandResult commandResult = commandService.runCommand(script, commandParameter);
             if (!commandResult.isSuccessful()) {
                 String message = MessageFormat.format(
                     "Could not create directory {0} in {1}! No new directory was created", directoryName,
                     parentFolderUri.getPath());
                 logger.warn(message);
                 throw new CommandException(message);
             }
         }
         return directoryUri;
     }
 
     /**
      * Generates the URI to the anchor file for Production v. 2 hierarchical
      * processes. This should not be used except for migration of legacy data.
      *
      * @param metadataFilePath
      *            path URI to meta XML file
      * @return path URI to anchor XML file
      */
     public URI createAnchorFile(URI metadataFilePath) {
         return insertIntoURI(metadataFilePath, ".xml", APPENDIX_ANCOR);
     }
 
     /**
      * Creates a directory including any missing parent directories.
      *
      * @param pathToCreate
      *            path to create
      */
     public void createDirectories(URI pathToCreate) throws IOException {
         if (fileManagementModule.isDirectory(pathToCreate)) {
             return;
         }
         String path = pathToCreate.toString();
         int lastSlash = path.lastIndexOf('/');
         if (lastSlash <= 0) {
             createDirectory(null, path);
         } else {
             URI before = URI.create(path.substring(0, lastSlash));
             String after = path.substring(lastSlash + 1);
             createDirectories(before);
             createDirectory(before, after);
         }
     }
 
     /**
      * Creates a directory at a given URI with a given name.
      *
      * @param parentFolderUri
      *            the uri, where the directory should be created
      * @param directoryName
      *            the name of the directory.
      * @return the URI of the new directory or URI of parent directory if
      *         directoryName is null or empty
      */
     public URI createDirectory(URI parentFolderUri, String directoryName) throws IOException {
         if (Objects.nonNull(directoryName)) {
             return fileManagementModule.create(parentFolderUri, directoryName, false);
         }
         return URI.create("");
     }
 
     /**
      * Creates a directory with a name given and assigns permissions to the
      * given user. Under Linux a script is used to set the file system
      * permissions accordingly. This cannot be done from within java code before
      * version 1.7.
      *
      * @param dirName
      *            Name of directory to create
      * @throws IOException
      *             If an I/O error occurs.
      */
     public void createDirectoryForUser(URI dirName, String userName) throws IOException {
         if (!fileExist(dirName)) {
             CommandService commandService = ServiceManager.getCommandService();
             List<String> commandParameter = Arrays.asList(userName, new File(dirName).getAbsolutePath());
             commandService.runCommand(new File(ConfigCore.getParameter(ParameterCore.SCRIPT_CREATE_DIR_USER_HOME)),
                 commandParameter);
         }
     }
 
     /**
      * Creates the folder structure needed for a process.
      *
      * @param process
      *            the process
      * @return the URI to the process location
      */
     public URI createProcessLocation(Process process) throws IOException, CommandException {
         URI processLocationUri = fileManagementModule.createProcessLocation(process.getId().toString());
         createProcessFolders(process, processLocationUri);
         return processLocationUri;
     }
 
     /**
      * Creates the folders inside a process location.
      *
      * @param process
      *            the process
      */
     public void createProcessFolders(Process process) throws IOException, CommandException {
         createProcessFolders(process, fileManagementModule.createUriForExistingProcess(process.getId().toString()));
     }
 
     private void createProcessFolders(Process process, URI processLocationUri) throws IOException, CommandException {
         for (Folder folder : process.getProject().getFolders()) {
             if (folder.isCreateFolder()) {
                 URI parentFolderUri = processLocationUri;
                 for (String singleFolder : new Subfolder(process, folder).getRelativeDirectoryPath()
                         .split(Pattern.quote(File.separator))) {
                     parentFolderUri = createMetaDirectory(parentFolderUri, singleFolder);
                 }
             }
         }
     }
 
     /**
      * Creates a new File.
      *
      * @param fileName
      *            the name of the new file
      * @return the uri of the new file
      */
     public URI createResource(String fileName) throws IOException {
         return fileManagementModule.create(null, fileName, true);
     }
 
     /**
      * Creates a resource at a given URI with a given name.
      *
      * @param targetFolder
      *            the URI of the target folder
      * @param name
      *            the name of the new resource
      * @return the URI of the created resource
      */
     public URI createResource(URI targetFolder, String name) throws IOException {
         return fileManagementModule.create(targetFolder, name, true);
     }
 
     /**
      * Generates the URI to the year file for Production v. 2 newspaper
      * processes. This should not be used except for migration of legacy data.
      *
      * @param metadataFilePath
      *            path URI to meta XML file
      * @return path URI to year XML file
      */
     public URI createYearFile(URI metadataFilePath) {
         return insertIntoURI(metadataFilePath, ".xml", APPENDIX_YEAR);
     }
 
     /**
      * Inserts a string before another substring in an URI.
      *
      * @param uri
      *            URI to insert the string into
      * @param tail
      *            part of the URI before which the string is to be inserted
      * @param insert
      *            string to insert
      * @return URI with string inserted
      */
     private static URI insertIntoURI(URI uri, String tail, String insert) {
         String data = uri.toASCIIString();
         int dataLength = data.length();
         int questionMark = data.indexOf('?');
         int resourceLength = questionMark >= 0 ? questionMark : dataLength;
         if (questionMark < 0) {
             int hash = data.indexOf('#');
             resourceLength = hash >= 0 ? hash : dataLength;
         }
         int beforeTail = data.lastIndexOf(tail, resourceLength);
         int cutPosition = beforeTail >= 0 ? beforeTail : resourceLength;
         String buffer = data.substring(0, cutPosition)
                 + insert
                 + data.substring(cutPosition, dataLength);
         return URI.create(buffer);
     }
 
     /**
      * Writes to a file at a given URI.
      *
      * @param uri
      *            the URI, to write to.
      * @return an output stream to the file at the given URI or null
      */
     public OutputStream write(URI uri) throws IOException {
         return fileManagementModule.write(uri);
     }
 
     /**
      * Reads a file at a given URI.
      *
      * @param uri
      *            the uri to read
      * @return an InputStream to read from or null
      */
     public InputStream read(URI uri) throws IOException {
         return fileManagementModule.read(uri);
     }
 
     /**
      * Read metadata file (meta.xml).
      *
      * @param process
      *            for which file should be read
      * @return InputStream with metadata file
      */
     public InputStream readMetadataFile(Process process) throws IOException {
         return read(getMetadataFilePath(process));
     }
 
     /**
      * Read metadata file (meta.xml).
      *
      * @param process
      *            for which file should be read
      * @return InputStream with metadata file
      */
     public InputStream readMetadataFile(Process process, boolean forIndexingAll) throws IOException {
         return read(getMetadataFilePath(process, true, forIndexingAll));
     }
 
     /**
      * This function implements file renaming. Renaming of files is full of
      * mischief under Windows which unaccountably holds locks on files.
      * Sometimes running the JVM’s garbage collector puts things right.
      *
      * @param fileUri
      *            File to rename
      * @param newFileName
      *            New file name / destination
      * @throws IOException
      *             is thrown if renaming the file fails permanently
      */
     public URI renameFile(URI fileUri, String newFileName) throws IOException {
         return fileManagementModule.rename(fileUri, newFileName);
     }
 
     /**
      * Calculate all files with given file extension at specified directory
      * recursively.
      *
      * @param directory
      *            the directory to run through
      * @return number of files as Integer
      */
     public Integer getNumberOfFiles(URI directory) {
         return fileManagementModule.getNumberOfFiles(null, directory);
     }
 
     /**
      * Calculate all files with given file extension at specified directory
      * recursively.
      *
      * @param directory
      *            the directory to run through
      * @return number of files as Integer
      */
     public Integer getNumberOfImageFiles(URI directory) {
         return fileManagementModule.getNumberOfFiles(ImageHelper.imageNameFilter, directory);
     }
 
     /**
      * Get size of directory.
      *
      * @param directory
      *            URI to get size
      * @return size of directory as Long
      */
     public Long getSizeOfDirectory(URI directory) throws IOException {
         return fileManagementModule.getSizeOfDirectory(directory);
     }
 
     /**
      * Copy directory.
      *
      * @param sourceDirectory
      *            source file as uri
      * @param targetDirectory
      *            destination file as uri
      */
     public void copyDirectory(URI sourceDirectory, URI targetDirectory) throws IOException {
         fileManagementModule.copy(sourceDirectory, targetDirectory);
     }
 
     /**
      * Copies a file from a given URI to a given URI.
      *
      * @param sourceUri
      *            the uri to copy from
      * @param destinationUri
      *            the uri to copy to
      * @throws IOException
      *             if copying fails
      */
     public void copyFile(URI sourceUri, URI destinationUri) throws IOException {
         fileManagementModule.copy(sourceUri, destinationUri);
     }
 
     /**
      * Copies a file to a directory.
      *
      * @param sourceDirectory
      *            The source directory
      * @param targetDirectory
      *            the target directory
      * @throws IOException
      *             if copying fails.
      */
     public void copyFileToDirectory(URI sourceDirectory, URI targetDirectory) throws IOException {
         String target = targetDirectory.toString();
         if (!target.endsWith("/")) {
             targetDirectory = URI.create(target.concat("/"));
         }
         fileManagementModule.copy(sourceDirectory, targetDirectory);
     }
 
     /**
      * Deletes a resource at a given URI.
      *
      * @param uri
      *            the uri to delete
      * @return true, if successful, false otherwise
      * @throws IOException
      *             if get of module fails
      */
     public boolean delete(URI uri) throws IOException {
         return fileManagementModule.delete(uri);
     }
 
     /**
      * Checks, if a file exists.
      *
      * @param uri
      *            the URI, to check, if there is a file
      * @return true, if the file exists
      */
     public boolean fileExist(URI uri) {
         return fileManagementModule.fileExist(uri);
     }
 
     /**
      * Checks if a resource at a given URI is a file.
      *
      * @param uri
      *            the URI to check, if there is a file
      * @return true, if it is a file, false otherwise
      */
     public boolean isFile(URI uri) {
         return fileManagementModule.isFile(uri);
     }
 
     /**
      * checks, if a URI leads to a directory.
      *
      * @param dir
      *            the uri to check.
      * @return true, if it is a directory.
      */
     public boolean isDirectory(URI dir) {
         return fileManagementModule.isDirectory(dir);
     }
 
     /**
      * Checks if an uri is readable.
      *
      * @param uri
      *            the uri to check.
      * @return true, if it's readable, false otherwise.
      */
     public boolean canRead(URI uri) {
         return fileManagementModule.canRead(uri);
     }
 
     /**
      * Returns the name of a file at a given URI.
      *
      * @param uri
      *            the URI, to get the filename from.
      * @return the name of the file
      */
     public String getFileName(URI uri) {
         return FilenameUtils.getBaseName(fileManagementModule.getFileNameWithExtension(uri));
     }
 
     /**
      * Returns the name of a file at a given uri.
      *
      * @param uri
      *            the URI, to get the filename from
      * @return the name of the file
      */
     public String getFileNameWithExtension(URI uri) {
         return fileManagementModule.getFileNameWithExtension(uri);
     }
 
     /**
      * Moves a directory from a given URI to a given URI.
      *
      * @param sourceUri
      *            the source URI
      * @param targetUri
      *            the target URI
      * @throws IOException
      *             if get of module fails
      */
     public void moveDirectory(URI sourceUri, URI targetUri) throws IOException {
         fileManagementModule.move(sourceUri, targetUri);
     }
 
     /**
      * Moves a file from a given URI to a given URI.
      *
      * @param sourceUri
      *            the source URI
      * @param targetUri
      *            the target URI
      * @throws IOException
      *             if get of module fails
      */
     public void moveFile(URI sourceUri, URI targetUri) throws IOException {
         fileManagementModule.move(sourceUri, targetUri);
     }
 
     /**
      * Process owns anchor XML.
      *
      * @param process
      *            whose metadata file path to use
      * @return whether an anchor file was found
      * @throws IOException
      *             if Io failed
      */
     public boolean processOwnsAnchorXML(Process process) throws IOException {
         URI yearFile = createAnchorFile(getMetadataFilePath(process, false, true));
         return fileExist(yearFile);
     }
 
     /**
      * Process owns year XML.
      *
      * @param process
      *            whose metadata file path to use
      * @return whether a year file was found
      * @throws IOException
      *             if Io failed
      */
     public boolean processOwnsYearXML(Process process) throws IOException {
         URI yearFile = createYearFile(getMetadataFilePath(process, false, true));
         return fileExist(yearFile);
     }
 
     /**
      * Get all sub URIs of an URI.
      *
      * @param uri
      *            the URI, to get the sub URIs from
      * @return a List of sub URIs
      */
     public List<URI> getSubUris(URI uri) {
         return fileManagementModule.getSubUris(null, uri);
     }
 
     /**
      * Get all sub URIs of an URI with a given filter.
      *
      * @param filter
      *            the filter to filter the sub URIs
      * @param uri
      *            the URI, to get the sub URIs from
      * @return a List of sub URIs
      */
     public List<URI> getSubUris(FilenameFilter filter, URI uri) {
         return fileManagementModule.getSubUris(filter, uri);
     }
 
     /**
      * Lists all Files at the given Path.
      *
      * @param file
      *            the directory to get the Files from
      * @return an Array of Files.
      */
     private File[] listFiles(File file) {
         File[] unchecked = file.listFiles();
         return Objects.nonNull(unchecked) ? unchecked : new File[0];
     }
 
     /**
      * Writes a metadata file.
      *
      * @param gdzfile
      *            the file format
      * @param process
      *            the process
      * @throws IOException
      *             if error occurs
      */
     public void writeMetadataFile(LegacyMetsModsDigitalDocumentHelper gdzfile, Process process) throws IOException {
         RulesetService rulesetService = ServiceManager.getRulesetService();
         LegacyMetsModsDigitalDocumentHelper ff;
 
         Ruleset ruleset = process.getRuleset();
         ff = new LegacyMetsModsDigitalDocumentHelper(rulesetService.getPreferences(ruleset).getRuleset());
 
         // createBackupFile();
         URI metadataFileUri = getMetadataFilePath(process, false, false);
         String temporaryMetadataFileName = getTemporaryMetadataFileName(metadataFileUri);
 
         ff.setDigitalDocument(gdzfile.getDigitalDocument());
         // ff.write(getMetadataFilePath());
         ff.write(temporaryMetadataFileName);
         File temporaryMetadataFile = new File(temporaryMetadataFileName);
         boolean backupCondition = temporaryMetadataFile.exists() && temporaryMetadataFile.length() > 0;
         if (backupCondition) {
             createBackupFile(process);
             renameFile(Paths.get(temporaryMetadataFileName).toUri(), metadataFileUri.getRawPath());
             removePrefixFromRelatedMetsAnchorFilesFor(Paths.get(temporaryMetadataFileName).toUri());
         }
     }
 
     private void removePrefixFromRelatedMetsAnchorFilesFor(URI temporaryMetadataFilename) throws IOException {
         File temporaryFile = new File(temporaryMetadataFilename);
         File directoryPath = new File(temporaryFile.getParentFile().getPath());
         for (File temporaryAnchorFile : listFiles(directoryPath)) {
             String temporaryAnchorFileName = temporaryAnchorFile.toString();
             if (temporaryAnchorFile.isFile()
                     && FilenameUtils.getBaseName(temporaryAnchorFileName).startsWith(TEMPORARY_FILENAME_PREFIX)) {
                 String anchorFileName = FilenameUtils.concat(FilenameUtils.getFullPath(temporaryAnchorFileName),
                     temporaryAnchorFileName.replace(TEMPORARY_FILENAME_PREFIX, ""));
                 temporaryAnchorFileName = FilenameUtils.concat(FilenameUtils.getFullPath(temporaryAnchorFileName),
                     temporaryAnchorFileName);
                 renameFile(Paths.get(temporaryAnchorFileName).toUri(), new File(anchorFileName).toURI().getRawPath());
             }
         }
     }
 
     /**
      * Creates a backup of {@code meta.xml}.
      *
      * @param process
      *            process whose {@code meta.xml} shall be created a backup of.
      */
     public void createBackupFile(Process process) throws IOException {
         int numberOfBackups;
 
         numberOfBackups = ConfigCore.getIntParameter(ParameterCore.NUMBER_OF_META_BACKUPS);
 
         if (numberOfBackups != ConfigCore.INT_PARAMETER_NOT_DEFINED_OR_ERRONEOUS) {
             BackupFileRotation bfr = new BackupFileRotation();
             bfr.setNumberOfBackups(numberOfBackups);
             bfr.setFormat("meta.*\\.xml");
             bfr.setProcess(process);
             bfr.performBackup();
         } else {
             logger.warn("No backup configured for meta data files.");
         }
     }
 
     /**
      * Gets the URI of the metadata.xml of a given process.
      *
      * @param process
      *            the process to get the metadata.xml for.
      * @return The URI to the metadata.xml
      */
     public URI getMetadataFilePath(Process process) throws IOException {
         return getMetadataFilePath(process, true, false);
     }
 
     /**
      * Gets the URI of the metadata.xml of a given processDTO.
      *
      * @param processDTO
      *            the process to get the metadata.xml for.
      * @return The URI to the metadata.xml
      */
     public URI getMetadataFilePath(ProcessDTO processDTO) throws IOException {
         return getMetadataFilePath(processDTO, true);
     }
 
     /**
      * Gets the URI of the metadata.xml of a given process.
      *
      * @param process
      *            the process to get the metadata.xml for.
      * @param mustExist
      *            whether the file must exist
      * @return The URI to the metadata.xml
      */
     public URI getMetadataFilePath(Process process, boolean mustExist, boolean forIndexingAll) throws IOException {
         URI metadataFilePath = getProcessSubTypeURI(process, ProcessSubType.META_XML, null, forIndexingAll);
         if (mustExist && !fileExist(metadataFilePath)) {
             throw new IOException(Helper.getTranslation("metadataFileNotFound", metadataFilePath.getPath()));
         }
         return metadataFilePath;
     }
 
     /**
      * Gets the URI of the metadata.xml of a given processDTO.
      *
      * @param processDTO
      *            the process to get the metadata.xml for.
      * @param mustExist
      *            whether the file must exist
      * @return The URI to the metadata.xml
      */
     public URI getMetadataFilePath(ProcessDTO processDTO, boolean mustExist) throws IOException {
         URI metadataFilePath = getProcessSubTypeURI(processDTO, ProcessSubType.META_XML, null);
         if (mustExist && !fileExist(metadataFilePath)) {
             throw new IOException(Helper.getTranslation("metadataFileNotFound", metadataFilePath.getPath()));
         }
         return metadataFilePath;
     }
 
     private String getTemporaryMetadataFileName(URI fileName) {
         File temporaryFile = getFile(fileName);
         String directoryPath = temporaryFile.getParentFile().getPath();
         String temporaryFileName = TEMPORARY_FILENAME_PREFIX + temporaryFile.getName();
 
         return directoryPath + File.separator + temporaryFileName;
     }
 
     /**
      * Gets the specific IMAGE sub type.
      *
      * @param process
      *            the process to get the imageDirectory for.
      * @return The uri of the Image Directory.
      */
     public URI getImagesDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.IMAGE, null);
     }
 
     /**
      * Gets the URI to the ocr directory.
      *
      * @param process
      *            the process tog et the ocr directory for.
      * @return the uri to the ocr directory.
      */
     public URI getOcrDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.OCR, null);
     }
 
     /**
      * Gets the URI to the import directory.
      *
      * @param process
      *            the process to get the import directory for.
      * @return the uri of the import directory
      */
     public URI getImportDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.IMPORT, null);
     }
 
     /**
      * Gets the URI to the text directory.
      *
      * @param process
      *            the process to get the text directory for.
      * @return the uri of the text directory
      */
     public URI getTxtDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.OCR_TXT, null);
     }
 
     /**
      * Gets the URI to the pdf directory.
      *
      * @param process
      *            the process to get the pdf directory for.
      * @return the uri of the pdf directory
      */
     public URI getPdfDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.OCR_PDF, null);
     }
 
     /**
      * Gets the URI to the alto directory.
      *
      * @param process
      *            the process to get the alto directory for.
      * @return the uri of the alto directory
      */
     public URI getAltoDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.OCR_ALTO, null);
     }
 
     /**
      * Gets the URI to the word directory.
      *
      * @param process
      *            the process to get the word directory for.
      * @return the uri of the word directory
      */
     public URI getWordDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.OCR_WORD, null);
     }
 
     /**
      * Gets the URI to the template file.
      *
      * @param process
      *            the process to get the template file for.
      * @return the uri of the template file
      */
     public URI getTemplateFile(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.TEMPLATE, null);
     }
 
     /**
      * Returns the URI of the resource in the file management module for a URI
      * relative to the process directory.
      *
      * @param process
      *            Process in whose possession the URI should be resolved
      * @param processRelativeUri
      *            URI relative to the specified process
      * @return URI of the resource
      */
     public URI getResourceUriForProcessRelativeUri(Process process, URI processRelativeUri) {
         URI processBaseUri = getProcessBaseUriForExistingProcess(process);
         return asDirectory(processBaseUri).resolve(processRelativeUri);
     }
 
     /**
      * This method is needed for migration purposes. It maps existing filePaths
      * to the correct URI. File.separator doesn't work because on Windows it
      * appends backslash to URI.
      *
      * @param process
      *            the process, the uri is needed for.
      * @return the URI.
      */
     public URI getProcessBaseUriForExistingProcess(Process process) {
         URI processBaseUri = process.getProcessBaseUri();
         if (Objects.isNull(processBaseUri) && Objects.nonNull(process.getId())) {
             process.setProcessBaseUri(fileManagementModule.createUriForExistingProcess(process.getId().toString()));
         }
         return process.getProcessBaseUri();
     }
 
     /**
      * This method is needed for migration purposes. It maps existing filePaths
      * to the correct URI. File.separator doesn't work because on Windows it
      * appends backslash to URI.
      *
      * @param processDTO
      *            the process, the uri is needed for.
      * @return the URI.
      */
     public String getProcessBaseUriForExistingProcess(ProcessDTO processDTO) {
         String processBaseUri = processDTO.getProcessBaseUri();
         if (Objects.isNull(processBaseUri) && Objects.nonNull(processDTO.getId())) {
             processDTO.setProcessBaseUri(fileManagementModule.createUriForExistingProcess(processDTO.getId().toString()).toString());
         }
         return processDTO.getProcessBaseUri();
     }
 
     /**
      * Get the URI for a process sub-location. Possible locations are listed in
      * ProcessSubType.
      *
      * @param processId
      *            the id of process to get the sublocation for
      * @param processTitle
      *            the title of process to get the sublocation for
      * @param processDataDirectory
      *            the base URI of process to get the sublocation for
      * @param processSubType
      *            The subType.
      * @param resourceName
      *            the name of the single object (e.g. image) if null, the root
      *            folder of the sublocation is returned
      * @return The URI of the requested location
      */
     public URI getProcessSubTypeURI(Integer processId, String processTitle, URI processDataDirectory,
             ProcessSubType processSubType, String resourceName) {
 
         if (Objects.isNull(processDataDirectory)) {
             try {
                 Process process = ServiceManager.getProcessService().getById(processId);
                 processDataDirectory = ServiceManager.getProcessService().getProcessDataDirectory(process);
             } catch (DAOException e) {
                 processDataDirectory = URI.create(String.valueOf(processId));
             }
         }
 
         if (Objects.isNull(resourceName)) {
             resourceName = "";
         }
         return fileManagementModule.getProcessSubTypeUri(processDataDirectory, processTitle, processSubType,
             resourceName);
     }
 
     /**
      * Gets the URI for a Process Sub-location. Possible Locations are listed
      * in ProcessSubType
      *
      * @param process
      *            the process to get the sublocation for.
      * @param processSubType
      *            The subType.
      * @param resourceName
      *            the name of the single object (e.g. image) if null, the root
      *            folder of the sublocation is returned
      * @return The URI of the requested location
      */
     public URI getProcessSubTypeURI(Process process, ProcessSubType processSubType, String resourceName) {
         return getProcessSubTypeURI(process, processSubType, resourceName, false);
     }
 
     /**
      * Gets the URI for a Process Sub-location. Possible Locations are listed
      * in ProcessSubType
      *
      * @param process
      *            the process to get the sublocation for.
      * @param processSubType
      *            The subType.
      * @param resourceName
      *            the name of the single object (e.g. image) if null, the root
      *            folder of the sublocation is returned
      * @return The URI of the requested location
      */
     private URI getProcessSubTypeURI(Process process, ProcessSubType processSubType, String resourceName,
             boolean forIndexingAll) {
 
         URI processDataDirectory = ServiceManager.getProcessService().getProcessDataDirectory(process, forIndexingAll);
 
         if (Objects.isNull(resourceName)) {
             resourceName = "";
         }
         return fileManagementModule.getProcessSubTypeUri(processDataDirectory,
             Helper.getNormalizedTitle(process.getTitle()), processSubType, resourceName);
     }
 
     /**
      * Gets the URI for a Process Sub-location. Possible Locations are listed
      * in ProcessSubType
      *
      * @param processDTO
      *            the process to get the sublocation for.
      * @param processSubType
      *            The subType.
      * @param resourceName
      *            the name of the single object (e.g. image) if null, the root
      *            folder of the sublocation is returned
      * @return The URI of the requested location
      */
     private URI getProcessSubTypeURI(ProcessDTO processDTO, ProcessSubType processSubType, String resourceName) {
 
         String processDataDirectory = ServiceManager.getProcessService().getProcessDataDirectory(processDTO);
 
         if (Objects.isNull(resourceName)) {
             resourceName = "";
         }
         return fileManagementModule.getProcessSubTypeUri(URI.create(processDataDirectory),
                 Helper.getNormalizedTitle(processDTO.getTitle()), processSubType, resourceName);
     }
 
     /**
      * Get part of the URI for specific process.
      *
      * @param filter
      *            FilenameFilter object
      * @param processId
      *            the id of process
      * @param processTitle
      *            the title of process
      * @param processDataDirectory
      *            the base URI of process
      * @param processSubType
      *            object
      * @param resourceName
      *            as String
      * @return unmapped URI
      */
     public List<URI> getSubUrisForProcess(FilenameFilter filter, Integer processId, String processTitle,
             URI processDataDirectory, ProcessSubType processSubType, String resourceName) {
         URI processSubTypeURI = getProcessSubTypeURI(processId, processTitle, processDataDirectory, processSubType,
             resourceName);
         return getSubUris(filter, processSubTypeURI);
     }
 
     /**
      * Get part of the URI for specific process.
      *
      * @param filter
      *            FilenameFilter object
      * @param process
      *            object
      * @param processSubType
      *            object
      * @param resourceName
      *            as String
      * @return unmapped URI
      */
     public List<URI> getSubUrisForProcess(FilenameFilter filter, Process process, ProcessSubType processSubType,
             String resourceName) {
         URI processSubTypeURI = getProcessSubTypeURI(process, processSubType, resourceName);
         return getSubUris(filter, processSubTypeURI);
     }
 
     /**
      * Deletes all process directories and their content.
      *
      * @param process
      *            the process to delete the directories for.
      * @return true, if deletion was successful.
      */
     public boolean deleteProcessContent(Process process) throws IOException {
         for (ProcessSubType processSubType : ProcessSubType.values()) {
             URI processSubTypeURI = getProcessSubTypeURI(process, processSubType, null);
             if (!fileManagementModule.delete(processSubTypeURI)) {
                 return false;
             }
         }
         return true;
     }
 
     /**
      * Gets the image source directory.
      *
      * @param process
      *            the process, to get the source directory for
      * @return the source directory as an URI
      */
     public URI getSourceDirectory(Process process) {
         return getProcessSubTypeURI(process, ProcessSubType.IMAGE_SOURCE, null);
     }
 
     /**
      * Searches for new media and adds them to the media list of the workpiece, if any are found.
      *
      * @param process
      *         Process in which folders should be searched for media
      * @param workpiece
      *         Workpiece to which the media are to be added
      */
     public boolean searchForMedia(Process process, Workpiece workpiece)
             throws InvalidImagesException, MediaNotFoundException {
         final long begin = System.nanoTime();
         List<Folder> folders = process.getProject().getFolders();
         int mapCapacity = (int) Math.ceil(folders.size() / 0.75);
         Map<String, Subfolder> subfolders = new HashMap<>(mapCapacity);
         for (Folder folder : folders) {
             subfolders.put(folder.getFileGroup(), new Subfolder(process, folder));
         }
         Map<String, Map<Subfolder, URI>> currentMedia = new TreeMap<>(metadataImageComparator);
         for (Subfolder subfolder : subfolders.values()) {
             for (Entry<String, URI> element : subfolder.listContents(false).entrySet()) {
                 currentMedia.computeIfAbsent(element.getKey(), any -> new HashMap<>(mapCapacity));
                 currentMedia.get(element.getKey()).put(subfolder, element.getValue());
             }
         }
 
         List<String> canonicals = getCanonicalFileNamePartsAndSanitizeAbsoluteURIs(workpiece, subfolders,
                 process.getProcessBaseUri());
 
         if (currentMedia.size() == 0 && canonicals.size() > 0) {
             throw new MediaNotFoundException();
         }
 
         addNewURIsToExistingPhysicalDivisions(currentMedia,
                 workpiece.getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted(), canonicals);
         Map<String, Map<Subfolder, URI>> mediaToAdd = new TreeMap<>(currentMedia);
         List<String> mediaToRemove = new LinkedList<>(canonicals);
 
         mediaToRemove.removeAll(mediaToAdd.keySet());
         canonicals.forEach(mediaToAdd.keySet()::remove);
         removeMissingMediaFromWorkpiece(mediaToRemove, workpiece, subfolders.values());
         List<PhysicalDivision> children = workpiece.getPhysicalStructure().getChildren();
         boolean orderedChildren = (!children.isEmpty() && children.get(0).getOrder() > 0);
         addNewMediaToWorkpiece(canonicals, mediaToAdd, workpiece, orderedChildren);
         renumberPhysicalDivisions(workpiece, true);
         if (ConfigCore.getBooleanParameter(ParameterCore.WITH_AUTOMATIC_PAGINATION)) {
             repaginatePhysicalDivisions(workpiece);
         }
         if (Workpiece.treeStream(workpiece.getLogicalStructure())
                 .allMatch(logicalDivision -> logicalDivision.getViews().isEmpty())) {
             automaticallyAssignPhysicalDivisionsToEffectiveRootRecursive(workpiece, workpiece.getLogicalStructure());
         }
         if (logger.isTraceEnabled()) {
             logger.trace("Searching for media took {} ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin));
         }
         return orderedChildren && !(mediaToAdd.isEmpty() && mediaToRemove.isEmpty());
     }
 
     private void automaticallyAssignPhysicalDivisionsToEffectiveRootRecursive(Workpiece workpiece,
             LogicalDivision logicalDivision) {
 
         if (Objects.nonNull(logicalDivision.getType())) {
             Workpiece.treeStream(workpiece.getPhysicalStructure()).filter(physicalDivision -> !physicalDivision.getMediaFiles().isEmpty())
                     .map(View::of).forEachOrdered(logicalDivision.getViews()::add);
         } else if (logicalDivision.getChildren().size() == 1) {
             automaticallyAssignPhysicalDivisionsToEffectiveRootRecursive(workpiece,
                 logicalDivision.getChildren().get(0));
         }
     }
 
     /**
      * Parses the canonical part of the filename from the URIs of the media
      * units. Because we need to do this to be able to parse correctly, old
      * absolute URIs are converted to relative URIs.
      */
     private List<String> getCanonicalFileNamePartsAndSanitizeAbsoluteURIs(Workpiece workpiece,
             Map<String, Subfolder> subfolders, URI processBaseUri) throws InvalidImagesException {
 
         List<String> canonicals = new LinkedList<>();
         String baseUriString = processBaseUri.toString();
         if (!baseUriString.endsWith("/")) {
             baseUriString = baseUriString.concat("/");
         }
         for (PhysicalDivision physicalDivision : workpiece.getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted()) {
             String unitCanonical = "";
             for (Entry<MediaVariant, URI> entry : physicalDivision.getMediaFiles().entrySet()) {
                 Subfolder subfolder = subfolders.get(entry.getKey().getUse());
                 if (Objects.isNull(subfolder)) {
                     logger.warn("Missing subfolder for USE {}", entry.getKey().getUse());
                     continue;
                 }
                 URI mediaFile = entry.getValue();
                 String fileUriString = mediaFile.toString();
                 if (fileUriString.startsWith(baseUriString)) {
                     mediaFile = URI.create(fileUriString.substring(baseUriString.length()));
                     physicalDivision.getMediaFiles().put(entry.getKey(), mediaFile);
                 }
                 String fileCanonical = subfolder.getCanonical(mediaFile);
                 if ("".equals(unitCanonical)) {
                     unitCanonical = fileCanonical;
                 } else if (!unitCanonical.equals(fileCanonical)) {
                     throw new InvalidImagesException("Ambiguous canonical file name part in the same physical division: \""
                             + unitCanonical + "\" and \"" + fileCanonical + "\"!");
                 }
             }
             if (physicalDivision.getMediaFiles().size() > 0 && "".equals(unitCanonical)) {
                 throw new InvalidImagesException("Missing canonical file name part in physical division " + physicalDivision);
             }
             canonicals.add(unitCanonical);
         }
         return canonicals;
     }
 
     /**
      * Adds new media variants found to existing physical divisions.
      */
     private void addNewURIsToExistingPhysicalDivisions(Map<String, Map<Subfolder, URI>> mediaToAdd,
             List<PhysicalDivision> physicalDivisions, List<String> canonicals) {
 
         for (int i = 0; i < canonicals.size(); i++) {
             String canonical = canonicals.get(i);
             PhysicalDivision physicalDivision = physicalDivisions.get(i);
             if (mediaToAdd.containsKey(canonical)) {
                 for (Entry<Subfolder, URI> entry : mediaToAdd.get(canonical).entrySet()) {
                     physicalDivision.getMediaFiles().put(createMediaVariant(entry.getKey().getFolder()), entry.getValue());
                 }
             }
         }
     }
 
     private void removeMissingMediaFromWorkpiece(List<String> mediaToRemove, Workpiece workpiece,
                                                  Collection<Subfolder> subfolders) {
         List<PhysicalDivision> pages = workpiece.getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted();
         for (String removal : mediaToRemove) {
             if (StringUtils.isNotBlank(removal)) {
                 for (PhysicalDivision page : pages) {
                     if (removal.equals(getCanonical(subfolders, page))) {
                         workpiece.getPhysicalStructure().getChildren().remove(page);
                         for (LogicalDivision structuralElement : page.getLogicalDivisions()) {
                             structuralElement.getViews().removeIf(view -> view.getPhysicalDivision().equals(page));
                         }
                         page.getLogicalDivisions().clear();
                         LinkedList<PhysicalDivision> ancestors = MetadataEditor
                                 .getAncestorsOfPhysicalDivision(page, workpiece.getPhysicalStructure());
                         if (!ancestors.isEmpty()) {
                             PhysicalDivision parent = ancestors.getLast();
                             parent.getChildren().remove(page);
                         }
                     }
                 }
             }
         }
         if (!mediaToRemove.isEmpty()) {
             int i = 1;
             for (PhysicalDivision division : workpiece.getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted()) {
                 division.setOrder(i);
                 i++;
             }
         }
     }
 
     private String getCanonical(Collection<Subfolder> folders, PhysicalDivision division) {
         Map<MediaVariant, URI> mediaFiles = division.getMediaFiles();
         for (Subfolder folder : folders) {
             for (URI mediaUri : mediaFiles.values()) {
                 if (mediaUri.getPath().startsWith(folder.getFolder().getRelativePath())) {
                     String canonical = folder.getCanonical(mediaUri);
                     if (Objects.nonNull(canonical)) {
                         return canonical;
                     }
                 }
             }
         }
         return "";
     }
 
     /**
      * Adds the new media to the workpiece. The media are sorted in according to
      * the canonical part of the file name.
      */
     private void addNewMediaToWorkpiece(List<String> canonicals, Map<String, Map<Subfolder, URI>> mediaToAdd,
             Workpiece workpiece, boolean orderedChildren) {
 
         LogicalDivision actualLogicalRoot = workpiece.getLogicalStructure();
         while (Objects.isNull(actualLogicalRoot.getType()) && actualLogicalRoot.getChildren().size() == 1) {
             actualLogicalRoot = actualLogicalRoot.getChildren().get(0);
         }
         // If the newspaper has multiple issues in the process, then everything stays as it was
         if (Objects.isNull(actualLogicalRoot.getType()) && actualLogicalRoot.getChildren().size() != 1) {
             actualLogicalRoot = workpiece.getLogicalStructure();
         }
 
         for (Entry<String, Map<Subfolder, URI>> entry : mediaToAdd.entrySet()) {
             PhysicalDivision physicalDivision = createPhysicalDivision(entry.getValue());
             View view = new View();
             view.setPhysicalDivision(physicalDivision);
             // do not use canonical filename parts if existing physical structures already have "order" values > 0!
             if (orderedChildren) {
                 physicalDivision.setOrder(workpiece.getPhysicalStructure().getChildren().size());
                 workpiece.getPhysicalStructure().getChildren().add(physicalDivision);
                 actualLogicalRoot.getViews().add(view);
                 view.getPhysicalDivision().getLogicalDivisions().add(actualLogicalRoot);
                 canonicals.add(entry.getKey());
             } else {
                 // only use canonical filename parts if no ordered physical structures exist in the workpiece, yet
                 int insertionPoint = 0;
                 for (String canonical : canonicals) {
                     if (metadataImageComparator.compare(entry.getKey(), canonical) > 0) {
                         insertionPoint++;
                     } else {
                         break;
                     }
                 }
                 workpiece.getPhysicalStructure().getChildren().add(insertionPoint, physicalDivision);
                 actualLogicalRoot.getViews().add(insertionPoint, view);
                 view.getPhysicalDivision().getLogicalDivisions().add(actualLogicalRoot);
                 canonicals.add(insertionPoint, entry.getKey());
             }
         }
     }
 
     /**
      * Creates a new physical division with the given uses and URIs.
      */
     private PhysicalDivision createPhysicalDivision(Map<Subfolder, URI> data) {
         PhysicalDivision physicalDivision = new PhysicalDivision();
         if (!data.entrySet().isEmpty()) {
             physicalDivision.setType(PhysicalDivision.TYPE_PAGE);
         }
         for (Entry<Subfolder, URI> entry : data.entrySet()) {
             Folder folder = entry.getKey().getFolder();
             MediaVariant mediaVariant = createMediaVariant(folder);
             physicalDivision.getMediaFiles().put(mediaVariant, entry.getValue());
         }
         return physicalDivision;
     }
 
     /**
      * Creates a new media variant for the given use.
      */
     private MediaVariant createMediaVariant(Folder folder) {
         MediaVariant mediaVariant = new MediaVariant();
         mediaVariant.setUse(folder.getFileGroup());
         mediaVariant.setMimeType(folder.getMimeType());
         return mediaVariant;
     }
 
     /**
      * Renumbers the order of the physical divisions.
      */
     public void renumberPhysicalDivisions(Workpiece workpiece, boolean sortByOrder) {
         int order = 1;
         for (PhysicalDivision physicalDivision : sortByOrder ? workpiece.getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted()
                 : workpiece.getPhysicalStructure().getAllChildren()) {
             physicalDivision.setOrder(order++);
         }
     }
 
     /**
      * Adds a count to media that do not yet have a count. But only at the end,
      * or if none of the media has been counted yet at all. New media found in
      * intermediate places are marked uncounted.
      */
     private void repaginatePhysicalDivisions(Workpiece workpiece) {
         List<PhysicalDivision> physicalDivisions = workpiece.getAllPhysicalDivisionChildrenFilteredByTypePageAndSorted();
         int first = 0;
         String value;
         switch (ConfigCore.getParameter(ParameterCore.METS_EDITOR_DEFAULT_PAGINATION)) {
             case ARABIC:
                 value = ARABIC_DEFAULT_VALUE;
                 break;
             case ROMAN:
                 value = ROMAN_DEFAULT_VALUE;
                 break;
             default:
                 value = UNCOUNTED_DEFAULT_VALUE;
                 break;
         }
         if (!UNCOUNTED_DEFAULT_VALUE.equals(value)) {
             for (int i = physicalDivisions.size() - 1; i >= 0; i--) {
                 PhysicalDivision physicalDivision = physicalDivisions.get(i);
                 String orderlabel = physicalDivision.getOrderlabel();
                 if (Objects.nonNull(orderlabel) && !physicalDivision.getMediaFiles().isEmpty()) {
                     first = i + 1;
                     value = orderlabel;
                     physicalDivisions.get(i).setType(PhysicalDivision.TYPE_PAGE);
                     break;
                 }
             }
         }
         Paginator paginator = new Paginator(value);
         if (first > 0) {
             paginator.next();
             for (int i = first; i < physicalDivisions.size(); i++) {
                 physicalDivisions.get(i).setOrderlabel(paginator.next());
             }
         }
         for (PhysicalDivision physicalDivision : physicalDivisions) {
             if (Objects.isNull(physicalDivision.getOrderlabel())) {
                 physicalDivision.setOrderlabel(UNCOUNTED_DEFAULT_VALUE);
             }
         }
     }
 
     public void writeMetadataAsTemplateFile(LegacyMetsModsDigitalDocumentHelper inFile, Process process)
             throws IOException {
         inFile.write(getTemplateFile(process).toString());
     }
 
     /**
      * Creates a symbolic link.
      *
      * @param targetUri
      *            the target URI for the link
      * @param homeUri
      *            the home URI
      * @return true, if link creation was successful
      */
     public boolean createSymLink(URI homeUri, URI targetUri, boolean onlyRead, User user) {
         return fileManagementModule.createSymLink(homeUri, targetUri, onlyRead, user.getLogin());
     }
 
     /**
      * Delete a symbolic link.
      *
      * @param homeUri
      *            the URI of the home folder, where the link should be deleted
      * @return true, if deletion was successful
      */
     public boolean deleteSymLink(URI homeUri) {
         return fileManagementModule.deleteSymLink(homeUri);
     }
 
     public File getFile(URI uri) {
         return fileManagementModule.getFile(uri);
     }
 
     /**
      * Deletes the slash as first character from an uri object.
      *
      * @param uri
      *            The uri object.
      * @return The new uri object without first slash.
      */
     public URI deleteFirstSlashFromPath(URI uri) {
         String uriString = uri.getPath();
         if (uriString.startsWith("/")) {
             uriString = uriString.replaceFirst("/", "");
         }
         return URI.create(uriString);
     }
 
     /**
      * Check and return whether given process has an empty generator folder or not.
      *
      * @param process Process
      * @param generatorSource Folder
      * @return whether given URI points to empty directory or not
      * @throws IOException thrown if listing contents of given URI is not possible
      */
     public static boolean hasImages(Process process, Folder generatorSource) throws IOException, DAOException {
         if (Objects.nonNull(generatorSource)) {
             Subfolder sourceFolder = new Subfolder(process, generatorSource);
             return !sourceFolder.listContents().isEmpty();
         }
         return false;
     }
 
     /**
      * Returns the comparator for metadata images.
      *
      * @return comparator for metadata images
      */
     public MetadataImageComparator getMetadataImageComparator() {
         return metadataImageComparator;
     }
 }