EMMA Coverage Report (generated Sun Apr 20 22:38:01 CEST 2008)
[all classes][net.sf.jomic.tools]

COVERAGE SUMMARY FOR SOURCE FILE [FileTools.java]

nameclass, %method, %block, %line, %
FileTools.java100% (1/1)90%  (43/48)77%  (1709/2228)83%  (380.8/457)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class FileTools100% (1/1)90%  (43/48)77%  (1709/2228)83%  (380.8/457)
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)
getRelativePath (File, File): String 100% (1/1)49%  (21/43)87%  (4.4/5)
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)
deleteOrWarn (File, Logger): String 100% (1/1)68%  (50/73)77%  (10/13)
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, Logger): 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)
getSuffix (String): String 100% (1/1)85%  (23/27)92%  (5.5/6)
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)
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)
sortSmart (String []): String [] 100% (1/1)95%  (368/387)98%  (87.5/89)
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-2008 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 net.sf.wraplog.Logger;
43 
44/**
45 *  Utility methods to deal with comic files.
46 *
47 * @author    Thomas Aglassinger
48 */
49public final class FileTools
50{
51    public static final String DELETED = "deleted";
52    public static final String DELETE_DID_NOT_EXIST = "didNotExist";
53    public static final String DELETE_FAILED = "failed";
54    public static final String FORMAT_PDF = "pdf";
55    public static final String FORMAT_RAR = "rar";
56    public static final String FORMAT_ZIP = "zip";
57 
58    /**
59     *  Value used as folder for files that do not have any folder in their path (for example,
60     *  "hugo.png").
61     *
62     * @see    #getFolderMap(File[])
63     */
64    public static final String NO_FOLDER_NAME = "";
65 
66    /**
67     *  Sort mode using natural sort order.
68     *
69     * @see    NaturalCaseInsensitiveOrderComparator
70     */
71    public static final String SORT_NATURAL = "natural";
72 
73    /**
74     *  Sort mode trying to figure out if names are numbered using a moronic naming schema.
75     */
76    public static final String SORT_SMART = "smart";
77 
78    static final String[] RAR_SUFFIXES = new String[]{"cbp", "cbr", "rar"};
79    static final String[] ZIP_SUFFIXES = new String[]{"cbz", "zip"};
80    private static final int BUFFER_SIZE = 4096;
81 
82    /**
83     *  Magic bytes to identify a PDF archive.
84     */
85    private static final String MAGIC_PDF = "%PDF";
86    private static final int MAGIC_PDF_LENGTH = MAGIC_PDF.length();
87 
88    /**
89     *  Magic bytes to identify a RAR archive.
90     */
91    private static final String MAGIC_RAR = "Rar!";
92    private static final int MAGIC_RAR_LENGTH = MAGIC_RAR.length();
93 
94    /**
95     *  Magic bytes to identify a ZIP archive.
96     */
97    private static final String MAGIC_ZIP = "PK";
98    private static final int MAGIC_ZIP_LENGTH = MAGIC_ZIP.length();
99 
100    // HACK: Stupid name to prevent pretty printer from sorting it in front of undefined symbols
101    // (see sf.net [bug 971615]: pretty printer alphabetically sorts interdependant constants).
102    private static final int MAGIC_ZZZ_MAX_LENGTH =
103            Math.max(MAGIC_PDF_LENGTH, Math.max(MAGIC_RAR_LENGTH, MAGIC_ZIP_LENGTH));
104    private static FileTools instance;
105    private String acceptableSuffixText;
106    private Map allSuffixes;
107    private JFileChooser iconChooser;
108    private LocaleTools localeTools;
109    private Logger logger;
110    private char[] portableChars;
111    private Map preferredSuffixes;
112    private Map rarSuffixes;
113    private StringTools stringTools;
114    private Map zipSuffixes;
115 
116    private FileTools() {
117        logger = Logger.getLogger(FileTools.class);
118        rarSuffixes = new TreeMap();
119        zipSuffixes = new TreeMap();
120        allSuffixes = new TreeMap();
121        preferredSuffixes = new TreeMap();
122        localeTools = LocaleTools.instance();
123        stringTools = StringTools.instance();
124 
125        setPortableChars();
126        putAll(rarSuffixes, RAR_SUFFIXES);
127        putAll(allSuffixes, RAR_SUFFIXES);
128        putAll(zipSuffixes, ZIP_SUFFIXES);
129        putAll(allSuffixes, ZIP_SUFFIXES);
130        preferredSuffixes.put(FORMAT_PDF, "pdf");
131        preferredSuffixes.put(FORMAT_RAR, "cbr");
132        preferredSuffixes.put(FORMAT_ZIP, "cbz");
133 
134        acceptableSuffixText = "";
135 
136        Iterator rider = allSuffixes.keySet().iterator();
137 
138        while (rider.hasNext()) {
139            String suffix = (String) rider.next();
140 
141            acceptableSuffixText += suffix;
142            if (rider.hasNext()) {
143                acceptableSuffixText += ", ";
144            }
145        }
146 
147        iconChooser = new JFileChooser();
148    }
149 
150 
151    /**
152     *  Ensures that portableChars contains only chars conforming to the POSIX recommendation.
153     */
154    private void setPortableChars() {
155        StringBuffer all = new StringBuffer("-_.0123456789");
156 
157        for (char c = 'a'; c <= 'z'; c += 1) {
158            all.append(c);
159            all.append(Character.toUpperCase(c));
160        }
161        portableChars = all.toString().toCharArray();
162        Arrays.sort(portableChars);
163    }
164 
165    public String getAcceptableSuffixText() {
166        return acceptableSuffixText;
167    }
168 
169    public File getAdjustedComicFile(File comicFile)
170        throws IOException {
171        assert comicFile != null;
172        File result;
173        String suffix = getSuffix(comicFile).toLowerCase();
174        FileArchive comicArchive = new FileArchive(comicFile);
175        String fileType = comicArchive.getFileType();
176        String preferredSuffix = (String) preferredSuffixes.get(fileType);
177 
178        assert preferredSuffix != null;
179        assert preferredSuffix.toLowerCase().equals(preferredSuffix);
180 
181        if (!suffix.equals(preferredSuffix)) {
182            String comicFileWithoutSuffix = getWithoutLastSuffix(comicFile.getAbsolutePath());
183 
184            result = new File(comicFileWithoutSuffix + "." + preferredSuffix);
185        } else {
186            result = comicFile;
187        }
188        return result;
189    }
190 
191    public Map getFlattenedFolderNames(Map folderNames) {
192        Map result = new TreeMap();
193        NaturalCaseInsensitiveOrderComparator naturalComparator = new NaturalCaseInsensitiveOrderComparator();
194        List names = new ArrayList(folderNames.keySet());
195        Iterator nameRider;
196 
197        Collections.sort(names, naturalComparator);
198        nameRider = names.iterator();
199        while (nameRider.hasNext()) {
200            String folderName = (String) nameRider.next();
201            String uniqueName = folderName;
202            List folders = (List) folderNames.get(folderName);
203            int uniqueIndex = 1;
204 
205            Collections.sort(folders, naturalComparator);
206 
207            Iterator folderRider = folders.iterator();
208 
209            while (folderRider.hasNext()) {
210                File folder = (File) folderRider.next();
211 
212                while (result.containsKey(uniqueName)) {
213                    // TODO: Use leading zeros before uniqueIndex.
214                    uniqueName = folderName + "_" + uniqueIndex;
215                    uniqueIndex += 1;
216                }
217                result.put(uniqueName, folder);
218            }
219        }
220        return result;
221    }
222 
223    /**
224     *  Get a map with all non-empty sub folders in <code>folder</code> as keys and a <code>List</code>
225     *  of all files in it as values.
226     */
227    public Map getFolderMap(File folder) {
228        return getFolderMap(folder, null);
229    }
230 
231    /**
232     *  Get a map with all non-empty sub folders in <code>folder</code> as keys and a <code>List</code>
233     *  of all files in it accepted by <code>filter</code> as values.
234     */
235    public Map getFolderMap(File folder, FileFilter filter) {
236        File[] files = listFilesRecursively(folder, filter);
237 
238        return getFolderMap(files);
239    }
240 
241    /**
242     *  Get a map where the keys are distinct folders in <code>files</code> and the values are the
243     *  respective files found in each folder.
244     */
245    public Map getFolderMap(File[] files) {
246        Map result = new TreeMap();
247        NaturalCaseInsensitiveOrderComparator naturalCaselessComparator = new NaturalCaseInsensitiveOrderComparator();
248 
249        Arrays.sort(files, naturalCaselessComparator);
250        for (int i = 0; i < files.length; i += 1) {
251            File file = files[i];
252            String parent = file.getParent();
253 
254            if (parent == null) {
255                // Parent will be null for files without folder, for example File("hugo.png").
256                parent = NO_FOLDER_NAME;
257            }
258            addToValueList(result, parent, file);
259        }
260        return result;
261    }
262 
263    /**
264     *  Get a map where the keys are all distinct folder names (without parent path) in <code>folderMap</code>
265     *  and the value is a list of all folders having this name.
266     *
267     * @see    #getFolderMap(File)
268     */
269    public Map getFolderNames(Map folderMap) {
270        Map result = new TreeMap();
271        Iterator folderRider = folderMap.keySet().iterator();
272 
273        while (folderRider.hasNext()) {
274            String folderPath = (String) folderRider.next();
275            File folder = new File(folderPath);
276            String folderName = folder.getName();
277 
278            addToValueList(result, folderName, folder);
279        }
280        return result;
281    }
282 
283    /**
284     *  Get the user's home directory.
285     */
286    public File getHomeDir() {
287        String homeText = System.getProperty("user.home");
288 
289        assert homeText != null;
290        return new File(homeText);
291    }
292 
293    /**
294     *  Get icon for <code>file</code>.
295     *
296     * @see    javax.swing.filechooser.FileView#getIcon(java.io.File)
297     */
298    public Icon getIconFor(File file) {
299        assert file != null;
300        return iconChooser.getIcon(file);
301    }
302 
303    /**
304     *  Get an image URL from net.sf.jomic.images.
305     *
306     * @throws  IllegalStateException  if the image cannot be found
307     */
308    public URL getImageResource(String name) {
309        assert name != null;
310        String fullName = "images/" + name;
311        Class loaderClass = net.sf.jomic.Jomic.class;
312        URL result = loaderClass.getResource(fullName);
313 
314        ifNullThrowCannotFindImageResouce(result, fullName);
315        return result;
316    }
317 
318    /**
319     *  Get an image InputStream from net.sf.jomic.images.
320     *
321     * @throws  IllegalStateException  if the image cannot be found
322     */
323    public InputStream getImageResourceAsStream(String name) {
324        assert name != null;
325        String fullName = "images/" + name;
326        Class loaderClass = net.sf.jomic.Jomic.class;
327        InputStream result = loaderClass.getResourceAsStream(fullName);
328 
329        ifNullThrowCannotFindImageResouce(result, fullName);
330        return result;
331    }
332 
333    /**
334     *  Gets a portable version of <code>suspiciousFileName</code> contains only chars conforming to
335     *  the POSIX recommendation on portable file names. A file name retured by this function is
336     *  guaranteed to be creatable on any common file system. This is achieved by replacing all
337     *  troublesome characters with a hyphen (-). To make the result look less silly, avoid multiple
338     *  hyphens in a row.
339     */
340    public String getPortableFileName(String suspiciousFileName) {
341        assert suspiciousFileName != null;
342        int length = suspiciousFileName.length();
343        char previousCh = 0;
344        StringBuffer result = new StringBuffer(length);
345 
346        for (int i = 0; i < length; i += 1) {
347            char c = suspiciousFileName.charAt(i);
348 
349            if (Arrays.binarySearch(portableChars, c) < 0) {
350                c = '-';
351            }
352            if ((previousCh != '-') || (c != '-')) {
353                result.append(c);
354            }
355            previousCh = c;
356        }
357        return result.toString();
358    }
359 
360    public String getRelativePath(File baseDir, File fileInBaseDir) {
361        String basePath = baseDir.getAbsolutePath();
362        String filePath = fileInBaseDir.getAbsolutePath();
363 
364        assert filePath.startsWith(basePath)
365                : "file " + stringTools.sourced(filePath) + " must start with " + stringTools.sourced(baseDir);
366        String result = filePath.substring(basePath.length() + 1);
367 
368        return result;
369    }
370 
371    public String[] getRelativePaths(File baseDir, File[] filesInBaseDir) {
372        String[] result = new String[filesInBaseDir.length];
373 
374        for (int i = 0; i < filesInBaseDir.length; i += 1) {
375            result[i] = getRelativePath(baseDir, filesInBaseDir[i]);
376        }
377        return result;
378    }
379 
380    /**
381     *  Recusively get size of all files and directories in <code>fileOrDir</code>.
382     */
383    public long getSize(File fileOrDir) {
384        assert fileOrDir != null;
385        long result = 0;
386        File[] items = fileOrDir.listFiles();
387 
388        if (items != null) {
389            for (int i = 0; i < items.length; i += 1) {
390                File item = items[i];
391 
392                if (item.isDirectory()) {
393                    result += getSize(item);
394                } else {
395                    result += item.length();
396                }
397            }
398        }
399        return result;
400    }
401 
402    /**
403     *  Get the (lower case) last suffix of name (without the "."), for example: "hugo.tar.gz"
404     *  yields "gz".
405     */
406    public String getSuffix(File file) {
407        assert file != null;
408        return getSuffix(file.getName());
409    }
410 
411    /**
412     *  Get the (lower case) last suffix of name (without the "."), for example: "hugo.tar.gz"
413     *  yields "gz".
414     */
415    public String getSuffix(String name) {
416        // TODO: handle dir.xxx/name correctly -> should be "", not
417        // "xxx/name"
418        assert name != null;
419        String result;
420        int lastDotIndex = name.lastIndexOf('.');
421 
422        if (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, Logger 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>. If this fails, log a warning to <code>deleteLogger</code>
690     *  .
691     *
692     * @return    DELETED, DELETE_FAILED, or DELETE_DID_NOT_EXIST
693     */
694    public String deleteOrWarn(File file, Logger 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.warn("cannot delete non-existent \"" + file + "\"");
710            result = DELETE_DID_NOT_EXIST;
711        }
712        return result;
713    }
714 
715    /**
716     *  Obtain a recursive list of files in <code>dir</code>.
717     */
718    public File[] listFilesRecursively(File dir) {
719        return listFilesRecursively(dir, null);
720    }
721 
722    /**
723     *  Obtain a recursive list of files in <code>dir</code> using <code>filter</code> to decide
724     *  which files to include in the result.
725     */
726    public File[] listFilesRecursively(File dir, FileFilter filter) {
727        List result = new LinkedList();
728 
729        addFiles(result, dir, filter);
730 
731        return (File[]) result.toArray(new File[0]);
732    }
733 
734    /**
735     *  Same as <code>File.mkdirs()</code> but throws an <code>IOException</code> if the directory
736     *  does not yet exist and also cannot be created.
737     *
738     * @see    File#mkdirs()
739     */
740    public boolean mkdirs(File dir)
741        throws FileNotFoundException {
742        boolean result = dir.mkdirs();
743 
744        if (!dir.exists()) {
745            String message = localeTools.getMessage("errors.cannotCreateDirectory", dir);
746 
747            throw new FileNotFoundException(message);
748        }
749        return result;
750    }
751 
752    /**
753     *  Figure out file type by looking at magic bytes.
754     *
755     * @throws  IOException  in case the format cannot be determined
756     */
757    public String obtainComicFormat(File comicFile)
758        throws IOException {
759        assert comicFile != null;
760        InputStream in = new FileInputStream(comicFile);
761        String result = null;
762 
763        try {
764            byte[] magicBytes = new byte[MAGIC_ZZZ_MAX_LENGTH];
765            String fullMagicText;
766 
767            in.read(magicBytes, 0, MAGIC_ZZZ_MAX_LENGTH);
768            fullMagicText = new String(magicBytes, "US-ASCII");
769 
770            int fullMagicTextLength = fullMagicText.length();
771 
772            if (fullMagicTextLength >= MAGIC_RAR_LENGTH) {
773                String magicText = fullMagicText.substring(0, MAGIC_RAR_LENGTH);
774 
775                // TODO: Find out if magic bytes are exactly "Rar!", and use equals().
776                if (magicText.equalsIgnoreCase(MAGIC_RAR)) {
777                    result = FORMAT_RAR;
778                }
779            }
780            if ((result == null) && (fullMagicTextLength >= MAGIC_ZIP_LENGTH)) {
781                String magicText = fullMagicText.substring(0, MAGIC_ZIP_LENGTH);
782 
783                if (magicText.equals(MAGIC_ZIP)) {
784                    result = FORMAT_ZIP;
785                }
786            }
787            if ((result == null) && (fullMagicTextLength >= MAGIC_PDF_LENGTH)) {
788                String magicText = fullMagicText.substring(0, MAGIC_PDF_LENGTH);
789 
790                if (magicText.equals(MAGIC_PDF)) {
791                    result = FORMAT_PDF;
792                }
793            }
794            if (result == null) {
795                Object[] options = new Object[]{comicFile, fullMagicText};
796                String errorMessage = localeTools.getMessage("errors.cannotDetermineComicFileType", options);
797 
798                throw new IOException(errorMessage);
799            }
800        } finally {
801            in.close();
802        }
803        return result;
804    }
805 
806    /**
807     *  Sort <code>filePaths</code> according to <code>mode</code>.
808     *
809     * @param  mode  one of: SORT_NATURAL, SORT_SMART
810     */
811    public String[] sort(String[] filePaths, String mode) {
812        assert filePaths != null;
813        assert mode != null;
814        String[] result;
815 
816        if (mode.equals(SORT_NATURAL)) {
817            result = (String[]) filePaths.clone();
818            Arrays.sort(result, new NaturalCaseInsensitiveOrderComparator());
819        } else if (mode.equals(SORT_SMART)) {
820            result = sortSmart(filePaths);
821        } else {
822            throw new IllegalArgumentException("mode=" + mode);
823        }
824        return result;
825    }
826 
827    public void writeLines(File targetFile, String[] lines)
828        throws IOException {
829        BufferedWriter writer = new BufferedWriter(new FileWriter(targetFile));
830        String lineSeparator = System.getProperty("line.separator");
831 
832        try {
833            for (int i = 0; i < lines.length; i += 1) {
834                writer.write(lines[i]);
835                writer.write(lineSeparator);
836            }
837        } finally {
838            writer.close();
839        }
840    }
841 
842    private void addFiles(List files, File dirToScanForFiles, FileFilter filter) {
843        assert files != null;
844        assert dirToScanForFiles != null;
845        File[] filesFound = dirToScanForFiles.listFiles();
846 
847        if (filesFound != null) {
848            for (int i = 0; i < filesFound.length; i += 1) {
849                File nextFile = filesFound[i];
850 
851                if (nextFile.isDirectory()) {
852                    addFiles(files, nextFile, filter);
853                } else if ((filter == null) || filter.accept(nextFile)) {
854                    files.add(nextFile);
855                }
856            }
857        } else {
858            String folderNotFound = dirToScanForFiles.getPath();
859            String errorMessage = localeTools.getMessage("errors.cannotAccessFolder", folderNotFound);
860            FileNotFoundException folderNotFoundError = new FileNotFoundException(errorMessage);
861 
862            throw new TunneledIOException(folderNotFoundError);
863        }
864    }
865 
866    /**
867     *  Add <code>value</code> to a <code>List</code> that can be accessed via <code>map</code>
868     *  using <code>key</code>. If there is no list for the key yet, create it and add it.
869     */
870    private void addToValueList(Map map, Object key, Object value) {
871        List nameList = (List) map.get(key);
872 
873        if (nameList == null) {
874            nameList = new ArrayList();
875            map.put(key, nameList);
876        }
877        nameList.add(value);
878    }
879 
880    private void ifNullThrowCannotFindImageResouce(Object result, String fullName) {
881        if (result == null) {
882            String message = localeTools.getMessage("errors.cannotFindImageResource", fullName);
883 
884            throw new IllegalStateException(message);
885        }
886    }
887 
888    private void putAll(Map map, String[] items) {
889        assert map != null;
890        assert items != null;
891        assert items.length > 0;
892        for (int i = 0; i < items.length; i += 1) {
893            String item = items[i];
894 
895            map.put(item, item);
896        }
897    }
898 
899    /**
900     *  Sort images specified by <code>filePaths</code>. This uses natural sort and also takes
901     *  "moronic" numbering of pages in account (for instance using "page-1617.png" instead of the
902     *  proper "page-16+17.png").
903     *
904     * @see    NumberedNameComparator
905     * @see    NaturalCaseInsensitiveOrderComparator
906     */
907    private String[] sortSmart(String[] filePaths) {
908        String[] result;
909        Comparator naturalComparator = new NaturalCaseInsensitiveOrderComparator();
910        SortedMap pathMap = new TreeMap(naturalComparator);
911 
912        // Build a "directory tree" with the following hierarchy: path/prefix/NumberedName
913        for (int i = 0; i < filePaths.length; i += 1) {
914            NumberedName numberedName = new NumberedName(filePaths[i]);
915            String path = numberedName.getPath();
916            SortedMap prefixMap = (SortedMap) pathMap.get(path);
917 
918            if (prefixMap == null) {
919                prefixMap = new TreeMap(naturalComparator);
920                pathMap.put(path, prefixMap);
921            }
922            String prefix = numberedName.getPrefix();
923            List nameList = (List) prefixMap.get(prefix);
924 
925            if (nameList == null) {
926                nameList = new LinkedList();
927                prefixMap.put(prefix, nameList);
928            }
929            nameList.add(numberedName);
930        }
931 
932        List resultList = new LinkedList();
933        List pageLessNames = new LinkedList();
934        Iterator pathRider = pathMap.entrySet().iterator();
935 
936        while (pathRider.hasNext()) {
937            // Build a list of prefixes that have to be demoronized
938            List moronicPrefixList = new LinkedList();
939            Map.Entry entry = (Map.Entry) pathRider.next();
940            String path = (String) entry.getKey();
941            Map prefixMap = (Map) entry.getValue();
942            Iterator prefixRider = prefixMap.entrySet().iterator();
943 
944            while (prefixRider.hasNext()) {
945                Map.Entry prefixEntry = (Map.Entry) prefixRider.next();
946                String prefix = (String) prefixEntry.getKey();
947                List nameList = (List) prefixEntry.getValue();
948                boolean allNamesAreDemoronizable = true;
949                Iterator nameRider = nameList.iterator();
950 
951                while (nameRider.hasNext() && allNamesAreDemoronizable) {
952                    NumberedName name = (NumberedName) nameRider.next();
953 
954                    if (!name.usesPotentiallyMoronicNumbering()) {
955                        allNamesAreDemoronizable = false;
956                    }
957                }
958                if (allNamesAreDemoronizable) {
959                    moronicPrefixList.add(prefix);
960                    if (logger.isInfoEnabled()) {
961                        logger.info(path + prefix + ": demoronizable");
962                    }
963                } else if (logger.isDebugEnabled()) {
964                    logger.debug(path + prefix + ": non-demoronizable");
965                }
966            }
967 
968            if (logger.isInfoEnabled()) {
969                logger.info("prefixes found:");
970                prefixRider = prefixMap.entrySet().iterator();
971                while (prefixRider.hasNext()) {
972                    Map.Entry prefixEntry = (Map.Entry) prefixRider.next();
973                    String prefix = (String) prefixEntry.getKey();
974 
975                    logger.info("  " + prefix);
976                }
977            }
978 
979            // Sort the list for the current paths, possibly demoronizing names.
980            prefixRider = prefixMap.entrySet().iterator();
981            while (prefixRider.hasNext()) {
982                Map.Entry prefixEntry = (Map.Entry) prefixRider.next();
983                String prefix = (String) prefixEntry.getKey();
984                List nameList = (List) prefixEntry.getValue();
985                boolean demoronize = moronicPrefixList.contains(prefix);
986                Comparator comparator = new NumberedNameComparator(demoronize);
987 
988                Collections.sort(nameList, comparator);
989 
990                Iterator sortedRider = nameList.iterator();
991 
992                while (sortedRider.hasNext()) {
993                    NumberedName name = (NumberedName) sortedRider.next();
994                    String fullName = name.getFullName();
995 
996                    if (name.getPage().length() > 0) {
997                        resultList.add(fullName);
998                    } else {
999                        pageLessNames.add(fullName);
1000                    }
1001                }
1002            }
1003        }
1004 
1005        // Add the remaining names that have any page number using
1006        // stock natural comparison
1007        Collections.sort(pageLessNames, naturalComparator);
1008 
1009        Iterator pageLessRider = pageLessNames.iterator();
1010        int targetIndex = 0;
1011 
1012        while (pageLessRider.hasNext()) {
1013            String pageLessName = (String) pageLessRider.next();
1014            String pagedNamed = null;
1015            boolean foundGreaterName = false;
1016 
1017            while ((targetIndex < resultList.size() && !foundGreaterName)) {
1018                pagedNamed = (String) resultList.get(targetIndex);
1019                foundGreaterName = (naturalComparator.compare(pageLessName, pagedNamed)) < 0;
1020                if (!foundGreaterName) {
1021                    targetIndex += 1;
1022                }
1023            }
1024            if (logger.isDebugEnabled()) {
1025                logger.debug("inserting pagesless name at " + targetIndex + ": "
1026                        + stringTools.sourced(pageLessName) + " < " + stringTools.sourced(pagedNamed));
1027            }
1028            resultList.add(targetIndex, pageLessName);
1029        }
1030 
1031        result = (String[]) resultList.toArray(new String[0]);
1032 
1033        // Assert that we did not lose an names
1034        int resultLength = result.length;
1035        int filePathsLength = filePaths.length;
1036 
1037        assert resultLength == filePathsLength : "number of names in result and filePaths must be equal: "
1038                + resultLength + " != " + filePathsLength;
1039 
1040        return result;
1041    }
1042 
1043}

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