org.kitodo.filemanagement.locking.ImmutableReadFileManagement.java Source code

Java tutorial

Introduction

Here is the source code for org.kitodo.filemanagement.locking.ImmutableReadFileManagement.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.locking;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * The immutable read file management manages the temporary files that need to
 * be created for immutable reading and later deleted. The immutable read file
 * management belongs to the lock management and should only be instantiated by
 * the lock management. Basically, you could have programmed everything together
 * into a class, but then it would have become very large, hence the division.
 */
class ImmutableReadFileManagement {
    private static final Logger logger = LogManager.getLogger(ImmutableReadFileManagement.class);

    /**
     * Map of the up-to-date copies.
     */
    private final Map<URI, URI> upToDateCopies = new HashMap<>();

    /**
     * Which copy of a URI was passed to which user.
     */
    private final Map<URI, UserMapForURI> urisGivenToUsers = new HashMap<>();

    /**
     * Checks if the copy in question can be disposed of, and if so, does it. A
     * copy can be discarded if it isnt noted by any user more than his or her
     * visible version of this file. To find this out, the entire map must be
     * searched below the original URI. This process can be parallelized very
     * well. If the copy can be disposed of, it must first be removed from the
     * map of up-to-date copies so that it will not be reissued during the
     * deletion, and then deleted. If the file cannot be deleted, a warning is
     * written to the logfile. System administrators should be on the lookout
     * for such alerts, because if they occur frequently, this could signal that
     * the partiton is filling up with temporary files. This can be a problem
     * especially on Windows, because the Java virtual machine occasionally has
     * problems deleting files here.
     *
     * @param copyInQuestion
     *            URI of the file that may be deleted
     * @param originUri
     *            URI of the file of which the file in question is a copy. Since
     *            the maps storing the temporary file information use the URI of
     *            the source file as a key, they are much more efficient to use
     *            if it is known. (It would work without, but then you would
     *            have to chew through the whole map every time.)
     */
    private void cleanUp(URI copyInQuestion, URI originUri) {
        UserMapForURI userMapForURI = urisGivenToUsers.get(originUri);
        if (Objects.isNull(userMapForURI) || userMapForURI.entrySet().parallelStream()
                .map(userMapForUriEntry -> userMapForUriEntry.getValue().getKey())
                .noneMatch(readCopy -> readCopy.equals(copyInQuestion))) {

            URI upToDateCopy = upToDateCopies.get(originUri);
            if (Objects.nonNull(upToDateCopy) && upToDateCopy.equals(copyInQuestion)) {
                upToDateCopies.remove(originUri);
            }

            File fileToDelete = new File(copyInQuestion.getPath());
            if (FileUtils.deleteQuietly(fileToDelete)) {
                logger.debug("the temporary read copy {} was deleted", fileToDelete);
            } else {
                logger.warn("The temporary read file {} could not be deleted.", fileToDelete);
            }
        }
    }

    /**
     * Creates a read copy of a file. The copy will be created in the same
     * directory and marked as a temporary file. This method is called if there
     * is no or no up-to-date copy of the file, but one has been requested.
     *
     * @param uri
     *            file to be copied
     * @return URI of the copy
     * @throws IOException
     *             if the file does not exist or if an error occurs in disk
     *             access, e.g. because the write permission for the directory
     *             is missing
     */
    private URI createReadCopy(URI uri) throws IOException {
        File srcFile = new File(uri.getPath());
        File destFile = deriveTempFile(srcFile);
        FileUtils.copyFile(srcFile, destFile);
        logger.debug("{} was created as a temporary reading copy of {}", destFile.getName(), srcFile);
        return destFile.toURI();
    }

