EMMA Coverage Report (generated Sat Oct 08 11:41:37 CEST 2011)
[all classes][net.sf.jomic.tools]

COVERAGE SUMMARY FOR SOURCE FILE [FileTools.java]

nameclass, %method, %block, %line, %
FileTools.java100% (1/1)90%  (44/49)74%  (1697/2308)82%  (385.8/471)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class FileTools100% (1/1)90%  (44/49)74%  (1697/2308)82%  (385.8/471)
copyAndClose (InputStream, OutputStream): void 0%   (0/1)0%   (0/83)0%   (0/18)
createCopyDirTasks (File, File): CopyFileTask [] 0%   (0/1)0%   (0/94)0%   (0/18)
getAcceptableSuffixText (): String 0%   (0/1)0%   (0/3)0%   (0/1)
getImageResourceAsStream (String): InputStream 0%   (0/1)0%   (0/36)0%   (0/6)
isValidSortMode (String): boolean 0%   (0/1)0%   (0/20)0%   (0/2)
ifNullThrowCannotFindImageResouce (Object, String): void 100% (1/1)21%  (3/14)50%  (2/4)
mkdirs (File): boolean 100% (1/1)42%  (8/19)60%  (3/5)
deleteOrSkip (File, Log): String 100% (1/1)44%  (32/73)54%  (7/13)
getRelativePath (File, File): String 100% (1/1)49%  (21/43)87%  (4.4/5)
deleteOrWarn (File, Log): String 100% (1/1)51%  (37/73)69%  (9/13)
sort (String [], String): String [] 100% (1/1)63%  (34/54)78%  (7/9)
isIn (Map, String): boolean 100% (1/1)64%  (21/33)71%  (3.6/5)
copyDir (File, File): void 100% (1/1)66%  (63/95)83%  (15.7/19)
getIconFor (File): Icon 100% (1/1)69%  (9/13)75%  (1.5/2)
getSuffix (File): String 100% (1/1)69%  (9/13)75%  (1.5/2)
isPdf (String): boolean 100% (1/1)71%  (10/14)75%  (1.5/2)
isRar (String): boolean 100% (1/1)71%  (10/14)75%  (1.5/2)
isZip (String): boolean 100% (1/1)71%  (10/14)75%  (1.5/2)
putAll (Map, String []): void 100% (1/1)72%  (31/43)79%  (5.6/7)
copy (InputStream, OutputStream): void 100% (1/1)74%  (23/31)88%  (7/8)
getHomeDir (): File 100% (1/1)75%  (12/16)88%  (2.6/3)
attemptToDeleteAll (File, Log): void 100% (1/1)81%  (34/42)88%  (7/8)
createTempDir (String): File 100% (1/1)81%  (17/21)90%  (4.5/5)
delete (File): void 100% (1/1)83%  (19/23)90%  (4.5/5)
isComic (String): boolean 100% (1/1)83%  (20/24)75%  (1.5/2)
addFiles (List, File, FileFilter): void 100% (1/1)84%  (61/73)89%  (13.3/15)
getAdjustedComicFile (File): File 100% (1/1)84%  (62/74)92%  (12/13)
sortSmart (String []): String [] 100% (1/1)85%  (330/387)96%  (85.5/89)
copyFile (File, File): void 100% (1/1)86%  (48/56)90%  (9/10)
writeLines (File, String []): void 100% (1/1)86%  (31/36)92%  (7.4/8)
getSuffix (String): String 100% (1/1)88%  (30/34)93%  (6.5/7)
getImageResource (String): URL 100% (1/1)89%  (32/36)92%  (5.5/6)
getSize (File): long 100% (1/1)91%  (40/44)95%  (9.5/10)
getWithoutLastSuffix (String): String 100% (1/1)92%  (46/50)94%  (8.5/9)
getPortableFileName (String): String 100% (1/1)92%  (47/51)96%  (11.5/12)
<static initializer> 100% (1/1)95%  (53/56)99%  (6.9/7)
FileTools (): void 100% (1/1)96%  (121/126)100% (25.9/26)
obtainComicFormat (File): String 100% (1/1)96%  (109/113)98%  (25.5/26)
addToValueList (Map, Object, Object): void 100% (1/1)100% (21/21)100% (6/6)
getFlattenedFolderNames (Map): Map 100% (1/1)100% (75/75)100% (21/21)
getFolderMap (File []): Map 100% (1/1)100% (37/37)100% (10/10)
getFolderMap (File): Map 100% (1/1)100% (5/5)100% (1/1)
getFolderMap (File, FileFilter): Map 100% (1/1)100% (9/9)100% (2/2)
getFolderNames (Map): Map 100% (1/1)100% (31/31)100% (9/9)
getRelativePaths (File, File []): String [] 100% (1/1)100% (23/23)100% (4/4)
instance (): FileTools 100% (1/1)100% (8/8)100% (3/3)
listFilesRecursively (File): File [] 100% (1/1)100% (5/5)100% (1/1)
listFilesRecursively (File, FileFilter): File [] 100% (1/1)100% (16/16)100% (3/3)
setPortableChars (): void 100% (1/1)100% (34/34)100% (7/7)

