de.joinout.criztovyl.tools.directory.DirectoryChanges.java Source code

Java tutorial

Introduction

Here is the source code for de.joinout.criztovyl.tools.directory.DirectoryChanges.java

Source

/**
This is a part of my tool collection.
Copyright (C) 2014 Christoph "criztovyl" Schulz
    
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.joinout.criztovyl.tools.directory;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.joinout.criztovyl.tools.file.Path;
import de.joinout.criztovyl.tools.files.FileList;

/**
 * Locates changes between directories.
 * 
 * @author criztovyl
 * 
 */
public class DirectoryChanges {

    private final FileList current, previous;
    private Logger logger;
    private HashSet<Path> del, changed, nevv;

    /**
     * Creates a new instance. <code>previous</code> and <code>current</code> list are set.
     * 
     * @param previous
     *            the previous list
     * @param current
     *            the current list
     */
    public DirectoryChanges(FileList current, FileList previous) {

        //Setup FileLists
        this.current = new FileList(current);
        this.previous = new FileList(previous);

        //Remove symbolic links and their sub-directories from set

        this.current.remove(this.previous.getSymLinks(true), true, false);
        this.previous.remove(this.current.getSymLinks(true), true, false);

        logger = LogManager.getLogger();
    }

    /**
     * Creates a new instance. <code>current</code> will created upon a path and <code>previous</code> will be loaded from JSON.
     * 
     * @param path
     *            the path
     * @throws IOException If an I/O error occurs when creating {@link FileList} by {@link FileList#FileList(Path, boolean)}
     */
    public DirectoryChanges(Path path) throws IOException {
        this(new FileList(path, false).relative(), new FileList(path, true).relative());
    }

    /**
     * Creates a new instance. <code>current</code> and <code>previous</code> are created from {@link Path}s by {@link FileList#FileList(Path, boolean)} and {@link FileList#relative()}.
     * 
     * @param older
     *            the older directory
     * @param newer
     *            the newer directory
     * @throws IOException 
     */
    public DirectoryChanges(Path older, Path newer) throws IOException {
        this(new FileList(newer, false).relative(), new FileList(older, false).relative());
    }

    /**
     * Creates a new DirectoryChanges upon the both {@link FileList}s which are created with the given regular exception for excluding files.
     * @param current the current directory
     * @param previous the previous directory
     * @param ignoreRegex the regular exception for excluding files.
     * @throws IOException 
     */
    public DirectoryChanges(Path current, Path previous, String ignoreRegex) throws IOException {
        this(new FileList(current, ignoreRegex).relative(), new FileList(previous, ignoreRegex).relative());
    }

    /**
     * 
     * @return the current {@link FileList}.
     */
    public FileList getCurrentList() {
        return current;
    }

    /**
     * Pass-through to {@link #getDeletedFiles(boolean)} with <code>false</code> so that there will no recalculation.
     * 
     * @return a {@link Set} of {@link Path}s
     * @see #getDeletedFiles(boolean)
     */
    public Set<Path> getDeletedFiles() {
        return getDeletedFiles(false);
    }

    /**
     * Locates all deleted files by subtract the current set from the previous.
     * @return a {@link Set} of {@link Path}s
     * @param forceRecalculate whether there should be a recalculation of the deleted files
     */
    public Set<Path> getDeletedFiles(boolean forceRecalculate) {

        if (forceRecalculate || del == null) { //(Re-)calculate if is wanted or there is no previous calculation

            // Copy previous set and replace/initiate set for deleted files
            del = new HashSet<>(previous);

            //Inform if sizes above 1,000 elements
            if (previous.size() > 1000 || current.size() > 1000) {
                logger.info("There are much files in the directories, the calculation can take a moment :)");
                logger.debug("Sizes: Current: {}; Previous: {}", current.size(), previous.size());
            }

            // Remove all which is in current set
            del.removeAll(current);

        }

        //Return
        return del;
    }

    /**
     * Pass-through to {@link #getChangedFiles(boolean)} with <code>false</code> so that there will no recalculation.
     * @return a {@link Set} of {@link Path}s
     * @see #getChangedFiles(boolean)
     */
    public Set<Path> getChangedFiles() {
        return getChangedFiles(false);
    }

