001/*
002 * #%L
003 * Netarchivesuite - common - test
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023
024package dk.netarkivet.testutils;
025
026import java.io.File;
027import java.io.FileFilter;
028import java.io.FileNotFoundException;
029import java.io.FilenameFilter;
030import java.io.IOException;
031import java.util.ArrayList;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036import dk.netarkivet.common.exceptions.IOFailure;
037import dk.netarkivet.common.utils.FileUtils;
038
039/**
040 * File utilities specific to the test classes.
041 */
042@SuppressWarnings({"unchecked"})
043public class TestFileUtils {
044    public static final FilenameFilter NON_CVS_DIRS_FILTER = new FilenameFilter() {
045        public boolean accept(File directory, String filename) {
046            return !((filename.equals("CVS") && new File(directory, filename).isDirectory() || (filename.equals(".svn") && new File(
047                    directory, filename).isDirectory())));
048        }
049    };
050    public static final FileFilter DIRS_ONLY_FILTER = new FileFilter() {
051        public boolean accept(File dir) {
052            return dir.isDirectory();
053        }
054    };
055
056    /**
057     * Copy an entire directory from one location to another, skipping CVS directories. Note that this will silently
058     * overwrite old files, just like copyFile().
059     *
060     * @param from Original directory (or file, for that matter) to copy.
061     * @param to Destination directory, i.e. the 'new name' of the copy of the from directory.
062     * @throws IOFailure
063     */
064    public static final void copyDirectoryNonCVS(File from, File to) throws IOFailure {
065        if (from.isFile()) {
066            try {
067                FileUtils.copyFile(from, to);
068            } catch (Exception e) {
069                throw new IOFailure("Error copying from " + from.getAbsolutePath() + " to " + to.getAbsolutePath(), e);
070            }
071        } else {
072            if (from.getName().equals("CVS")) {
073                return;
074            }
075            if (from.getName().equals(".svn")) {
076                return;
077            }
078            if (!from.exists()) {
079                throw new IOFailure("Can't find directory " + from);
080            }
081
082            if (!from.isDirectory()) {
083                throw new IOFailure("File is not a directory: " + from);
084            }
085
086            to.mkdir();
087
088            if (!to.exists()) {
089                throw new IOFailure("Failed to create directory " + to);
090            }
091
092            File[] subfiles = from.listFiles();
093
094            for (File subfile : subfiles) {
095                copyDirectoryNonCVS(subfile, new File(to, subfile.getName()));
096            }
097        }
098    }
099
100    /**
101     * Compares the content of two directories and report all differences in the returned text string. If no difference
102     * are located, an empty string ("") is returned. All files located in the directories are treated as text files,
103     * and a text comparison is done on a line by line basis. This function will not work if the dirs contain binary
104     * files. No attempt is made to recover from errors.
105     *
106     * @param fstDir The directory to compare with sndDir
107     * @param sndDir The directory to compare with fstDir
108     * @return A text string describing the differences between the two dirs. Empty if no differences are found.
109     * @throws IOFailure if there are problems reading the content of the dirs.
110     */
111    public static String compareDirsText(File fstDir, File sndDir) throws IOFailure {
112        String result = "";
113
114        // retrieve lists of all files in the two directories
115        List<File> fstFiles = new ArrayList<File>();
116        List<File> sndFiles = new ArrayList<File>();
117
118        FileUtils.getFilesRecursively(fstDir.getPath(), fstFiles, "");
119        FileUtils.getFilesRecursively(sndDir.getPath(), sndFiles, "");
120
121        Map<String, File> fstFilesMap = new HashMap<String, File>();
122        for (File f : fstFiles) {
123            fstFilesMap.put(removePrefixDir(fstDir, f), f);
124        }
125
126        Map<String, File> sndFilesMap = new HashMap<String, File>();
127        for (File f : sndFiles) {
128            sndFilesMap.put(removePrefixDir(sndDir, f), f);
129        }
130
131        // The two dirs should contain the same files
132        for (String s : fstFilesMap.keySet()) {
133            if (!sndFilesMap.containsKey(s)) {
134                result += "Result file not found in second dir:" + s + "\n";
135            }
136        }
137
138        // The two dirs should contain the same files
139        for (String s : sndFilesMap.keySet()) {
140            if (!fstFilesMap.containsKey(s)) {
141                result += "Target file not found in result set:" + s + "\n";
142            }
143        }
144
145        // No reason to continue when sets of files do not match
146        if (result.length() > 0) {
147            return result;
148        }
149
150        // The files in each dir should be identical
151        try {
152            for (String s : fstFilesMap.keySet()) {
153                // Remove all carriage returns to make the comparison work on both Windows and Linux:
154                String fst = FileUtils.readFile(fstFilesMap.get(s)).replaceAll("\r", "");
155                String snd = FileUtils.readFile(sndFilesMap.get(s)).replaceAll("\r", "");
156                if (!fst.equals(snd)) {
157                    result += "Target and result differs for:" + s + "\n";
158                    result += getDifferences(fst, snd) + "\n";
159                }
160            }
161        } catch (FileNotFoundException e) {
162            throw new IOFailure("While comparing the files in " + fstFilesMap.keySet(), e);
163        } catch (IOException e) {
164            throw new IOFailure("While comparing the files in " + fstFilesMap.keySet(), e);
165        }
166
167        return result;
168    }
169
170    /**
171     * Strips a path prefix from a file name.
172     *
173     * @param dir The path prefix to remove from the given file's name.
174     * @param f The file to remove the path prefix from.
175     * @return The name of the file without the specified path prefix.
176     */
177    private static String removePrefixDir(File dir, File f) {
178        return f.getAbsolutePath().replaceAll(dir.getAbsolutePath(), "");
179    }
180
181    /**
182     * Return textual description of the differences between two strings.
183     *
184     * @param s1 strings to compare
185     * @param s2 strings to compare
186     * @return first line of text that differs
187     */
188    public static String getDifferences(String s1, String s2) {
189        String[] startStrings = s1.split("\n");
190        String[] endStrings = s2.split("\n");
191        List<Difference> differences = new Diff(startStrings, endStrings).diff();
192        StringBuilder res = new StringBuilder();
193
194        for (Difference d : differences) {
195            // Would like to do this as a unitfied diff (diff -u) instead,
196            // but that's a little more complex.
197            if (d.getAddedEnd() == -1) {
198                // Deletion
199                res.append("Deleted " + getDifferenceLines(d, false) + "\n");
200                for (int i = d.getDeletedStart(); i <= d.getDeletedEnd(); i++) {
201                    res.append("< " + startStrings[i] + "\n");
202                }
203            } else if (d.getDeletedEnd() == -1) {
204                // Addition
205                res.append("Added " + getDifferenceLines(d, true) + "\n");
206                for (int i = d.getAddedStart(); i <= d.getAddedEnd(); i++) {
207                    res.append("> " + endStrings[i] + "\n");
208                }
209            } else {
210                // Modification
211                res.append("Modified " + getDifferenceLines(d, false) + " into " + getDifferenceLines(d, true) + "\n");
212                for (int i = d.getDeletedStart(); i <= d.getDeletedEnd(); i++) {
213                    res.append("< " + startStrings[i] + "\n");
214                }
215                res.append("---\n");
216                for (int i = d.getAddedStart(); i <= d.getAddedEnd(); i++) {
217                    res.append("> " + endStrings[i] + "\n");
218                }
219            }
220        }
221        return res.toString();
222    }
223
224    private static String getDifferenceLines(Difference d, boolean added) {
225        int startLine;
226        int endLine;
227        if (added) {
228            startLine = d.getAddedStart();
229            endLine = d.getAddedEnd();
230        } else {
231            startLine = d.getDeletedStart();
232            endLine = d.getDeletedEnd();
233        }
234        if (startLine == endLine) {
235            return "line " + (startLine + 1);
236        } else {
237            return "lines " + (startLine + 1) + "-" + (endLine + 1);
238        }
239    }
240
241    /**
242     * Make a temporary directory using File.createTempFile.
243     *
244     * @param prefix
245     * @param suffix
246     * @return a temporary directory using File.createTempFile
247     * @throws IOException
248     */
249    public static File createTempDir(String prefix, String suffix) throws IOException {
250        File temp = File.createTempFile(prefix, suffix);
251        temp.delete();
252        temp.mkdir();
253        return temp;
254    }
255
256    /**
257     * Make a temporary directory using File.createTempFile.
258     *
259     * @param prefix
260     * @param suffix
261     * @param directory
262     * @return a temporary directory using File.createTempFile
263     * @throws IOException
264     */
265    public static File createTempDir(String prefix, String suffix, File directory) throws IOException {
266        File temp = File.createTempFile(prefix, suffix, directory);
267        temp.delete();
268        temp.mkdir();
269        return temp;
270    }
271
272    /**
273     * Find files recursively that match the given filter.
274     *
275     * @param start The directory (or file) to start at.
276     * @param filter Filter of files to include. All files (including directories) are passed to this filter and are
277     * included if filter.accept() returns true. Subdirectories are scanned whether or not filter.accept() returns true
278     * for them.
279     * @return List of files (in no particular order) that match the filter. and reside under start.
280     */
281    public static List<File> findFiles(File start, FileFilter filter) {
282        List<File> results = new ArrayList<File>();
283        File[] filesThisLevel = start.listFiles();
284        for (File f : filesThisLevel) {
285            if (filter.accept(f)) {
286                results.add(f);
287            }
288            if (f.isDirectory()) {
289                results.addAll(findFiles(f, filter));
290            }
291        }
292        return results;
293    }
294
295}