1// Jomic - a viewer for comic book archives.
2// Copyright (C) 2004-2011 Thomas Aglassinger
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16package net.sf.jomic.tools;
17 
18import java.io.BufferedWriter;
19import java.io.File;
20import java.io.FileFilter;
21import java.io.FileInputStream;
22import java.io.FileNotFoundException;
23import java.io.FileWriter;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.OutputStream;
27import java.net.URL;
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.Collections;
31import java.util.Comparator;
32import java.util.Iterator;
33import java.util.LinkedList;
34import java.util.List;
35import java.util.Map;
36import java.util.SortedMap;
37import java.util.TreeMap;
38 
39import javax.swing.Icon;
40import javax.swing.JFileChooser;
41 
42import org.apache.commons.logging.Log;
43import org.apache.commons.logging.LogFactory;
44 
45/**
46 *  Utility methods to deal with comic files.
47 *
48 * @author    Thomas Aglassinger
49 */
50public final class FileTools
51{
52    public static final String DELETED = "deleted";
53    public static final String DELETE_DID_NOT_EXIST = "didNotExist";
54    public static final String DELETE_FAILED = "failed";
55    public static final String FORMAT_PDF = "pdf";
56    public static final String FORMAT_RAR = "rar";
57    public static final String FORMAT_ZIP = "zip";
58 
59    /**
60     *  Value used as folder for files that do not have any folder in their path (for example,
61     *  "hugo.png").
62     *
63     * @see    #getFolderMap(File[])
64     */
65    public static final String NO_FOLDER_NAME = "";
66 
67    /**
68     *  Sort mode using natural sort order.
69     *
70     * @see    NaturalCaseInsensitiveOrderComparator
71     */
72    public static final String SORT_NATURAL = "natural";
73 
74    /**
75     *  Sort mode trying to figure out if names are numbered using a messed up naming schema.
76     */
77    public static final String SORT_SMART = "smart";
78 
79    static final String[] RAR_SUFFIXES = new String[]{"cbp", "cbr", "rar"};
80    static final String[] ZIP_SUFFIXES = new String[]{"cbz", "zip"};
81    private static final int BUFFER_SIZE = 4096;
82 
83    /**
84     *  Magic bytes to identify a PDF archive.
85     */
86    private static final String MAGIC_PDF = "%PDF";
87    private static final int MAGIC_PDF_LENGTH = MAGIC_PDF.length();
88 
89    /**
90     *  Magic bytes to identify a RAR archive.
91     */
92    private static final String MAGIC_RAR = "Rar!";
93    private static final int MAGIC_RAR_LENGTH = MAGIC_RAR.length();
94 
95    /**
96     *  Magic bytes to identify a ZIP archive.
97     */
98    private static final String MAGIC_ZIP = "PK";
99    private static final int MAGIC_ZIP_LENGTH = MAGIC_ZIP.length();
100 
101    // HACK: Stupid name to prevent pretty printer from sorting it in front of undefined symbols
102    // (see sf.net [bug 971615]: pretty printer alphabetically sorts interdependant constants).
103    private static final int MAGIC_ZZZ_MAX_LENGTH =
104            Math.max(MAGIC_PDF_LENGTH, Math.max(MAGIC_RAR_LENGTH, MAGIC_ZIP_LENGTH));
105    private static FileTools instance;
106    private String acceptableSuffixText;
107    private Map allSuffixes;
108    private JFileChooser iconChooser;
109    private LocaleTools localeTools;
110    private Log logger;
111    private char[] portableChars;
112    private Map preferredSuffixes;
113    private Map rarSuffixes;
114    private StringTools stringTools;
115    private Map zipSuffixes;
116 
117    private FileTools() {
118        logger = LogFactory.getLog(FileTools.class);
119        rarSuffixes = new TreeMap();
120        zipSuffixes = new TreeMap();
121        allSuffixes = new TreeMap();
122        preferredSuffixes = new TreeMap();
123        localeTools = LocaleTools.instance();
124        stringTools = StringTools.instance();
125 
126        setPortableChars();
127        putAll(rarSuffixes, RAR_SUFFIXES);
128        putAll(allSuffixes, RAR_SUFFIXES);
129        putAll(zipSuffixes, ZIP_SUFFIXES);
130        putAll(allSuffixes, ZIP_SUFFIXES);
131        preferredSuffixes.put(FORMAT_PDF, "pdf");
132        preferredSuffixes.put(FORMAT_RAR, "cbr");
133        preferredSuffixes.put(FORMAT_ZIP, "cbz");
134 
135        acceptableSuffixText = "";
136 
137        Iterator rider = allSuffixes.keySet().iterator();
138 
139        while (rider.hasNext()) {
140            String suffix = (String) rider.next();
141 
142            acceptableSuffixText += suffix;
143            if (rider.hasNext()) {
144                acceptableSuffixText += ", ";
145            }
146        }
147 
148        iconChooser = new JFileChooser();
149    }
150 
151 
152    /**
153     *  Ensures that portableChars contains only chars conforming to the POSIX recommendation.
154     */
155    private void setPortableChars() {
156        StringBuffer all = new StringBuffer("-_.0123456789");
157 
158        for (char c = 'a'; c <= 'z'; c += 1) {
159            all.append(c);
160            all.append(Character.toUpperCase(c));
161        }
162        portableChars = all.toString().toCharArray();
163        Arrays.sort(portableChars);
164    }
165 
166    public String getAcceptableSuffixText() {
167        return acceptableSuffixText;
168    }
169 
170    public File getAdjustedComicFile(File comicFile)
171        throws IOException {
172        assert comicFile != null;
173        File result;
174        String suffix = getSuffix(comicFile).toLowerCase();
175        FileArchive comicArchive = new FileArchive(comicFile);
176        String fileType = comicArchive.getFileType();
177        String preferredSuffix = (String) preferredSuffixes.get(fileType);
178 
179        assert preferredSuffix != null;
180        assert preferredSuffix.toLowerCase().equals(preferredSuffix);
181 
182        if (!suffix.equals(preferredSuffix)) {
183            String comicFileWithoutSuffix = getWithoutLastSuffix(comicFile.getAbsolutePath());
184 
185            result = new File(comicFileWithoutSuffix + "." + preferredSuffix);
186        } else {
187            result = comicFile;
188        }
189        return result;
190    }
191 
192    public Map getFlattenedFolderNames(Map folderNames) {
193        Map result = new TreeMap();
194        NaturalCaseInsensitiveOrderComparator naturalComparator = new NaturalCaseInsensitiveOrderComparator();
195        List names = new ArrayList(folderNames.keySet());
196        Iterator nameRider;
197 
198        Collections.sort(names, naturalComparator);
199        nameRider = names.iterator();
200        while (nameRider.hasNext()) {
201            String folderName = (String) nameRider.next();
202            String uniqueName = folderName;
203            List folders = (List) folderNames.get(folderName);
204            int uniqueIndex = 1;
205 
206            Collections.sort(folders, naturalComparator);
207 
208            Iterator folderRider = folders.iterator();
209 
210            while (folderRider.hasNext()) {
211                File folder = (File) folderRider.next();
212 
213                while (result.containsKey(uniqueName)) {
214                    // TODO: Use leading zeros before uniqueIndex.
215                    uniqueName = folderName + "_" + uniqueIndex;
216                    uniqueIndex += 1;
217                }
218                result.put(uniqueName, folder);
219            }
220        }
221        return result;
222    }
223 
224    /**
225     *  Get a map with all non-empty sub folders in <code>folder</code> as keys and a <code>List</code>
226     *  of all files in it as values.
227     */
228    public Map getFolderMap(File folder) {
229        return getFolderMap(folder, null);
230    }
231 
232    /**
233     *  Get a map with all non-empty sub folders in <code>folder</code> as keys and a <code>List</code>
234     *  of all files in it accepted by <code>filter</code> as values.
235     */
236    public Map getFolderMap(File folder, FileFilter filter) {
237        File[] files = listFilesRecursively(folder, filter);
238 
239        return getFolderMap(files);
240    }
241 
242    /**
243     *  Get a map where the keys are distinct folders in <code>files</code> and the values are the
244     *  respective files found in each folder.
245     */
246    public Map getFolderMap(File[] files) {
247        Map result = new TreeMap();
248        NaturalCaseInsensitiveOrderComparator naturalCaselessComparator = new NaturalCaseInsensitiveOrderComparator();
249 
250        Arrays.sort(files, naturalCaselessComparator);
251        for (int i = 0; i < files.length; i += 1) {
252            File file = files[i];
253            String parent = file.getParent();
254 
255            if (parent == null) {
256                // Parent will be null for files without folder, for example File("hugo.png").
257                parent = NO_FOLDER_NAME;
258            }
259            addToValueList(result, parent, file);
260        }
261        return result;
262    }
263 
264    /**
265     *  Get a map where the keys are all distinct folder names (without parent path) in <code>folderMap</code>
266     *  and the value is a list of all folders having this name.
267     *
268     * @see    #getFolderMap(File)
269     */
270    public Map getFolderNames(Map folderMap) {
271        Map result = new TreeMap();
272        Iterator folderRider = folderMap.keySet().iterator();
273 
274        while (folderRider.hasNext()) {
275            String folderPath = (String) folderRider.next();
276            File folder = new File(folderPath);
277            String folderName = folder.getName();
278 
279            addToValueList(result, folderName, folder);
280        }
281        return result;
282    }
283 
284    /**
285     *  Get the user's home directory.
286     */
287    public File getHomeDir() {
288        String homeText = System.getProperty("user.home");
289 
290        assert homeText != null;
291        return new File(homeText);
292    }
293 
294    /**
295     *  Get icon for <code>file</code>.
296     *
297     * @see    javax.swing.filechooser.FileView#getIcon(java.io.File)
298     */
299    public Icon getIconFor(File file) {
300        assert file != null;
301        return iconChooser.getIcon(file);
302    }
303 
304    /**
305     *  Get an image URL from net.sf.jomic.images.
306     *
307     * @throws  IllegalStateException  if the image cannot be found
308     */
309    public URL getImageResource(String name) {
310        assert name != null;
311        String fullName = "images/" + name;
312        Class loaderClass = net.sf.jomic.Jomic.class;
313        URL result = loaderClass.getResource(fullName);
314 
315        ifNullThrowCannotFindImageResouce(result, fullName);
316        return result;
317    }
318 
319    /**
320     *  Get an image InputStream from net.sf.jomic.images.
321     *
322     * @throws  IllegalStateException  if the image cannot be found
323     */
324    public InputStream getImageResourceAsStream(String name) {
325        assert name != null;
326        String fullName = "images/" + name;
327        Class loaderClass = net.sf.jomic.Jomic.class;
328        InputStream result = loaderClass.getResourceAsStream(fullName);
329 
330        ifNullThrowCannotFindImageResouce(result, fullName);
331        return result;
332    }
333 
334    /**
335     *  Gets a portable version of <code>suspiciousFileName</code> contains only chars conforming to
336     *  the POSIX recommendation on portable file names. A file name returned by this function is
337     *  guaranteed to be creatable on any common file system. This is achieved by replacing all
338     *  troublesome characters with a hyphen (-). To make the result look less silly, avoid multiple
339     *  hyphens in a row.
340     */
341    public String getPortableFileName(String suspiciousFileName) {
342        assert suspiciousFileName != null;
343        int length = suspiciousFileName.length();
344        char previousCh = 0;
345        StringBuffer result = new StringBuffer(length);
346 
347        for (int i = 0; i < length; i += 1) {
348            char c = suspiciousFileName.charAt(i);
349 
350            if (Arrays.binarySearch(portableChars, c) < 0) {
351                c = '-';
352            }
353            if ((previousCh != '-') || (c != '-')) {
354                result.append(c);
355            }
356            previousCh = c;
357        }
358        return result.toString();
359    }
360 
361    public String getRelativePath(File baseDir, File fileInBaseDir) {
362        String basePath = baseDir.getAbsolutePath();
363        String filePath = fileInBaseDir.getAbsolutePath();
364 
365        assert filePath.startsWith(basePath)
366                : "file " + stringTools.sourced(filePath) + " must start with " + stringTools.sourced(baseDir);
367        String result = filePath.substring(basePath.length() + 1);
368 
369        return result;
370    }
371 
372    public String[] getRelativePaths(File baseDir, File[] filesInBaseDir) {
373        String[] result = new String[filesInBaseDir.length];
374 
375        for (int i = 0; i < filesInBaseDir.length; i += 1) {
376            result[i] = getRelativePath(baseDir, filesInBaseDir[i]);
377        }
378        return result;
379    }
380 
381    /**
382     *  Recursively get size of all files and directories in <code>fileOrDir</code>.
383     */
384    public long getSize(File fileOrDir) {
385        assert fileOrDir != null;
386        long result = 0;
387        File[] items = fileOrDir.listFiles();
388 
389        if (items != null) {
390            for (int i = 0; i < items.length; i += 1) {
391                File item = items[i];
392 
393                if (item.isDirectory()) {
394                    result += getSize(item);
395                } else {
396                    result += item.length();
397                }
398            }
399        }
400        return result;
401    }
402 
403    /**
404     *  Get the (lower case) last suffix of name (without the "."), for example: "hugo.tar.gz"
405     *  yields "gz".
406     */
407    public String getSuffix(File file) {
408        assert file != null;
409        return getSuffix(file.getName());
410    }
411 
412    /**
413     *  Get the (lower case) last suffix of name (without the "."), for example: "hugo.tar.gz"
414     *  yields "gz".
415     */
416    public String getSuffix(String name) {
417        assert name != null;
418        String result;
419        int lastDotIndex = name.lastIndexOf('.');
420        int lastSeparatorIndex = name.lastIndexOf(File.separator);
421 
422        if ((lastDotIndex < lastSeparatorIndex) || (lastDotIndex == -1)) {
423            result = "";
424        } else {
425            result = name.substring(lastDotIndex + 1).toLowerCase();
426        }
427        return result;
428    }
429 
430    public String getWithoutLastSuffix(String fileName) {
431        assert fileName != null;
432        String result;
433        String suffix = getSuffix(fileName);
434        int length = fileName.length();
435 
436        if (suffix.length() == 0) {
437            if ((length > 0) && (fileName.charAt(length - 1) == '.')) {
438                result = fileName.substring(0, length - 1);
439            } else {
440                result = fileName;
441            }
442        } else {
443            result = fileName.substring(0, length - suffix.length() - 1);
444        }
445        return result;
446    }
447 
448    public boolean isComic(String name) {
449        assert name != null;
450        return isPdf(name) || isRar(name) || isZip(name);
451    }
452 
453    /**
454     *  Does name indicate a PDF file?
455     */
456    public boolean isPdf(String name) {
457        assert name != null;
458        return getSuffix(name).equals("pdf");
459    }
460 
461    /**
462     *  Does name indicate a rar compressed archive?
463     */
464    public boolean isRar(String name) {
465        assert name != null;
466        return isIn(rarSuffixes, name);
467    }
468 
469    /**
470     *  Is <code>mode</code> a valid value for property sortMode?
471     */
472    public boolean isValidSortMode(String mode) {
473        assert mode != null;
474        return mode.equals(SORT_NATURAL)
475                || mode.equals(SORT_SMART);
476    }
477 
478    /**
479     *  Does name indicate a zip compressed archive?
480     */
481    public boolean isZip(String name) {
482        assert name != null;
483        return isIn(zipSuffixes, name);
484    }
485 
486    /**
487     *  Does the suffix map contain the suffix of name?
488     */
489    private boolean isIn(Map suffixes, String name) {
490        assert name != null;
491        assert suffixes != null;
492        assert suffixes.size() > 0;
493        String suffix = getSuffix(name);
494 
495        return suffixes.containsKey(suffix);
496    }
497 
498    public static synchronized FileTools instance() {
499        if (instance == null) {
500            instance = new FileTools();
501        }
502        return instance;
503    }
504 
505    /**
506     *  Attempts to recursively delete all files in <code>dir</code>. Every file or directory that
507     *  cannot be deleted causes a warning in the log.
508     */
509    public void attemptToDeleteAll(File dir, Log deleteLogger) {
510        assert dir != null;
511        assert deleteLogger != null;
512        if (dir.isDirectory()) {
513            File[] files = dir.listFiles();
514 
515            for (int i = 0; i < files.length; i += 1) {
516                attemptToDeleteAll(files[i], deleteLogger);
517            }
518        }
519        deleteOrWarn(dir, deleteLogger);
520    }
521 
522    /**
523     *  Copy all data from <code>in</code> to <code>out</code>.
524     */
525    public void copy(InputStream in, OutputStream out)
526        throws IOException {
527        assert in != null;
528        assert out != null;
529 
530        byte[] buffer = new byte[BUFFER_SIZE];
531        int bytesRead;
532 
533        do {
534            bytesRead = in.read(buffer);
535            if (bytesRead > 0) {
536                out.write(buffer);
537            }
538        } while (bytesRead > 0);
539    }
540 
541    /**
542     *  Copy all data from <code>in</code> to <code>out</code>, and close both streams.
543     */
544    public void copyAndClose(InputStream in, OutputStream out)
545        throws IOException {
546        assert in != null;
547        assert out != null;
548 
549        try {
550            copy(in, out);
551        } finally {
552            Exception cause = null;
553 
554            try {
555                out.close();
556            } catch (Exception error) {
557                cause = error;
558            }
559            try {
560                in.close();
561            } catch (Exception error) {
562                if (cause == null) {
563                    cause = error;
564                }
565            }
566            if (cause != null) {
567                String message = localeTools.getMessage("errors.cannotCopyStream");
568 
569                throw new IOExceptionWithCause(message, cause);
570            }
571        }
572    }
573 
574    /**
575     *  Recursively copy all files and directories in <code>sourceDir</code> to <code>targetDir</code>
576     *  .
577     *
578     * @throws  IOException
579     */
580    public void copyDir(File sourceDir, File targetDir)
581        throws IOException {
582        // TODO: Use CopyDirTask, add optional parameter for ProgressChangeListener.
583        assert sourceDir != null;
584        assert sourceDir.isDirectory();
585        assert targetDir != null;
586        assert targetDir.isDirectory();
587        boolean allCopied = false;
588        File[] contents = sourceDir.listFiles();
589        Iterator rider = Arrays.asList(contents).iterator();
590 
591        try {
592            while (rider.hasNext()) {
593                File item = (File) rider.next();
594                File target = new File(targetDir, item.getName());
595 
596                if (item.isDirectory()) {
597                    mkdirs(target);
598                    copyDir(item, target);
599                } else {
600                    copyFile(item, target);
601                }
602            }
603            allCopied = true;
604        } finally {
605            if (!allCopied) {
606                attemptToDeleteAll(targetDir, logger);
607            }
608        }
609    }
610 
611    /**
612     *  Copy file <code>source</code> to <code>target</code>.
613     */
614    public void copyFile(File source, File target)
615        throws IOException {
616        assert source != null;
617        assert target != null;
618 
619        Task copyTask = new CopyFileTask(source, target);
620 
621        try {
622            copyTask.start();
623        } catch (Exception error) {
624            String[] filePaths = new String[]{stringTools.sourced(source), stringTools.sourced(target)};
625            String errorMessage = localeTools.getMessage("errors.cannotCopyFile", filePaths);
626 
627            throw new IOExceptionWithCause(errorMessage, error);
628        }
629    }
630 
631    public CopyFileTask[] createCopyDirTasks(File sourceDir, File targetDir) {
632        assert sourceDir != null;
633        assert sourceDir.isDirectory();
634        assert targetDir != null;
635        assert targetDir.isDirectory();
636        List result = new LinkedList();
637        File[] contents = sourceDir.listFiles();
638        Iterator rider = Arrays.asList(contents).iterator();
639 
640        while (rider.hasNext()) {
641            File item = (File) rider.next();
642            File target = new File(targetDir, getRelativePath(sourceDir, item));
643 
644            if (item.isDirectory()) {
645                CopyFileTask[] subTasks = createCopyDirTasks(item, target);
646 
647                result.addAll(Arrays.asList(subTasks));
648            } else {
649                CopyFileTask copyFileTask = new CopyFileTask(item, target, true);
650 
651                result.add(copyFileTask);
652            }
653        }
654 
655        return (CopyFileTask[]) result.toArray(new CopyFileTask[0]);
656    }
657 
658    /**
659     *  Create a temporary directory (to dump a bundle of temporary files in it later. To remove it
660     *  when done, use <code>attemptToDeleteAll</code> or something similar.
661     *
662     * @see    File#createTempFile(java.lang.String, java.lang.String)
663     * @see    #attemptToDeleteAll(File, Logger)
664     */
665    public File createTempDir(String prefix)
666        throws IOException {
667        assert prefix != null;
668        File result = File.createTempFile(prefix, ".tmp");
669 
670        delete(result);
671        mkdirs(result);
672        return result;
673    }
674 
675    /**
676     *  Same as File.delete(), but throws an IOException if the file can not be deleted.
677     */
678    public void delete(File file)
679        throws IOException {
680        assert file != null;
681        if (!file.delete()) {
682            String message = localeTools.getMessage("errors.cannotDeleteFile", file);
683 
684            throw new IOException(message);
685        }
686    }
687 
688    /**
689     *  Attempt to delete <code>file</code> provided it exists. If a deletion fails, log a warning
690     *  to <code>deleteLogger</code> .
691     *
692     * @return    DELETED, DELETE_FAILED, or DELETE_DID_NOT_EXIST
693     */
694    public String deleteOrSkip(File file, Log deleteLogger) {
695        assert file != null;
696        assert deleteLogger != null;
697        String result;
698        boolean deleted = file.delete();
699 
700        if (deleted) {
701            result = DELETED;
702            if (deleteLogger.isDebugEnabled()) {
703                deleteLogger.debug("deleted \"" + file + "\"");
704            }
705        } else if (file.exists()) {
706            deleteLogger.warn("cannot delete \"" + file + "\"");
707            result = DELETE_FAILED;
708        } else {
709            deleteLogger.debug("nothing to delete for \"" + file + "\"");
710            result = DELETE_DID_NOT_EXIST;
711        }
712        return result;
713    }
714 
715    /**
716     *  Attempt to delete <code>file</code>. If this fails, log a warning to <code>deleteLogger</code>
717     *  .
718     *
719     * @return    DELETED, DELETE_FAILED, or DELETE_DID_NOT_EXIST
720     */
721    public String deleteOrWarn(File file, Log deleteLogger) {
722        assert file != null;
723        assert deleteLogger != null;
724        String result;
725        boolean deleted = file.delete();
726 
727        if (deleted) {
728            result = DELETED;
729            if (deleteLogger.isDebugEnabled()) {
730                deleteLogger.debug("deleted \"" + file + "\"");
731            }
732        } else if (file.exists()) {
733            deleteLogger.warn("cannot delete \"" + file + "\"");
734            result = DELETE_FAILED;
735        } else {
736            deleteLogger.warn("cannot delete non-existent \"" + file + "\"");
737            result = DELETE_DID_NOT_EXIST;
738        }
739        return result;
740    }
741 
742    /**
743     *  Obtain a recursive list of files in <code>dir</code>.
744     */
745    public File[] listFilesRecursively(File dir) {
746        return listFilesRecursively(dir, null);
747    }
748 
749    /**
750     *  Obtain a recursive list of files in <code>dir</code> using <code>filter</code> to decide
751     *  which files to include in the result.
752     */
753    public File[] listFilesRecursively(File dir, FileFilter filter) {
754        List result = new LinkedList();
755 
756        addFiles(result, dir, filter);
757 
758        return (File[]) result.toArray(new File[0]);
759    }
760 
761    /**
762     *  Same as <code>File.mkdirs()</code> but throws an <code>IOException</code> if the directory
763     *  does not yet exist and also cannot be created.
764     *
765     * @see    File#mkdirs()
766     */
767    public boolean mkdirs(File dir)
768        throws FileNotFoundException {
769        boolean result = dir.mkdirs();
770 
771        if (!dir.exists()) {
772            String message = localeTools.getMessage("errors.cannotCreateDirectory", dir);
773 
774            throw new FileNotFoundException(message);
775        }
776        return result;
777    }
778 
779    /**
780     *  Figure out file type by looking at magic bytes.
781     *
782     * @throws  IOException  in case the format cannot be determined
783     */
784    public String obtainComicFormat(File comicFile)
785        throws IOException {
786        assert comicFile != null;
787        InputStream in = new FileInputStream(comicFile);
788        String result = null;
789 
790        try {
791            byte[] magicBytes = new byte[MAGIC_ZZZ_MAX_LENGTH];
792            String fullMagicText;
793 
794            in.read(magicBytes, 0, MAGIC_ZZZ_MAX_LENGTH);
795            // TODO: Use CharsetDecoder instead of String constructor to avoid unspecified
796            // behavior on decoding errors.
797            fullMagicText = new String(magicBytes, "US-ASCII");
798 
799            int fullMagicTextLength = fullMagicText.length();
800 
801            if (fullMagicTextLength >= MAGIC_RAR_LENGTH) {
802                String magicText = fullMagicText.substring(0, MAGIC_RAR_LENGTH);
803 
804                // TODO: Find out if magic bytes are exactly "Rar!", and use equals().
805                if (magicText.equalsIgnoreCase(MAGIC_RAR)) {
806                    result = FORMAT_RAR;
807                }
808            }
809            if ((result == null) && (fullMagicTextLength >= MAGIC_ZIP_LENGTH)) {
810                String magicText = fullMagicText.substring(0, MAGIC_ZIP_LENGTH);
811 
812                if (magicText.equals(MAGIC_ZIP)) {
813                    result = FORMAT_ZIP;
814                }
815            }
816            if ((result == null) && (fullMagicTextLength >= MAGIC_PDF_LENGTH)) {
817                String magicText = fullMagicText.substring(0, MAGIC_PDF_LENGTH);
818 
819                if (magicText.equals(MAGIC_PDF)) {
820                    result = FORMAT_PDF;
821                }
822            }
823            if (result == null) {
824                Object[] options = new Object[]{comicFile, fullMagicText};
825                String errorMessage = localeTools.getMessage("errors.cannotDetermineComicFileType", options);
826 
827                throw new IOException(errorMessage);
828            }
829        } finally {
830            in.close();
831        }
832        return result;
833    }
834 
835    /**
836     *  Sort <code>filePaths</code> according to <code>mode</code>.
837     *
838     * @param  mode  one of: SORT_NATURAL, SORT_SMART
839     */
840    public String[] sort(String[] filePaths, String mode) {
841        assert filePaths != null;
842        assert mode != null;
843        String[] result;
844 
845        if (mode.equals(SORT_NATURAL)) {
846            result = (String[]) filePaths.clone();
847            Arrays.sort(result, new NaturalCaseInsensitiveOrderComparator());
848        } else if (mode.equals(SORT_SMART)) {
849            result = sortSmart(filePaths);
850        } else {
851            throw new IllegalArgumentException("mode=" + mode);
852        }
853        return result;
854    }
855 
856    public void writeLines(File targetFile, String[] lines)
857        throws IOException {
858        BufferedWriter writer = new BufferedWriter(new FileWriter(targetFile));
859        String lineSeparator = System.getProperty("line.separator");
860 
861        try {
862            for (int i = 0; i < lines.length; i += 1) {
863                writer.write(lines[i]);
864                writer.write(lineSeparator);
865            }
866        } finally {
867            writer.close();
868        }
869    }
870 
871    private void addFiles(List files, File dirToScanForFiles, FileFilter filter) {
872        assert files != null;
873        assert dirToScanForFiles != null;
874        File[] filesFound = dirToScanForFiles.listFiles();
875 
876        if (filesFound != null) {
877            for (int i = 0; i < filesFound.length; i += 1) {
878                File nextFile = filesFound[i];
879 
880                if (nextFile.isDirectory()) {
881                    addFiles(files, nextFile, filter);
882                } else if ((filter == null) || filter.accept(nextFile)) {
883                    files.add(nextFile);
884                }
885            }
886        } else {
887            String folderNotFound = dirToScanForFiles.getPath();
888            String errorMessage = localeTools.getMessage("errors.cannotAccessFolder", folderNotFound);
889            FileNotFoundException folderNotFoundError = new FileNotFoundException(errorMessage);
890 
891            throw new TunneledIOException(folderNotFoundError);
892        }
893    }
894 
895    /**
896     *  Add <code>value</code> to a <code>List</code> that can be accessed via <code>map</code>
897     *  using <code>key</code>. If there is no list for the key yet, create it and add it.
898     */
899    private void addToValueList(Map map, Object key, Object value) {
900        List nameList = (List) map.get(key);
901 
902        if (nameList == null) {
903            nameList = new ArrayList();
904            map.put(key, nameList);
905        }
906        nameList.add(value);
907    }
908 
909    private void ifNullThrowCannotFindImageResouce(Object result, String fullName) {
910        if (result == null) {
911            String message = localeTools.getMessage("errors.cannotFindImageResource", fullName);
912 
913            throw new IllegalStateException(message);
914        }
915    }
916 
917    private void putAll(Map map, String[] items) {
918        assert map != null;
919        assert items != null;
920        assert items.length > 0;
921        for (int i = 0; i < items.length; i += 1) {
922            String item = items[i];
923 
924            map.put(item, item);
925        }
926    }
927 
928    /**
929     *  Sort images specified by <code>filePaths</code>. This uses natural sort and also takes
930     *  "messed up" numbering of pages in account (for instance using "page-1617.png" instead of the
931     *  proper "page-16+17.png").
932     *
933     * @see    NumberedNameComparator
934     * @see    NaturalCaseInsensitiveOrderComparator
935     */
936    private String[] sortSmart(String[] filePaths) {
937        String[] result;
938        Comparator naturalComparator = new NaturalCaseInsensitiveOrderComparator();
939        SortedMap pathMap = new TreeMap(naturalComparator);
940 
941        // Build a "directory tree" with the following hierarchy: path/prefix/NumberedName
942        for (int i = 0; i < filePaths.length; i += 1) {
943            NumberedName numberedName = new NumberedName(filePaths[i]);
944            String path = numberedName.getPath();
945            SortedMap prefixMap = (SortedMap) pathMap.get(path);
946 
947            if (prefixMap == null) {
948                prefixMap = new TreeMap(naturalComparator);
949                pathMap.put(path, prefixMap);
950            }
951            String prefix = numberedName.getPrefix();
952            List nameList = (List) prefixMap.get(prefix);
953 
954            if (nameList == null) {
955                nameList = new LinkedList();
956                prefixMap.put(prefix, nameList);
957            }
958            nameList.add(numberedName);
959        }
960 
961        List resultList = new LinkedList();
962        List pageLessNames = new LinkedList();
963        Iterator pathRider = pathMap.entrySet().iterator();
964 
965        while (pathRider.hasNext()) {
966            // Build a list of prefixes that have to be demoronized
967            List moronicPrefixList = new LinkedList();
968            Map.Entry entry = (Map.Entry) pathRider.next();
969            String path = (String) entry.getKey();
970            Map prefixMap = (Map) entry.getValue();
971            Iterator prefixRider = prefixMap.entrySet().iterator();
972 
973            while (prefixRider.hasNext()) {
974                Map.Entry prefixEntry = (Map.Entry) prefixRider.next();
975                String prefix = (String) prefixEntry.getKey();
976                List nameList = (List) prefixEntry.getValue();
977                boolean allNamesAreDemoronizable = true;
978                Iterator nameRider = nameList.iterator();
979 
980                while (nameRider.hasNext() && allNamesAreDemoronizable) {
981                    NumberedName name = (NumberedName) nameRider.next();
982 
983                    if (!name.usesPotentiallyMoronicNumbering()) {
984                        allNamesAreDemoronizable = false;
985                    }
986                }
987                if (allNamesAreDemoronizable) {
988                    moronicPrefixList.add(prefix);
989                    if (logger.isInfoEnabled()) {
990                        logger.info(path + prefix + ": demoronizable");
991                    }
992                } else if (logger.isDebugEnabled()) {
993                    logger.debug(path + prefix + ": non-demoronizable");
994                }
995            }
996 
997            if (logger.isInfoEnabled()) {
998                logger.info("prefixes found:");
999                prefixRider = prefixMap.entrySet().iterator();
1000                while (prefixRider.hasNext()) {
1001                    Map.Entry prefixEntry = (Map.Entry) prefixRider.next();
1002                    String prefix = (String) prefixEntry.getKey();
1003 
1004                    logger.info("  " + prefix);
1005                }
1006            }
1007 
1008            // Sort the list for the current paths, possibly demoronizing names.
1009            prefixRider = prefixMap.entrySet().iterator();
1010            while (prefixRider.hasNext()) {
1011                Map.Entry prefixEntry = (Map.Entry) prefixRider.next();
1012                String prefix = (String) prefixEntry.getKey();
1013                List nameList = (List) prefixEntry.getValue();
1014                boolean demoronize = moronicPrefixList.contains(prefix);
1015                Comparator comparator = new NumberedNameComparator(demoronize);
1016 
1017                Collections.sort(nameList, comparator);
1018 
1019                Iterator sortedRider = nameList.iterator();
1020 
1021                while (sortedRider.hasNext()) {
1022                    NumberedName name = (NumberedName) sortedRider.next();
1023                    String fullName = name.getFullName();
1024 
1025                    if (name.getPage().length() > 0) {
1026                        resultList.add(fullName);
1027                    } else {
1028                        pageLessNames.add(fullName);
1029                    }
1030                }
1031            }
1032        }
1033 
1034        // Add the remaining names that have any page number using
1035        // stock natural comparison
1036        Collections.sort(pageLessNames, naturalComparator);
1037 
1038        Iterator pageLessRider = pageLessNames.iterator();
1039        int targetIndex = 0;
1040 
1041        while (pageLessRider.hasNext()) {
1042            String pageLessName = (String) pageLessRider.next();
1043            String pagedNamed = null;
1044            boolean foundGreaterName = false;
1045 
1046            while ((targetIndex < resultList.size() && !foundGreaterName)) {
1047                pagedNamed = (String) resultList.get(targetIndex);
1048                foundGreaterName = (naturalComparator.compare(pageLessName, pagedNamed)) < 0;
1049                if (!foundGreaterName) {
1050                    targetIndex += 1;
1051                }
1052            }
1053            if (logger.isDebugEnabled()) {
1054                logger.debug("inserting pagesless name at " + targetIndex + ": "
1055                        + stringTools.sourced(pageLessName) + " < " + stringTools.sourced(pagedNamed));
1056            }
1057            resultList.add(targetIndex, pageLessName);
1058        }
1059 
1060        result = (String[]) resultList.toArray(new String[0]);
1061 
1062        // Assert that we did not lose an names
1063        int resultLength = result.length;
1064        int filePathsLength = filePaths.length;
1065 
1066        assert resultLength == filePathsLength : "number of names in result and filePaths must be equal: "
1067                + resultLength + " != " + filePathsLength;
1068 
1069        return result;
1070    }
1071 
1072}

[all classes][net.sf.jomic.tools]
EMMA 2.0.4217 (C) Vladimir Roubtsov