    /**
     * Locates all changed files, does not include new or deleted files.<br>
     * As first there is created/received a map with hash-strings as keys and
     * {@link Path}s as values for the current and previous
     * {@link FileList} via {@link FileList#getMappedHashedModifications()}.
     * Then all keys of the previous map are removed from the current map and
     * the remaining values are returned.<br>
     * Only files which content changed are included.
     * 
     * @return a {@link Set} of {@link Path}s
     * @param forceRecalculate whether there should be a recalculation of the changed files
     * @see FileList#getMappedHashedModifications()
     */
    public Set<Path> getChangedFiles(boolean forceRecalculate) {

        if (forceRecalculate || changed == null) { //(Re-)calculate if is wanted or there is no previous calculation

            // Get all new and deleted files, they are not included in the modified
            // files, add them to list which files are ignored
            final Set<Path> ignore = new HashSet<>();
            ignore.addAll(getDeletedFiles());
            ignore.addAll(getNewFiles());

            if (logger.isDebugEnabled())
                logger.debug("Files ignored: {}", new TreeSet<>(ignore));

            // Create a map for modificated files and put modifications map from current directory
            final HashMap<String, Path> mod = new HashMap<>(current.getMappedHashedModifications(ignore));

            //Receive modifications from previous directory
            Map<String, Path> mod_p = previous.getMappedHashedModifications(ignore);

            //Intersect map keys
            Set<String> intersection = new HashSet<>(mod.keySet());
            intersection.retainAll(mod_p.keySet());

            if (logger.isDebugEnabled()) {

                if (!(mod_p.size() > 500))
                    logger.debug("Modifications map of previous list: {}", new TreeMap<>(mod_p));
                else
                    logger.debug("Previous modification map is bigger than 500 elements, will not print out.");

                if (!(mod_p.size() > 500))
                    logger.debug("Modifications map of current list: {}", new TreeMap<>(mod));
                else
                    logger.debug("Current modification map is bigger than 500 elements, will not print out.");

                if (!(mod_p.size() > 500))
                    logger.debug("Intersection of above: {}", intersection);
                else
                    logger.debug("Intersection set is bigger than 500 elements, will not print out.");
            }

            //Merge maps
            mod.putAll(mod_p);

            // Remove everything which is in both maps
            mod.keySet().removeAll(new TreeSet<>(intersection));

            //Only files which contents changed stay in map
            //Iterate over keys
            for (Iterator<String> i = mod.keySet().iterator(); i.hasNext();) {

                //Get path
                Path path = mod.get(i.next());

                //Check if file has changed (may throw I/O exception)
                try {
                    if (contentChanged(path))

                        //Remove if is not newer then complement file
                        if (!FileUtils.isFileNewer(path.getFile(), getComplementPath(path).getFile()))
                            i.remove();
                        else
                            ;

                    //Has not changed, remove from map
                    else
                        i.remove();
                } catch (IOException e) { //Catch IOException, remove from map to avoid further errors
                    i.remove();
                    if (logger.isWarnEnabled())
                        logger.warn(
                                "Caught IOException while testing if file is newer: \"{}\". Removing from modifications to prevent further errors.",
                                path);
                    if (logger.isDebugEnabled())
                        logger.debug(e);
                }
            }

            //Save for reuse
            changed = new HashSet<>(mod.values());
        }

        //Return changed files
        return changed;
    }

    /**
     * Pass-through to {@link #getNewFiles(boolean)} with <code>false</code> so that there will no recalculation.
     * @return a {@link Set} of {@link Path}s
     * @see #getNewFiles(boolean)
     */
    public Set<Path> getNewFiles() {
        return getNewFiles(false);
    }

    /**
     * Locates all new files by subtract the previous set from the current set
     * @param forceRecalculate whether new files should be recalculated
     * @return a {@link Set} of {@link Path}s
     */
    public Set<Path> getNewFiles(boolean forceRecalculate) {

        if (forceRecalculate || nevv == null) { //(Re-)calculate if is wanted or there is no previous calculation

            // Initiate/Replace Set for new files with a copy of the current set
            nevv = new HashSet<>(current);

            // Remove all what is in the previous set
            nevv.removeAll(previous);

        }
        //Return
        return nevv;
    }

    /**
     * 
     * @return the previous {@link FileList}
     */
    public FileList getPreviousList() {
        return previous;
    }

    /**
     * Saves the <code>previous</code> and <code>current</code> {@link FileList} by {@link FileList#save()}.
     */
    public void save() {
        current.save();
        previous.save();
    }

    /**
     * Checks, if two files are really different in the both lists by checking whether the checksums are different.
     * @param path a {@link Path} (should be from <code>current</code> or <code>previous</code>)
     * @return <code>true</code> if checksums are different and <code>false</code> if they are equal or the path is a directory.
     * @throws IOException If an I/O error occurs.
     */
    public boolean contentChanged(Path path) throws IOException {

        path = makeRelative(path);

        try {
            return FileUtils.checksumCRC32(current.getDirectory().append(path).realPath().getFile()) != FileUtils
                    .checksumCRC32(previous.getDirectory().append(path).realPath().getFile());
        } catch (IllegalArgumentException e) { //Catch if is directory
            logger.warn("Cannot compare file {}, is directory.", path);
            return false;
        }
    }

    /**
     * Makes a path relative also if you don't know whether it is from <code>current</code> or <code>previous</code> {@link FileList}. 
     * @param path the {@link Path}
     * @return a {@link Path}
     */
    public Path makeRelative(Path path) {

        path = path.relativeTo(current.getDirectory());
        path = path.relativeTo(previous.getDirectory());

        return path;
    }

    /**
     * Calculates the path of the complementary directory.<br>
     * i.e. if <code>current</code> is <code>currentdir/</code> and <code>previous</code> is <code>prevdir/</code> and you run this on <code>currentdir/dir/file</code> you will get <code>prevdir/</code>.
     * @param path the path as {@link Path}
     * @return a {@link Path}
     */
    public Path getComplementDirectory(Path path) {

        return path.getParent(makeRelative(path)).equals(current.getDirectory()) ? previous.getDirectory()
                : current.getDirectory();

    }

    /**
     * Calculates the path of the complementary file.<br>
     * Following the example from {@link #getComplementDirectory(Path)} you will get <code>prevdir/dir/file</code>.
     * @param path the path as a {@link Path}
     * @return a {@link Path}
     */
    public Path getComplementPath(Path path) {
        return getComplementDirectory(path).append(makeRelative(path));
    }

}