org.kitodo.filemanagement.FileManagement.java Source code

Java tutorial

Introduction

Here is the source code for org.kitodo.filemanagement.FileManagement.java

Source

/*
 * (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.filemanagement;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.api.filemanagement.FileManagementInterface;
import org.kitodo.api.filemanagement.ProcessSubType;
import org.kitodo.api.filemanagement.filters.FileNameEndsWithFilter;
import org.kitodo.config.KitodoConfig;
import org.kitodo.config.enums.ParameterFileManagement;

public class FileManagement implements FileManagementInterface {

    private static final Logger logger = LogManager.getLogger(FileManagement.class);
    private static final FileMapper fileMapper = new FileMapper();

    private static final String IMAGES_DIRECTORY_NAME = "images";

    @Override
    public URI create(URI parentFolderUri, String name, boolean file) throws IOException {
        if (file) {
            return createResource(parentFolderUri, name);
        }
        return createDirectory(parentFolderUri, name);
    }

    private URI createDirectory(URI parentFolderUri, String directoryName) throws IOException {
        parentFolderUri = fileMapper.mapUriToKitodoDataDirectoryUri(parentFolderUri);
        File directory = new File(Paths.get(new File(parentFolderUri).getPath(), directoryName).toUri());
        if (!directory.exists() && !directory.mkdir()) {
            throw new IOException("Could not create directory.");
        }
        return fileMapper.unmapUriFromKitodoDataDirectoryUri(Paths.get(directory.getPath()).toUri());
    }

    private URI createResource(URI targetFolder, String fileName) throws IOException {
        targetFolder = fileMapper.mapUriToKitodoDataDirectoryUri(targetFolder);
        File file = new File(Paths.get(new File(targetFolder).getPath(), fileName).toUri());
        if (file.exists() || file.createNewFile()) {
            return fileMapper.unmapUriFromKitodoDataDirectoryUri(Paths.get(file.getPath()).toUri());
        }
        return URI.create("");
    }

    @Override
    public OutputStream write(URI uri) throws IOException {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        return Files.newOutputStream(Paths.get(uri));
    }

    @Override
    public InputStream read(URI uri) throws IOException {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        return uri.toURL().openStream();
    }

    @Override
    public void copy(URI sourceUri, URI targetUri) throws IOException {
        sourceUri = fileMapper.mapUriToKitodoDataDirectoryUri(sourceUri);
        boolean isDirectory = targetUri.getPath().endsWith("/");
        targetUri = fileMapper.mapUriToKitodoDataDirectoryUri(targetUri);
        String targetPath = targetUri.getPath();
        File targetFile = new File(targetPath);
        if (!fileExist(sourceUri)) {
            throw new FileNotFoundException();
        } else if (isFile(sourceUri) && ((targetFile.exists() && !targetFile.isDirectory()) || !isDirectory)) {
            copyFile(new File(sourceUri), new File(targetUri));
        } else if (isFile(sourceUri)) {
            copyFileToDirectory(new File(sourceUri), new File(targetUri));
        } else if (isDirectory(sourceUri)) {
            copyDirectory(new File(sourceUri), new File(targetUri));
        }
    }

    private void copyDirectory(File sourceDirectory, File targetDirectory) throws IOException {
        if (!targetDirectory.exists()) {
            targetDirectory.mkdirs();
        }
        FileUtils.copyDirectory(sourceDirectory, targetDirectory, false);
    }

    private void copyFile(File sourceFile, File destinationFile) throws IOException {
        FileUtils.copyFile(sourceFile, destinationFile);
    }

    private void copyFileToDirectory(File sourceFile, File targetDirectory) throws IOException {
        FileUtils.copyFileToDirectory(sourceFile, targetDirectory);
    }

    @Override
    public boolean delete(URI uri) throws IOException {
        if (Objects.isNull(uri) || uri.getPath().isEmpty()) {
            /*
            This exception is thrown when the passed URI is empty or null.
            Using this URI would cause the deletion of the metadata directory.
            */
            throw new IOException("Attempt to delete subdirectory with URI that is empty or null!");
        }
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        File file = new File(uri);
        if (file.exists()) {
            if (file.isFile()) {
                return Files.deleteIfExists(file.toPath());
            }
            if (file.isDirectory()) {
                FileUtils.deleteDirectory(file);
                return true;
            }
            return false;
        }
        return true;
    }

    @Override
    public void move(URI sourceUri, URI targetUri) throws IOException {
        copy(sourceUri, targetUri);
        delete(sourceUri);
    }

    @Override
    public URI rename(URI uri, String newName) throws IOException {
        if (Objects.isNull(uri) || Objects.isNull(newName)) {
            return null;
        }

        String substring = uri.toString().substring(0, uri.toString().lastIndexOf('/') + 1);
        if (newName.contains("/")) {
            newName = newName.substring(newName.lastIndexOf('/') + 1);
        }
        URI newFileUri = URI.create(substring + newName);
        URI mappedFileURI = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        URI mappedNewFileURI = fileMapper.mapUriToKitodoDataDirectoryUri(newFileUri);

        if (!fileExist(mappedFileURI)) {
            logger.debug("File {} does not exist for renaming.", uri.getPath());
            throw new FileNotFoundException(uri + " does not exist for renaming.");
        }

        if (fileExist(mappedNewFileURI)) {
            String message = "Renaming of " + uri + " into " + newName + " failed: Destination exists.";
            logger.error(message);
            throw new IOException(message);
        }

        return performRename(mappedFileURI, mappedNewFileURI);
    }

    private URI performRename(URI mappedFileURI, URI mappedNewFileURI) throws IOException {
        File fileToRename = new File(mappedFileURI);
        File renamedFile = new File(mappedNewFileURI);

        final int sleepIntervalMilliseconds = 20;
        final int maxWaitMilliseconds = KitodoConfig
                .getIntParameter(ParameterFileManagement.FILE_MAX_WAIT_MILLISECONDS);

        boolean success;
        int millisWaited = 0;

        do {
            if (SystemUtils.IS_OS_WINDOWS && millisWaited == sleepIntervalMilliseconds) {
                logger.warn("Renaming {} failed. This is Windows. Running the garbage collector may yield good"
                        + " results. Forcing immediate garbage collection now!", fileToRename.getName());
                System.gc();
            }
            success = fileToRename.renameTo(renamedFile);
            if (!success) {
                if (millisWaited == 0) {
                    logger.info("Renaming {} failed. File may be locked. Retrying...", fileToRename.getName());
                }
                waitForThread(sleepIntervalMilliseconds);
                millisWaited += sleepIntervalMilliseconds;
            }
        } while (!success && millisWaited < maxWaitMilliseconds);

        if (!success) {
            logger.error("Rename {} failed. This is a permanent error. Giving up.", fileToRename.getName());
            throw new IOException(
                    "Renaming of " + fileToRename.getName() + " into " + renamedFile.getName() + " failed.");
        }

        if (millisWaited > 0) {
            logger.info("Rename finally succeeded after {} milliseconds.", Integer.toString(millisWaited));
        }
        return fileMapper.unmapUriFromKitodoDataDirectoryUri(Paths.get(renamedFile.getPath()).toUri());
    }

    private void waitForThread(int sleepIntervalMilliseconds) {
        try {
            Thread.sleep(sleepIntervalMilliseconds);
        } catch (InterruptedException e) {
            logger.warn("The thread was interrupted");
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public boolean fileExist(URI uri) {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        return new File(uri).exists();
    }

    @Override
    public boolean isFile(URI uri) {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        return new File(uri).isFile();
    }

    @Override
    public boolean isDirectory(URI directory) {
        directory = fileMapper.mapUriToKitodoDataDirectoryUri(directory);
        return new File(directory).isDirectory();
    }

    @Override
    public boolean canRead(URI uri) {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        return new File(uri).canRead();
    }

    @Override
    public Integer getNumberOfFiles(FilenameFilter filter, URI directory) {
        int count = 0;
        directory = fileMapper.mapUriToKitodoDataDirectoryUri(directory);
        if (filter == null) {
            count += iterateOverDirectories(directory);
        } else {
            count += iterateOverSpecificDirectories(filter, directory);
        }
        return count;
    }

    /**
     * Iterate over children directories of directory.
     *
     * @param directory
     *            as URI
     * @return amount of files
     */
    private Integer iterateOverDirectories(URI directory) {
        int count = 0;
        if (isDirectory(directory)) {
            List<URI> children = getSubUris(null, directory);
            for (URI child : children) {
                child = fileMapper.mapUriToKitodoDataDirectoryUri(child);
                if (isDirectory(child)) {
                    count += getNumberOfFiles(null, child);
                } else {
                    count += 1;
                }
            }
        }
        return count;
    }

    /**
     * Iterate over children specific directories of directory.
     *
     * @param directory
     *            as URI
     * @return amount of specific (eg. image) files
     */
    private Integer iterateOverSpecificDirectories(FilenameFilter filter, URI directory) {
        int count = 0;
        if (isDirectory(directory)) {
            count = getSubUris(filter, directory).size();
            List<URI> children = getSubUris(null, directory);
            for (URI child : children) {
                child = fileMapper.mapUriToKitodoDataDirectoryUri(child);
                count += getNumberOfFiles(filter, child);
            }
        }
        return count;
    }

    @Override
    public Long getSizeOfDirectory(URI directory) throws IOException {
        if (!directory.isAbsolute()) {
            directory = fileMapper.mapUriToKitodoDataDirectoryUri(directory);
        }
        if (isDirectory(directory)) {
            return FileUtils.sizeOfDirectory(new File(directory));
        } else {
            throw new IOException("Given URI doesn't point to the directory!");
        }
    }

    @Override
    public String getFileNameWithExtension(URI uri) {
        return FilenameUtils.getName(uri.getPath());
    }

    @Override
    public List<URI> getSubUris(FilenameFilter filter, URI uri) {
        if (!uri.isAbsolute()) {
            uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        }
        List<URI> resultList = new ArrayList<>();
        File[] files;
        if (filter == null) {
            files = listFiles(new File(uri));
        } else {
            files = listFiles(filter, new File(uri));
        }
        for (File file : files) {
            URI tempURI = Paths.get(file.getPath()).toUri();
            resultList.add(fileMapper.unmapUriFromKitodoDataDirectoryUri(tempURI));
        }
        return resultList;
    }

    /**
     * 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 unchecked != null ? unchecked : new File[0];
    }

    /**
     * Lists all files at the given path and with a given filter.
     *
     * @param file
     *            the directory to get the Files from
     * @return an Array of Files
     */
    private File[] listFiles(FilenameFilter filter, File file) {
        File[] unchecked = file.listFiles(filter);
        return unchecked != null ? unchecked : new File[0];
    }

    @Override
    public URI createProcessLocation(String processId) throws IOException {
        File processRootDirectory = new File(KitodoConfig.getKitodoDataDirectory() + File.separator + processId);
        if (!processRootDirectory.exists() && !processRootDirectory.mkdir()) {
            throw new IOException("Could not create processRoot directory.");
        }
        return fileMapper.unmapUriFromKitodoDataDirectoryUri(Paths.get(processRootDirectory.getPath()).toUri());
    }

    @Override
    public URI createUriForExistingProcess(String processId) {
        return URI.create(processId);
    }

    @Override
    public URI getProcessSubTypeUri(URI processBaseUri, String processTitle, ProcessSubType subType,
            String resourceName) {
        return URI.create(getProcessSubType(processBaseUri.toString(), processTitle, subType, resourceName));
    }

    /**
     * Get part of URI specific for process and process sub type.
     *
     * @param processTitle
     *            tile of process
     * @param processSubType
     *            object
     * @param resourceName
     *            as String
     * @return process specific part of URI
     */
    private String getProcessSubType(String processID, String processTitle, ProcessSubType processSubType,
            String resourceName) {
        processTitle = encodeTitle(processTitle);
        final String ocr = "/ocr/";
        if (Objects.isNull(resourceName)) {
            resourceName = "";
        }

        switch (processSubType) {
        case IMAGE:
            return processID + "/" + IMAGES_DIRECTORY_NAME + "/" + resourceName;
        case IMAGE_SOURCE:
            return getSourceDirectory(processID, processTitle) + resourceName;
        case META_XML:
            return processID + "/meta.xml";
        case TEMPLATE:
            return processID + "/template.xml";
        case IMPORT:
            return processID + "/import/" + resourceName;
        case OCR:
            return processID + ocr;
        case OCR_PDF:
            return processID + ocr + processTitle + "_pdf/" + resourceName;
        case OCR_TXT:
            return processID + ocr + processTitle + "_txt/" + resourceName;
        case OCR_WORD:
            return processID + ocr + processTitle + "_wc/" + resourceName;
        case OCR_ALTO:
            return processID + ocr + processTitle + "_alto/" + resourceName;
        default:
            return "";
        }
    }

    /**
     * Remove possible white spaces from process titles.
     *
     * @param title
     *            process title
     * @return encoded process title
     */
    private String encodeTitle(String title) {
        if (title.contains(" ")) {
            title = title.replace(" ", "__");
        }
        return title;
    }

    /**
     * Gets the image source directory.
     *
     * @param processTitle
     *            title of the process, to get the source directory for
     * @return the source directory as a string
     */
    private URI getSourceDirectory(String processId, String processTitle) {
        final String suffix = "_" + KitodoConfig.getParameter(ParameterFileManagement.DIRECTORY_SUFFIX, "tif");
        URI dir = URI.create(getProcessSubType(processId, processTitle, ProcessSubType.IMAGE, null));
        FilenameFilter filterDirectory = new FileNameEndsWithFilter(suffix);
        URI sourceFolder = URI.create("");
        try {
            List<URI> directories = getSubUris(filterDirectory, dir);
            if (directories.isEmpty()) {
                sourceFolder = dir.resolve(processTitle + suffix + "/");
                if (KitodoConfig.getBooleanParameter(ParameterFileManagement.CREATE_SOURCE_FOLDER, false)) {
                    if (!fileExist(dir)) {
                        createDirectory(dir.resolve(".."), IMAGES_DIRECTORY_NAME);
                    }
                    createDirectory(dir, processTitle + suffix);
                }
            } else {
                sourceFolder = dir.resolve("/" + directories.get(0));
            }
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }

        return sourceFolder;
    }

    @Override
    public boolean createSymLink(URI homeUri, URI targetUri, boolean onlyRead, String userLogin) {
        File imagePath = new File(fileMapper.mapUriToKitodoDataDirectoryUri(homeUri));
        File userHome = new File(getDecodedPath(targetUri));
        if (userHome.exists()) {
            return false;
        }

        String command = KitodoConfig.getParameter("script_createSymLink");
        CommandService commandService = new CommandService();
        List<String> parameters = new ArrayList<>();
        parameters.add(imagePath.getAbsolutePath());
        parameters.add(userHome.getAbsolutePath());

        if (onlyRead) {
            parameters.add(KitodoConfig.getParameter("UserForImageReading", "root"));
        } else {
            parameters.add(userLogin);
        }

        try {
            return commandService.runCommand(new File(command), parameters).isSuccessful();
        } catch (FileNotFoundException e) {
            logger.error("FileNotFoundException in createSymLink", e);
            return false;
        } catch (IOException e) {
            logger.error("IOException in createSymLink", e);
            return false;
        }
    }

    @Override
    public boolean deleteSymLink(URI homeUri) {
        File homeFile = new File(fileMapper.mapUriToKitodoDataDirectoryUri(homeUri));

        String command = KitodoConfig.getParameter("script_deleteSymLink");
        CommandService commandService = new CommandService();
        List<String> parameters = new ArrayList<>();
        parameters.add(homeFile.getAbsolutePath());
        try {
            return commandService.runCommand(new File(command), parameters).isSuccessful();
        } catch (FileNotFoundException e) {
            logger.error("FileNotFoundException in deleteSymLink", e);
            return false;
        } catch (IOException e) {
            logger.error("IOException in deleteSymLink", e);
            return false;
        }
    }

    private String getDecodedPath(URI uri) {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        String uriToDecode = new File(uri).getPath();
        String decodedPath;
        try {
            decodedPath = URLDecoder.decode(uriToDecode, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage(), e);
            return "";
        }
        return decodedPath;
    }

    public File getFile(URI uri) {
        uri = fileMapper.mapUriToKitodoDataDirectoryUri(uri);
        return new File(uri);
    }
}