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

COVERAGE SUMMARY FOR SOURCE FILE [ArchiveCache.java]

nameclass, %method, %block, %line, %
ArchiveCache.java67%  (2/3)84%  (31/37)78%  (711/915)84%  (189.5/226)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class ArchiveCache$ArchiveCacheContentHandler100% (1/1)57%  (8/14)57%  (104/182)66%  (28.5/43)
characters (char [], int, int): void 0%   (0/1)0%   (0/1)0%   (0/1)
endPrefixMapping (String): void 0%   (0/1)0%   (0/1)0%   (0/1)
ignorableWhitespace (char [], int, int): void 0%   (0/1)0%   (0/1)0%   (0/1)
processingInstruction (String, String): void 0%   (0/1)0%   (0/1)0%   (0/1)
skippedEntity (String): void 0%   (0/1)0%   (0/1)0%   (0/1)
startPrefixMapping (String, String): void 0%   (0/1)0%   (0/1)0%   (0/1)
getRequiredAttribute (Attributes, String): String 100% (1/1)44%  (16/36)57%  (4/7)
<static initializer> 100% (1/1)53%  (8/15)53%  (0.5/1)
startElement (String, String, String, Attributes): void 100% (1/1)59%  (66/111)76%  (16/21)
ArchiveCache$ArchiveCacheContentHandler (ArchiveCache): void 100% (1/1)100% (6/6)100% (3/3)
endDocument (): void 100% (1/1)100% (1/1)100% (1/1)
endElement (String, String, String): void 100% (1/1)100% (1/1)100% (1/1)
setDocumentLocator (Locator): void 100% (1/1)100% (1/1)100% (1/1)
startDocument (): void 100% (1/1)100% (5/5)100% (2/2)
     
class ArchiveCache100% (1/1)100% (23/23)83%  (607/733)88%  (161/183)
attemptToReadEntries (): void 100% (1/1)53%  (27/51)57%  (10.2/18)
createCacheEntry (File): ArchiveCacheEntry 100% (1/1)73%  (63/86)75%  (21.7/29)
writeEntries (): void 100% (1/1)75%  (15/20)96%  (5.7/6)
putWithoutAccess (ArchiveCacheEntry): void 100% (1/1)76%  (16/21)94%  (3.8/4)
put (ArchiveCacheEntry): void 100% (1/1)79%  (19/24)97%  (6.8/7)
<static initializer> 100% (1/1)80%  (12/15)80%  (0.8/1)
readEntries (Reader): void 100% (1/1)80%  (82/102)95%  (18/19)
get (File): ArchiveCacheEntry 100% (1/1)81%  (22/27)91%  (6.4/7)
findLeastRecentlyAccessedUnlockedEntry (): ArchiveCacheEntry 100% (1/1)82%  (55/67)89%  (15.9/18)
remove (File): void 100% (1/1)86%  (32/37)98%  (6.9/7)
clear (): void 100% (1/1)88%  (35/40)96%  (7.6/8)
ArchiveCache (File, long): void 100% (1/1)88%  (38/43)99%  (10.9/11)
adjustSize (): void 100% (1/1)89%  (32/36)89%  (8/9)
writeEntries (Writer): void 100% (1/1)95%  (101/106)97%  (21.4/22)
getBaseDir (): File 100% (1/1)100% (3/3)100% (1/1)
getCacheEntryDir (ArchiveCacheEntry): File 100% (1/1)100% (7/7)100% (2/2)
getCacheMapFile (): File 100% (1/1)100% (7/7)100% (1/1)
getEntryCount (): int 100% (1/1)100% (4/4)100% (1/1)
getMaxSize (): long 100% (1/1)100% (3/3)100% (1/1)
getUsedSize (): long 100% (1/1)100% (3/3)100% (1/1)
setCacheEntrySize (ArchiveCacheEntry): void 100% (1/1)100% (6/6)100% (3/3)
setCacheEntrySizeWithoutAdjustSize (ArchiveCacheEntry): void 100% (1/1)100% (19/19)100% (4/4)
setMaxSize (long): void 100% (1/1)100% (6/6)100% (3/3)
     
