Coverage Summary for Class: Subfolder (org.kitodo.production.model)

Class Class, % Method, % Line, %
Subfolder 100% (1/1) 80% (16/20) 62,4% (53/85)


 /*
  * (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.model;
 
 import java.io.File;
 import java.io.FilenameFilter;
 import java.lang.reflect.UndeclaredThrowableException;
 import java.net.URI;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.TreeMap;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.xml.bind.JAXBException;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.kitodo.config.ConfigCore;
 import org.kitodo.config.xml.fileformats.FileFormat;
 import org.kitodo.config.xml.fileformats.FileFormatsConfig;
 import org.kitodo.data.database.beans.Folder;
 import org.kitodo.data.database.beans.Process;
 import org.kitodo.production.helper.VariableReplacer;
 import org.kitodo.production.services.ServiceManager;
 import org.kitodo.production.services.file.FileService;
 
 /**
  * A subfolder is a folder in the file system below the process folder that
  * contains a variant of each file of the digital representation of the work
  * intended for a particular use. In this approach, a file is a fragment of the
  * digital representation of the artwork, a conceptual entity that can have
  * different variants. The individual variants of this file are then each stored
  * as computer-readable files in a folder for that use on the volume. They can
  * have different formats, both in terms of the data type and their form of
  * representation. The computer files, which are located in different use
  * folders and which are conceptually the same file, share a part of their
  * filename. This is referred to here as the canonical part of the file name.
  *
  * <p>
  * For historical reasons, in some institutions using this software, there is a
  * situation where the different variants of a file are in the same folder and
  * differ in filename suffix. We do not want to advertise this setup, but we
  * will continue to support it because it would be difficult for these
  * institutions to migrate the files.
  */
 public class Subfolder {
     private final FileService fileService = ServiceManager.getFileService();
     /**
      * The general metrics of this kind of subfolder. So to say its type.
      */
     private final Folder folder;
 
     /**
      * The process this subfolder belongs to. That is the process whose process
      * directory this sub-folder is below on the file storage.
      */
     private final Process process;
 
     /**
      * The variable replacer used to replace variables in the folder path to the
      * task folder.
      */
     private final VariableReplacer variableReplacer;
 
     /**
      * Creates a new subfolder.
      *
      * @param process
      *            the process this subfolder belongs to
      * @param folder
      *            the general metrics of this kind of subfolder
      */
     public Subfolder(Process process, Folder folder) {
         this.process = process;
         this.folder = folder;
         this.variableReplacer = new VariableReplacer(null, process, null);
     }
 
     /**
      * Returns a key mapper for a compiled regular expression.
      *
      * @param pattern
      *            compiled regular expression to determine the key
      * @return a mapping function for the key of the map
      */
     private static Function<URI, String> createKeyMapperForPattern(final Pattern pattern) {
         return uri -> {
             if (Objects.nonNull(uri)) {
                 Matcher matcher = pattern.matcher(FilenameUtils.getName(uri.getPath()));
                 if (!matcher.find()) {
                     throw new IllegalStateException(
                             "Regular expression \"" + pattern + "\" didn't match on \"" + uri + '"');
                 }
                 return matcher.group(1);
             }
             return null;
         };
     }
 
     /**
      * Computes the search patterns in the complicated case that the path
      * contains a search pattern.
      *
      * @param lastSeparator
      *            position of the last File.separatorChar in the path
      * @param lastSegment
      *            everything after the last File.separatorChar in the path
      * @param firstStar
      *            position of the first star after the last File.separatorChar
      *            in the path
      * @return search request consisting of an indication of the folder to be
      *         searched and a pattern to which the file names must correspond
      */
     private Pair<URI, Pattern> determineComplicatedSearchParameters(int lastSeparator, String lastSegment,
             int firstStar) {
         String realPath = variableReplacer.replace(folder.getPath().substring(0, lastSeparator));
         String processId = process.getId().toString();
         final URI directory = (realPath.isEmpty() ? Paths.get(ConfigCore.getKitodoDataDirectory(), processId)
                 : Paths.get(ConfigCore.getKitodoDataDirectory(), processId, realPath)).toUri();
         StringBuilder patternBuilder = new StringBuilder();
         if (firstStar > 0) {
             patternBuilder.append(Pattern.quote(lastSegment.substring(0, firstStar)));
         }
         patternBuilder.append("(.*)");
         if (firstStar < lastSegment.length() - 1) {
             patternBuilder.append(Pattern.quote(
                 lastSegment.substring(firstStar + 1).replaceFirst("\\*$", getFileFormat().getExtension(false))));
         }
         return Pair.of(directory, Pattern.compile(patternBuilder.toString()));
     }
 
     /**
      * Calculates the search pattern in the ordinary case that the path is
      * simply a path.
      *
      * @return search request consisting of an indication of the folder to be
      *         searched and a pattern to which the file names must correspond
      */
     private Pair<URI, Pattern> determineCommonSearchParameters() {
         String processId = process.getId().toString();
         URI directory = Paths
                 .get(ConfigCore.getKitodoDataDirectory(), processId, variableReplacer.replace(folder.getPath()))
                 .toUri();
         String pattern = "(.*)\\." + Pattern.quote(getFileFormat().getExtension(false));
         return Pair.of(directory, Pattern.compile(pattern));
     }
 
     /**
      * Determines the directory to search and the search pattern.
      *
      * @return search request consisting of an indication of the folder to be
      *         searched and a pattern to which the file names must correspond
      */
     private Pair<URI, Pattern> determineDirectoryAndFileNamePattern() {
         int lastSeparator = folder.getPath().lastIndexOf(File.separatorChar);
         String lastSegment = folder.getPath().substring(lastSeparator + 1);
         int firstStar = lastSegment.indexOf('*');
         if (firstStar == -1) {
             return determineCommonSearchParameters();
         } else {
             return determineComplicatedSearchParameters(lastSeparator, lastSegment, firstStar);
         }
     }
 
     /**
      * Returns the canonical part of the file name for a given URI.
      *
      * @param uri
      *            URI for which the canonical part of the file name should be
      *            returned
      * @return the canonical part of the file name
      */
     public String getCanonical(URI uri) {
         return createKeyMapperForPattern(determineDirectoryAndFileNamePattern().getRight()).apply(uri);
     }
 
     /**
      * Returns a file format by its MIME type, if any.
      *
      * @return the file format, if any
      */
     public FileFormat getFileFormat() {
         try {
             Optional<FileFormat> optionalFileFormat = FileFormatsConfig.getFileFormat(folder.getMimeType());
             if (optionalFileFormat.isPresent()) {
                 return optionalFileFormat.get();
             } else {
                 throw new NoSuchElementException(
                         "kitodo_fileFormats.xml has no <fileFormat mimeType=\"" + folder.getMimeType() + "\">");
             }
         } catch (JAXBException e) {
             throw new UndeclaredThrowableException(e);
         }
     }
 
     /**
      * Returns the folder database bean. The folder bean contains the settings
      * for the subfolder.
      *
      * @return the folder
      */
     public Folder getFolder() {
         return folder;
     }
 
     /**
      * Returns the relative path to a (fictitious) media file in the folder,
      * relative to the process dierctory.
      *
      * @return relative path to the file
      */
     public URI getRelativeFilePath(String canonical) {
         List<String> components = getUriComponents(canonical);
         return URI.create(components.size() > 2 ? components.get(1) + '/' + components.get(2) : components.get(1));
     }
 
     /**
      * Returns the relative path to the directory storing the files.
      *
      * @return relative path to the directory
      */
     public String getRelativeDirectoryPath() {
         String pathWithVariables = folder.getPath();
         int lastSeparator = pathWithVariables.lastIndexOf(File.separator);
         if (pathWithVariables.indexOf('*', lastSeparator) > -1) {
             pathWithVariables = pathWithVariables.substring(0, lastSeparator);
         }
         return variableReplacer.replace(pathWithVariables);
     }
 
     /**
      * Returns the URI to a file in this folder.
      *
      * @param canonical
      *            the canonical part of the file name. The canonical part is the
      *            part that is different from one file to another in the folder,
      *            but equal between two files representing the same content in
      *            two different folders. A typical canonical part could be
      *            “00000001”.
      * @return composed URI
      */
     public URI getUri(String canonical) {
         List<String> uriComponents = getUriComponents(canonical);
         return Paths.get(ConfigCore.getKitodoDataDirectory(), uriComponents.toArray(new String[uriComponents.size()]))
                 .toUri();
     }
 
     private List<String> getUriComponents(String canonical) {
         int lastSeparator = folder.getPath().lastIndexOf(File.separator);
         String lastSegment = folder.getPath().substring(lastSeparator + 1);
         List<String> components = new ArrayList<>(3);
         components.add(process.getId().toString());
 
         if (lastSegment.indexOf('*') == -1) {
             String localName = canonical + getFileFormat().getExtension(true);
             components.add(variableReplacer.replace(folder.getPath()));
             components.add(localName);
         } else {
             String realPath = folder.getPath().substring(0, lastSeparator);
             String localName = lastSegment.replaceFirst("\\*", canonical).replaceFirst("\\*$",
                 getFileFormat().getExtension(false));
             if (realPath.isEmpty()) {
                 components.add(localName);
             } else {
                 components.add(variableReplacer.replace(realPath));
                 components.add(localName);
             }
         }
         return components;
     }
 
     /**
      * Returns the URI to a file in this folder if that file does exist.
      *
      * @param canonical
      *            the canonical part of the file name. The canonical part is the
      *            part that is different from one file to another in the folder,
      *            but equal between two files representing the same content in
      *            two different folders. A typical canonical part could be
      *            “00000001”.
      * @return URI, or empty
      */
     public Optional<URI> getURIIfExists(String canonical) {
         URI uri = getUri(canonical);
         return new File(uri.getPath()).isFile() ? Optional.of(uri) : Optional.empty();
     }
 
     /**
      * Returns a map of canonical file name parts to URIs with all files
      * actually contained in this folder. The canonical part is the part that is
      * different from one file to another in the folder, but equal between two
      * files representing the same content in two different folders. A typical
      * canonical part could be “00000001”.
      *
      * @return map of canonical file name parts to URIs
      */
     public Map<String, URI> listContents() {
         return listContents(true);
     }
 
     /**
      * Returns a map of canonical file name parts to URIs with all files
      * actually contained in this folder. The canonical part is the part that is
      * different from one file to another in the folder, but equal between two
      * files representing the same content in two different folders. A typical
      * canonical part could be “00000001”.
      *
      * @param absolute
      *            whether to return absolute URIs, else URIs relative to the
      *            process base directory
      * @return map of canonical file name parts to URIs
      */
     public Map<String, URI> listContents(boolean absolute) {
         return listDirectory(determineDirectoryAndFileNamePattern(), absolute);
     }
 
     /**
      * Search for files with the file management interface.
      *
      * @param query
      *            search request consisting of an indication of the folder to be
      *            searched and a pattern to which the file names must correspond
      * @param absolute
      *            whether to return absolute URIs, else URIs relative to the
      *            process base directory
      * @return a map from the canonical file name part to the URI
      */
     private Map<String, URI> listDirectory(Pair<URI, Pattern> query, boolean absolute) {
         FilenameFilter filter = (dir, name) -> query.getRight().matcher(name).matches();
         try (Stream<URI> relativeURIs = fileService.getSubUris(filter, query.getLeft()).parallelStream()) {
             Stream<URI> resultURIs = absolute ? relativeURIs.map(
                 uri -> new File(ConfigCore.getKitodoDataDirectory().concat(uri.getPath())).toURI())
                     : relativeURIs.map(uri -> URI.create(uri.toString().replaceFirst("^[^/]+/", "")));
             Function<URI, String> keyMapper = createKeyMapperForPattern(query.getRight());
             return resultURIs.collect(Collectors.toMap(keyMapper, Function.identity(), (previous, latest) -> latest,
                 () -> new TreeMap<>(fileService.getMetadataImageComparator())));
         }
     }
 
     /**
      * Returns a string that textually represents this object.
      */
     @Override
     public String toString() {
         return Objects.toString(process) + '/' + folder;
     }
 }