Java tutorial
/** * Copyright (c) 2008-2014, XebiaLabs B.V., All rights reserved. * * * Overtherepy is licensed under the terms of the GPLv2 * <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most XebiaLabs Libraries. * There are special exceptions to the terms and conditions of the GPLv2 as it is applied to * this software, see the FLOSS License Exception * <http://github.com/xebialabs/overthere/blob/master/LICENSE>. * * 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; version 2 * of the License. * * 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, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth * Floor, Boston, MA 02110-1301 USA */ package com.xebialabs.overtherepy; import java.io.IOException; import java.io.InputStream; import java.io.File; import java.util.List; import java.util.Map; import java.util.Set; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; import com.google.common.io.InputSupplier; import com.google.common.io.Files; import com.xebialabs.overthere.OverthereFile; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newHashSet; /** * Compares 2 directories and determines which files were added, removed or changed. */ public class DirectoryDiff { private OverthereFile leftSide; private OverthereFile rightSide; private HashFunction hashFunction = Hashing.goodFastHash(32); /** * Constructor * * @param leftSide directory to compare * @param rightSide directory to compare */ public DirectoryDiff(OverthereFile leftSide, OverthereFile rightSide) { checkArgument(leftSide.isDirectory(), "File [%s] must be a directory.", leftSide); checkArgument(rightSide.isDirectory(), "File [%s] must be a directory.", rightSide); this.leftSide = leftSide; this.rightSide = rightSide; } /** * Calculate the differences between the two directories that this class was constructed with. * * @return differences * @throws IOException */ public DirectoryChangeSet diff() throws IOException { DirectoryChangeSet changeSet = new DirectoryChangeSet(); compareDirectoryRecursive(leftSide, rightSide, changeSet); return changeSet; } /** * Calculate an MD5 hash for the given file. * * @param file for which MD5 should be calculated. * @return MD5 hash * @throws IOException */ public static String md5(final OverthereFile file) throws IOException { File sourceFile = new File(file.getPath()); ByteSource source = Files.asByteSource(sourceFile); return source.hash(Hashing.md5()).toString(); } /* * Intermediate method for recursion, so that objects created in the compareDirectory method can be * garbage collected. */ private void compareDirectoryRecursive(OverthereFile left, OverthereFile right, DirectoryChangeSet changeSet) throws IOException { List<OverthereFile[]> dirsToRecurse = compareDirectory(left, right, changeSet); for (OverthereFile[] leftAndRightDir : dirsToRecurse) { compareDirectoryRecursive(leftAndRightDir[0], leftAndRightDir[1], changeSet); } } private List<OverthereFile[]> compareDirectory(OverthereFile left, OverthereFile right, DirectoryChangeSet changeSet) throws IOException { Set<FileWrapper> leftFiles = listFiles(left); Set<FileWrapper> rightFiles = listFiles(right); //find new files Set<FileWrapper> filesAdded = Sets.difference(rightFiles, leftFiles); //find removed files Set<FileWrapper> filesRemoved = Sets.difference(leftFiles, rightFiles); //find changed files Set<FileWrapper> potentialChangedFiles = newHashSet(leftFiles); potentialChangedFiles.removeAll(filesRemoved); //filter out directories Map<FileWrapper, FileWrapper> rightFilesIndex = newHashMap(); for (FileWrapper file : rightFiles) { rightFilesIndex.put(file, file); } Set<FileWrapper> filesChanged = newHashSet(); for (FileWrapper potentialChangedFile : Sets.filter(potentialChangedFiles, FileWrapperPredicates.FILE)) { HashCode leftHash = hash(potentialChangedFile.getFile(), hashFunction); FileWrapper rightFile = rightFilesIndex.get(potentialChangedFile); HashCode rightHash = hash(rightFile.getFile(), hashFunction); if (!leftHash.equals(rightHash)) { filesChanged.add(rightFile); } } Function<FileWrapper, OverthereFile> unwrapFunction = new Function<FileWrapper, OverthereFile>() { @Override public OverthereFile apply(final FileWrapper input) { return input.getFile(); } }; changeSet.getRemoved().addAll(Collections2.transform(filesRemoved, unwrapFunction)); changeSet.getAdded().addAll(Collections2.transform(filesAdded, unwrapFunction)); changeSet.getChanged().addAll(Collections2.transform(filesChanged, unwrapFunction)); Set<FileWrapper> potentialChangedDirectories = Sets.filter(potentialChangedFiles, FileWrapperPredicates.DIRECTORY); List<OverthereFile[]> directoriesStillToCheck = newArrayList(); for (FileWrapper potentialChangedDirectory : potentialChangedDirectories) { directoriesStillToCheck.add(new OverthereFile[] { potentialChangedDirectory.getFile(), rightFilesIndex.get(potentialChangedDirectory).getFile() }); } return directoriesStillToCheck; } private Set<FileWrapper> listFiles(OverthereFile dir) { return newHashSet(Lists.transform(newArrayList(dir.listFiles()), new WrapFile())); } private HashCode hash(final OverthereFile file, HashFunction hashFunction) throws IOException { File sourceFile = new File(file.getPath()); ByteSource source = Files.asByteSource(sourceFile); return source.hash(hashFunction); } public static class DirectoryChangeSet { private List<OverthereFile> removed = newArrayList(); private List<OverthereFile> added = newArrayList(); private List<OverthereFile> changed = newArrayList(); public List<OverthereFile> getAdded() { return added; } public List<OverthereFile> getChanged() { return changed; } public List<OverthereFile> getRemoved() { return removed; } } static class WrapFile implements Function<OverthereFile, FileWrapper> { @Override public FileWrapper apply(final OverthereFile input) { return new FileWrapper(input); } } static class FileWrapper { private OverthereFile file; FileWrapper(OverthereFile file) { this.file = file; } public OverthereFile getFile() { return file; } @Override public int hashCode() { return file.getName().hashCode(); } @Override public boolean equals(final Object obj) { if (obj instanceof FileWrapper) { return file.getName().equals(((FileWrapper) obj).file.getName()); } return false; } @Override public String toString() { return file.toString(); } } enum FileWrapperPredicates implements Predicate<FileWrapper> { FILE { @Override public boolean apply(final FileWrapper input) { return input.getFile().isFile(); } }, DIRECTORY { @Override public boolean apply(final FileWrapper input) { return input.getFile().isDirectory(); } } } }