class ArchiveCache$10%   (0/1)100% (0/0)100% (0/0)100% (0/0)

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.File;
19import java.io.FileNotFoundException;
20import java.io.FileReader;
21import java.io.FileWriter;
22import java.io.IOException;
23import java.io.Reader;
24import java.io.Writer;
25import java.util.Arrays;
26import java.util.HashMap;
27import java.util.Iterator;
28import java.util.Map;
29 
30import org.apache.commons.logging.Log;
31import org.apache.commons.logging.LogFactory;
32import org.w3c.dom.Document;
33import org.w3c.dom.Element;
34import org.xml.sax.Attributes;
35import org.xml.sax.ContentHandler;
36import org.xml.sax.InputSource;
37import org.xml.sax.Locator;
38import org.xml.sax.SAXException;
39import org.xml.sax.XMLReader;
40import org.xml.sax.helpers.XMLReaderFactory;
41 
42/**
43 *  Cache for extracted archives. The cache maps the file path a a comic book archive to a directory
44 *  where the extracted images are restored. While there is a size limit, it can temporarily be
45 *  exeeded, for example because a single comic is bigger than the limit, or no comics can be
46 *  removed because all of them are locked by a client right now.
47 *
48 * @see       net.sf.jomic.tools.ArchiveCacheEntry
49 * @author    Thomas Aglassinger
50 */
51public class ArchiveCache implements CacheInfo
52{
53    private static final String ATTRIBUTE_INDEX = "index";
54    private static final String ATTRIBUTE_LAST_ACCESSED = "lastAccessed";
55    private static final String ATTRIBUTE_LAST_MODIFIED = "lastModified";
56    private static final String ATTRIBUTE_SOURCE = "source";
57    private static final String ATTRIBUTE_VERSION = "version";
58    private static final String ELEMENT_ARCHIVE = "archive";
59    private static final String ELEMENT_ARCHIVE_CACHE = "archiveCache";
60    private static final String EXPECTED_VERSION = "1.0";
61 
62    //@ public model JMLDataGroup sizes;
63    private File baseDir;
64    private /*@ spec_public @*/ Map entries; //@ in sizes;
65    private FileTools fileTools;
66    private LocaleTools localeTools;
67    private Log logger;
68    private long maxSize; //@ in sizes;
69    private StringTools stringTools;
70    private /*@ spec_public @*/ long usedSize; //@ in sizes;
71    private XmlTools xmlTools;
72 
73    /**
74     *  Create ArchiveCache storing extracted files and index in the directory <code>newBaseDir</code>
75     *  . This directory must not contain any other data because clearing the cache means deleting
76     *  this directory.
77     *
78     * @throws  FileNotFoundException  if <code>newBaseDir</code> cannot be created
79     * @param  newBaseDir              the directory where extracted files will be stored
80     * @param  newMaxSize              intended maximum size in bytes extracted archives should use
81     *      before older files are removed from the cache
82     */
83    //@ requires newMaxSize >= 0;
84    public ArchiveCache(File newBaseDir, long newMaxSize)
85        throws FileNotFoundException {
86        logger = LogFactory.getLog(ArchiveCache.class);
87        fileTools = FileTools.instance();
88        localeTools = LocaleTools.instance();
89        stringTools = StringTools.instance();
90        xmlTools = XmlTools.instance();
91 
92        baseDir = newBaseDir;
93        entries = new HashMap();
94        fileTools.mkdirs(baseDir);
95        maxSize = newMaxSize;
96    }
97 
98    /**
99     *  Set the number of bytes used by <code>entry</code> by computing the number of bytes used by
100     *  all files stored in its archive cache directory.
101     */
102    //@ requires entries.containsValue(entry);
103    //@ assignable sizes;
104    public void setCacheEntrySize(ArchiveCacheEntry entry) {
105        setCacheEntrySizeWithoutAdjustSize(entry);
106        adjustSize();
107    }
108 
109    /**
110     *  Set the number of bytes used by <code>entry</code> by computing the number of bytes used by
111     *  all files stored in its archive cache directory. Use this only when iterating over the
112     *  whole cache as it does not adjust the total cache size. This prevents
113     *  ConcurrentModificationExceptions from happening.
114     */
115    private void setCacheEntrySizeWithoutAdjustSize(ArchiveCacheEntry entry) {
116        usedSize -= entry.getSize();
117        entry.setSize(getBaseDir());
118        usedSize += entry.getSize();
119    }
120 
121    /**
122     *  Set the maximum number of bytes the cache should use. If the cache currently is bigger than
123     *  that, attempt to shrink it to fit.
124     */
125    //@ requires newMaxSize >= 0;
126    //@ assignable sizes;
127    public void setMaxSize(long newMaxSize) {
128        maxSize = newMaxSize;
129        adjustSize();
130    }
131 
132    public /*@ nullable @*/ ArchiveCacheEntry get(File archiveFile) {
133        ArchiveCacheEntry result = null;
134 
135        synchronized (entries) {
136            result = (ArchiveCacheEntry) entries.get(archiveFile);
137 
138            if (result != null) {
139                result.updateLastAccessed();
140            }
141        }
142        return result;
143    }
144 
145    public /*@ pure @*/ File getBaseDir() {
146        return baseDir;
147    }
148 
149    /**
150     *  Get directory where cached files extracted from the archive are stored.
151     */
152    public /*@ pure @*/ File getCacheEntryDir(ArchiveCacheEntry cacheEntry) {
153        File result = cacheEntry.getCachedDir(getBaseDir());
154 
155        return result;
156    }
157 
158    public /*@ pure @*/ int getEntryCount() {
159        return entries.size();
160    }
161 
162    public /*@ pure @*/ long getMaxSize() {
163        return maxSize;
164    }
165 
166    public /*@ pure @*/ long getUsedSize() {
167        return usedSize;
168    }
169 
170    /**
171     *  Get the file in which the mapping of archive names to directories in the cache is stored.
172     */
173    private /*@ pure @*/ File getCacheMapFile() {
174        return new File(getBaseDir(), "cacheMap.xml");
175    }
176 
177    /**
178     *  Attempt to read entries from cacheMap.xml. If the map cannot be found or is broken, start
179     *  with an empty cache.
180     */
181    public void attemptToReadEntries() {
182        boolean doClearCache = false;
183        File cacheMapFile = getCacheMapFile();
184 
185        try {
186            FileReader reader = new FileReader(cacheMapFile);
187 
188            try {
189                readEntries(reader);
190            } finally {
191                reader.close();
192            }
193        } catch (FileNotFoundException error) {
194            logger.info("no cache map found; using new empty map", error);
195        } catch (IOException error) {
196            logger.warn("cannot read cache map entries; clearing cache", error);
197            doClearCache = true;
198        } catch (SAXException error) {
199            logger.warn("cannot parse cache map; clearing cache", error);
200            doClearCache = true;
201        }
202        if (doClearCache) {
203            clear();
204        }
205    }
206 
207    /**
208     *  Clear entries and remove <b>all</b> files in the base directory.
209     */
210    //@ assignable sizes;
211    //@ ensures getUsedSize() == 0;
212    //@ ensures getEntryCount() == 0;
213    public void clear() {
214        synchronized (entries) {
215            File[] cacheFiles = getBaseDir().listFiles();
216 
217            for (int i = 0; i < cacheFiles.length; i += 1) {
218                fileTools.attemptToDeleteAll(cacheFiles[i], logger);
219            }
220            entries.clear();
221            usedSize = 0;
222        }
223    }
224 
225    /**
226     *  Create a new cache entry for <code>newArchiveFile</code> using the next free index.
227     */
228    public ArchiveCacheEntry createCacheEntry(File newArchiveFile) {
229        ArchiveCacheEntry result;
230        int newIndex = -1;
231 
232        if (getEntryCount() > 0) {
233            int[] indexes = new int[getEntryCount()];
234            int i = 0;
235            Iterator rider = entries.values().iterator();
236 
237            while (rider.hasNext()) {
238                ArchiveCacheEntry entry = (ArchiveCacheEntry) rider.next();
239 
240                indexes[i] = entry.getIndex();
241                i += 1;
242            }
243            Arrays.sort(indexes);
244 
245            int firstIndex = indexes[0];
246 
247            if (firstIndex > 0) {
248                newIndex = firstIndex - 1;
249            } else {
250                int currentIndex = firstIndex;
251                boolean freeIndexFound = false;
252 
253                i = 1;
254                while ((i < indexes.length) && !freeIndexFound) {
255                    int nextIndex = indexes[i];
256 
257                    //@ assert currentIndex != nextIndex;
258                    if (nextIndex - currentIndex > 1) {
259                        freeIndexFound = true;
260                    } else {
261                        currentIndex = nextIndex;
262                        i += 1;
263                    }
264                }
265                newIndex = currentIndex + 1;
266            }
267            // TODO: @ assert Arrays.binarySearch(indexes, newIndex) < 0;
268        } else {
269            newIndex = 0;
270        }
271 
272        //@ assert newIndex >= 0;
273        result = new ArchiveCacheEntry(newArchiveFile, newIndex);
274 
275        return result;
276    }
277 
278    //@ requires entry.getSize() == 0;
279    //@ assignable sizes;
280    public void put(ArchiveCacheEntry entry) {
281        synchronized (entries) {
282            putWithoutAccess(entry);
283            entry.updateLastAccessed();
284            entry.updateLastModified();
285            setCacheEntrySize(entry);
286        }
287    }
288 
289    //@ assignable sizes;
290    public void readEntries(Reader reader)
291        throws IOException, SAXException {
292        String parserName = org.apache.xerces.parsers.SAXParser.class.getName();
293        XMLReader xmlReader = XMLReaderFactory.createXMLReader(parserName);
294        ContentHandler contentHandler = new ArchiveCacheContentHandler();
295        InputSource inputSource = new InputSource(reader);
296 
297        xmlReader.setContentHandler(contentHandler);
298        xmlReader.parse(inputSource);
299 
300        Iterator rider = entries.values().iterator();
301 
302        // Compute size used by cache.
303        usedSize = 0;
304        while (rider.hasNext()) {
305            ArchiveCacheEntry cacheEntryToSetSizeFor = (ArchiveCacheEntry) rider.next();
306 
307            setCacheEntrySizeWithoutAdjustSize(cacheEntryToSetSizeFor);
308 
309            long cacheEntrySize = cacheEntryToSetSizeFor.getSize();
310 
311            if (logger.isDebugEnabled()) {
312                logger.debug("size of " + cacheEntryToSetSizeFor.getCachedDirName()
313                        + ": " + localeTools.asByteText(cacheEntrySize));
314            }
315        }
316        // Adjust size in case the settings specified a smaller size now than back then when it was
317        // written.
318        adjustSize();
319        if (logger.isInfoEnabled()) {
320            logger.info("size of cache in " + getBaseDir() + ": " + localeTools.asByteText(usedSize));
321        }
322    }
323 
324    //@ assignable sizes;
325    public void remove(File archiveFile) {
326        synchronized (entries) {
327            ArchiveCacheEntry entry = (ArchiveCacheEntry) entries.remove(archiveFile);
328            long oldEntrySize = entry.getSize();
329 
330            //@ assert entry != null;
331            fileTools.attemptToDeleteAll(getCacheEntryDir(entry), logger);
332            usedSize -= oldEntrySize;
333        }
334    }
335 
336    public void writeEntries()
337        throws IOException {
338        File cacheMapFile = getCacheMapFile();
339        Writer writer = new FileWriter(cacheMapFile);
340 
341        try {
342            writeEntries(writer);
343        } finally {
344            writer.close();
345        }
346    }
347 
348    public void writeEntries(Writer writer)
349        throws IOException {
350        // Build the document
351        Document document = xmlTools.createEmptyDocument(null);
352        Element rootElement = document.createElement(ELEMENT_ARCHIVE_CACHE);
353 
354        synchronized (entries) {
355            Iterator rider = entries.values().iterator();
356 
357            document.appendChild(rootElement);
358            rootElement.setAttribute(ATTRIBUTE_VERSION, EXPECTED_VERSION);
359            while (rider.hasNext()) {
360                ArchiveCacheEntry entry = (ArchiveCacheEntry) rider.next();
361                Element archiveElement = document.createElement(ELEMENT_ARCHIVE);
362                int lockCount = entry.getLockCount();
363                File archiveFile = entry.getArchiveFile();
364 
365                if (lockCount > 0) {
366                    logger.warn("writting entry with lockCount=" + lockCount + " (should be 0): "
367                            + stringTools.sourced(archiveFile));
368                }
369                archiveElement.setAttribute(ATTRIBUTE_INDEX, Integer.toString(entry.getIndex()));
370                archiveElement.setAttribute(ATTRIBUTE_LAST_ACCESSED, Long.toString(entry.getLastAccessed()));
371                archiveElement.setAttribute(ATTRIBUTE_LAST_MODIFIED, Long.toString(entry.getLastModified()));
372                archiveElement.setAttribute(ATTRIBUTE_SOURCE, archiveFile.getAbsolutePath());
373                rootElement.appendChild(archiveElement);
374            }
375        }
376        xmlTools.write(document, writer);
377    }
378 
379    //@ assignable sizes;
380    private void adjustSize() {
381        if (usedSize > maxSize) {
382            // Remove old entries from the cache, but keep all locked ones.
383            while ((getEntryCount() >= 2) && (usedSize > maxSize)) {
384                ArchiveCacheEntry leastRecentlyAccessedEntry = findLeastRecentlyAccessedUnlockedEntry();
385 
386                if (leastRecentlyAccessedEntry != null) {
387                    remove(leastRecentlyAccessedEntry.getArchiveFile());
388                } else {
389                    logger.warn("cannot remove any cache entry dispite size exceeding maximum");
390                }
391            }
392        } else {
393            logger.debug("no size adjustment needed");
394        }
395    }
396 
397    private /*@ nullable pure @*/ ArchiveCacheEntry findLeastRecentlyAccessedUnlockedEntry() {
398        ArchiveCacheEntry result = null;
399 
400        synchronized (entries) {
401            Iterator rider = entries.values().iterator();
402 
403            while (rider.hasNext()) {
404                ArchiveCacheEntry nextEntry = (ArchiveCacheEntry) rider.next();
405                boolean unlocked = (nextEntry.getLockCount() == 0);
406 
407                if (unlocked) {
408                    if (result != null) {
409                        long nextAccessed = nextEntry.getLastAccessed();
410                        long leastRecentlyAccessed;
411 
412                        leastRecentlyAccessed = result.getLastAccessed();
413 
414                        if (leastRecentlyAccessed > nextAccessed) {
415                            result = nextEntry;
416                        }
417                    } else {
418                        result = nextEntry;
419                    }
420                }
421            }
422            assert (result == null) || (result.getLockCount() == 0);
423        }
424 
425        return result;
426    }
427 
428    //@ modifies entries;
429   private void putWithoutAccess(ArchiveCacheEntry entry) {
430       synchronized (entries) {
431           entries.put(entry.getArchiveFile(), entry);
432       }
433    }
434 
435    /**
436     *  SAX ContentHandler to read cache index for mapping archive paths to cache diretories.
437     *
438     * @author    Thomas Aglassinger
439     */
440    private final class ArchiveCacheContentHandler implements ContentHandler
441    {
442        private ArchiveCacheContentHandler() {
443            super();
444        }
445 
446        public void setDocumentLocator(Locator newLocator) {
447            // Do nothing.
448        }
449 
450        private String getRequiredAttribute(Attributes attributes, String attributeName)
451            throws SAXException {
452            assert attributes != null;
453            assert attributeName != null;
454            String result = attributes.getValue(attributeName);
455 
456            if (result == null) {
457                String message = localeTools.getMessage("errors.xml.attributeMustBeSpecified", attributeName);
458 
459                throw new SAXException(message);
460            }
461            return result;
462        }
463 
464        public void characters(char[] arg0, int arg1, int arg2)
465            throws SAXException {
466            // Do nothing.
467        }
468 
469        public void endDocument()
470            throws SAXException {
471            // Do nothing.
472        }
473 
474        public void endElement(String arg0, String arg1, String arg2)
475            throws SAXException {
476            // Do nothing.
477        }
478 
479        public void endPrefixMapping(String arg0)
480            throws SAXException {
481            // Do nothing.
482        }
483 
484        public void ignorableWhitespace(char[] arg0, int arg1, int arg2)
485            throws SAXException {
486            // Do nothing.
487        }
488 
489        public void processingInstruction(String arg0, String arg1)
490            throws SAXException {
491            // Do nothing.
492        }
493 
494        public void skippedEntity(String arg0)
495            throws SAXException {
496            // Do nothing.
497        }
498 
499        public void startDocument()
500            throws SAXException {
501            entries.clear();
502        }
503 
504        public void startElement(String namespaceURI, String localElementName,
505                String qName, Attributes attributes)
506            throws SAXException {
507            if (localElementName.equals(ELEMENT_ARCHIVE)) {
508                String indexValue = getRequiredAttribute(attributes, ATTRIBUTE_INDEX);
509                String lastAccessedValue = getRequiredAttribute(attributes, ATTRIBUTE_LAST_ACCESSED);
510                String lastModifiedValue = getRequiredAttribute(attributes, ATTRIBUTE_LAST_MODIFIED);
511                String archivePath = getRequiredAttribute(attributes, ATTRIBUTE_SOURCE);
512                int index = Integer.parseInt(indexValue);
513                long lastAccessed = Long.parseLong(lastAccessedValue);
514                long lastModified = Long.parseLong(lastModifiedValue);
515                File archiveFile = new File(archivePath);
516                ArchiveCacheEntry entry = new ArchiveCacheEntry(archiveFile, lastAccessed, lastModified, index);
517 
518                putWithoutAccess(entry);
519            } else if (localElementName.equals(ELEMENT_ARCHIVE_CACHE)) {
520                String version = getRequiredAttribute(attributes, ATTRIBUTE_VERSION);
521 
522                if (!version.equals(EXPECTED_VERSION)) {
523                    Object[] options = new Object[]{
524                            ATTRIBUTE_VERSION,
525                            new Integer(EXPECTED_VERSION),
526                            new Integer(version)
527                            };
528                    String message = localeTools.getMessage("errors.xml.valueOfAttributeMustBeXButIsY", options);
529 
530                    throw new SAXException(message);
531                }
532            } else {
533                String message = localeTools.getMessage("errors.xml.unknownElement", localElementName);
534 
535                throw new SAXException(message);
536            }
537        }
538 
539        public void startPrefixMapping(String arg0, String arg1)
540            throws SAXException {
541            // Do nothing.
542        }
543    }
544}

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