    /**
     * Derives the name of the temporary file from the name of the original file
     * and creates an empty placeholder file with the derived file name in the
     * same directory. The name of the temporary file is formed depending on the
     * operating system so that the file can easily be recognized as a temporary
     * file. For example, {@code PPN012345678.xml} could become
     * {@code PPN012345678.xml-2448103947405693446~}, but
     * {@code PPN012345678_xml-2448103947405693446.tmp} on Windows. The sequence
     * of numbers is inserted by the Java runtime and chosen so that it does not
     * exist guaranteed.
     *
     * @param srcFile
     *            the original file
     * @return the derived file
     * @throws IOException
     *             if the file does not exist or if an error occurs in disk
     *             access, e.g. because the write permission for the directory
     *             is missing
     */
    private File deriveTempFile(File srcFile) throws IOException {
        String prefix = srcFile.getName();
        if (SystemUtils.IS_OS_WINDOWS) {
            prefix = prefix.replace('.', '_');
        }
        prefix = prefix.concat("-");
        String suffix = SystemUtils.IS_OS_WINDOWS ? ".tmp" : "~";
        File directory = srcFile.getParentFile();
        return File.createTempFile(prefix, suffix, directory);
    }

    /**
     * Returns the immutable read copy for a user and a URI. If necessary, the
     * file must be created at this point.
     *
     * @param user
     *            user for whom the immutable read copy is to be returned
     * @param uri
     *            URI for which the immutable read copy is to be returned
     * @return the immutable read copy
     * @throws IOException
     *             if the file does not exist or if an error occurs in disk
     *             access, e.g. because the write permission for the directory
     *             is missing
     */
    URI getImmutableReadCopy(String user, URI uri) throws IOException {
        urisGivenToUsers.computeIfAbsent(uri, create -> new UserMapForURI());
        UserMapForURI userMapForURI = urisGivenToUsers.get(uri);
        if (userMapForURI.containsKey(user)) {
            Pair<URI, AtomicInteger> uriWithCount = userMapForURI.get(user);
            uriWithCount.getValue().incrementAndGet();
            return uriWithCount.getKey();
        } else {
            URI uriOfImmutableReadCopy = getUpToDateCopy(uri);
            userMapForURI.put(user, Pair.of(uriOfImmutableReadCopy, new AtomicInteger(1)));
            return uriOfImmutableReadCopy;
        }
    }

    /**
     * Returns the URI of a up-to-date read copy of the requested URI. Either,
     * there is already one, or a copy process is triggered and the newly
     * created copy is noted as up-to-date.
     *
     * @param originUri
     *            URI for which the immutable read copy is to be returned
     * @return the immutable read copy
     * @throws IOException
     *             if the file does not exist or if an error occurs in disk
     *             access, e.g. because the write permission for the directory
     *             is missing
     */
    private URI getUpToDateCopy(URI originUri) throws IOException {
        URI upToDateCopy = upToDateCopies.get(originUri);
        if (upToDateCopy == null) {
            upToDateCopy = createReadCopy(originUri);
            upToDateCopies.put(originUri, upToDateCopy);
        }
        return upToDateCopy;
    }

    /**
     * Returns whether there is already a read copy of this file.
     *
     * @param originUri
     *            URI for which the immutable read copy is searched
     * @return true, if there is an up-to-date copy of that URI
     */
    boolean isHavingACopyOf(URI originUri) {
        return upToDateCopies.containsKey(originUri);
    }

    /**
     * Marks a URI as changed. If another user subsequently requests a read copy
     * of this URI, a new copy is created for him.
     *
     * @param uri
     *            URI of the file that has changed
     */
    void markAsChanged(URI uri) {
        upToDateCopies.remove(uri);
    }

    /**
     * Removes a reference to the use of a file as a temporary copy. If no
     * reference is left, the user is logged out of the temporary copy. Then it
     * will also be checked if the copy can be deleted.
     *
     * @param originUri
     *            URI to which the user requested the immutable read lock, that
     *            is, the URI of the original file
     * @param user
     *            user who held the lock
     */
    void maybeRemoveReadFile(URI originUri, String user) {
        UserMapForURI userMapForURI = urisGivenToUsers.get(originUri);
        if (Objects.nonNull(userMapForURI)) {
            Pair<URI, AtomicInteger> copyUriWithCount = userMapForURI.get(user);
            if (copyUriWithCount.getValue().decrementAndGet() == 0) {
                userMapForURI.remove(user);
                if (userMapForURI.isEmpty()) {
                    urisGivenToUsers.remove(originUri);
                }
                cleanUp(copyUriWithCount.getKey(), originUri);
            }
        }
    }
}