├── .travis.yml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── .gitignore
├── README.third_party
├── LICENSE
├── CHANGELOG.md
├── src
├── main
│ └── java
│ │ └── com
│ │ └── bumptech
│ │ └── glide
│ │ └── disklrucache
│ │ ├── Util.java
│ │ ├── StrictLineReader.java
│ │ └── DiskLruCache.java
└── test
│ └── java
│ └── com
│ └── bumptech
│ └── glide
│ └── disklrucache
│ ├── StrictLineReaderTest.java
│ └── DiskLruCacheTest.java
├── README.md
├── checkstyle.xml
├── gradlew
└── LICENSE.txt
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | sudo: false
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjudd/DiskLruCache/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Glide Disk LRU Cache Library
2 | POM_ARTIFACT_ID=disklrucache
3 | POM_PACKAGING=jar
4 | POM_DESCRIPTION=A cache that uses a bounded amount of space on a filesystem. Based on Jake Wharton's tailored for Glide.
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jun 28 20:49:51 PDT 2014
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-1.12-all.zip
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #Eclipse
2 | .project
3 | .classpath
4 | .settings
5 | .checkstyle
6 |
7 | #IntelliJ IDEA
8 | .idea
9 | *.iml
10 | *.ipr
11 | *.iws
12 |
13 | #Maven
14 | target
15 | release.properties
16 | pom.xml.*
17 |
18 | #OSX
19 | .DS_Store
20 |
21 | #gradle
22 | build/**
23 | .gradle/**
24 |
--------------------------------------------------------------------------------
/README.third_party:
--------------------------------------------------------------------------------
1 | URL: https://github.com/JakeWharton/DiskLruCache/tarball/7a1ecbd38d2ad0873fb843e911d60235b7434acb
2 | Version: 7a1ecbd38d2ad0873fb843e911d60235b7434acb
3 | License: Apache 2.0
4 | License File: LICENSE
5 |
6 | Description:
7 | Java implementation of a Disk-based LRU cache which specifically targets Android compatibility.
8 |
9 | Local Modifications:
10 | Exposed File objects directly to gets, removed key validation, removed test sources.
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2012 Jake Wharton
2 | Copyright 2011 The Android Open Source Project
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Change Log
2 | ==========
3 |
4 | Version 2.0.2 *(2013-06-18)*
5 | ----------------------------
6 |
7 | * Fix: Prevent exception trying to delete a non-existent file.
8 |
9 |
10 | Version 2.0.1 *(2013-04-27)*
11 | ----------------------------
12 |
13 | * Fix: Do not throw runtime exceptions for racy file I/O.
14 | * Fix: Synchronize calls to `isClosed`.
15 |
16 |
17 | Version 2.0.0 *(2013-04-13)*
18 | ----------------------------
19 |
20 | The package name is now `com.jakewharton.disklrucache`.
21 |
22 | * New: Automatically flush the cache when an edit is completed.
23 | * Fix: Ensure file handles are not held when a file is not found.
24 | * Fix: Correct journal rebuilds on Windows.
25 | * Fix: Ensure file writer uses the appropriate encoding.
26 |
27 |
28 | Version 1.3.1 *(2013-01-02)*
29 | ----------------------------
30 |
31 | * Fix: Correct logic around detecting whether a journal rebuild is required.
32 | *(Thanks Jonathan Gerbaud)*
33 |
34 |
35 | Version 1.3.0 *(2012-12-24)*
36 | ----------------------------
37 |
38 | * Re-allow dash in cache key (now `[a-z0-9_-]{1,64}`).
39 | * New: `getLength` method on `Snapshot`. *(Thanks Edward Dale)*
40 | * Performance improvements reading journal lines.
41 |
42 |
43 | Version 1.2.1 *(2012-10-08)*
44 | ----------------------------
45 |
46 | * Fix: Ensure library references Java 5-compatible version of
47 | `Arrays.copyOfRange`. *(Thanks Edward Dale)*
48 |
49 |
50 | Version 1.2.0 *(2012-09-30)*
51 | ----------------------------
52 |
53 | * New API for cache size adjustment.
54 | * Keys are now enforced to match `[a-z0-9_]{1,64}` *(Thanks Brian Langel)*
55 | * Fix: Cache will gracefully recover if directory is deleted at runtime.
56 |
57 |
58 | Version 1.1.0 *(2012-01-07)*
59 | ----------------------------
60 |
61 | * New API for editing an existing snapshot. *(Thanks Jesse Wilson)*
62 |
63 |
64 | Version 1.0.0 *(2012-01-04)*
65 | ----------------------------
66 |
67 | Initial version.
68 |
--------------------------------------------------------------------------------
/src/main/java/com/bumptech/glide/disklrucache/Util.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2010 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.bumptech.glide.disklrucache;
18 |
19 | import java.io.Closeable;
20 | import java.io.File;
21 | import java.io.IOException;
22 | import java.io.Reader;
23 | import java.io.StringWriter;
24 | import java.nio.charset.Charset;
25 |
26 | /** Junk drawer of utility methods. */
27 | final class Util {
28 | static final Charset US_ASCII = Charset.forName("US-ASCII");
29 | static final Charset UTF_8 = Charset.forName("UTF-8");
30 |
31 | private Util() {
32 | }
33 |
34 | static String readFully(Reader reader) throws IOException {
35 | try {
36 | StringWriter writer = new StringWriter();
37 | char[] buffer = new char[1024];
38 | int count;
39 | while ((count = reader.read(buffer)) != -1) {
40 | writer.write(buffer, 0, count);
41 | }
42 | return writer.toString();
43 | } finally {
44 | reader.close();
45 | }
46 | }
47 |
48 | /**
49 | * Deletes the contents of {@code dir}. Throws an IOException if any file
50 | * could not be deleted, or if {@code dir} is not a readable directory.
51 | */
52 | static void deleteContents(File dir) throws IOException {
53 | File[] files = dir.listFiles();
54 | if (files == null) {
55 | throw new IOException("not a readable directory: " + dir);
56 | }
57 | for (File file : files) {
58 | if (file.isDirectory()) {
59 | deleteContents(file);
60 | }
61 | if (!file.delete()) {
62 | throw new IOException("failed to delete file: " + file);
63 | }
64 | }
65 | }
66 |
67 | static void closeQuietly(/*Auto*/Closeable closeable) {
68 | if (closeable != null) {
69 | try {
70 | closeable.close();
71 | } catch (RuntimeException rethrown) {
72 | throw rethrown;
73 | } catch (Exception ignored) {
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Disk LRU Cache
2 | ==============
3 |
4 | A cache that uses a bounded amount of space on a filesystem. Each cache entry
5 | has a string key and a fixed number of values. Each key must match the regex
6 | `[a-z0-9_-]{1,64}`. Values are byte sequences, accessible as streams or files.
7 | Each value must be between `0` and `Integer.MAX_VALUE` bytes in length.
8 |
9 | The cache stores its data in a directory on the filesystem. This directory must
10 | be exclusive to the cache; the cache may delete or overwrite files from its
11 | directory. It is an error for multiple processes to use the same cache
12 | directory at the same time.
13 |
14 | This cache limits the number of bytes that it will store on the filesystem.
15 | When the number of stored bytes exceeds the limit, the cache will remove
16 | entries in the background until the limit is satisfied. The limit is not
17 | strict: the cache may temporarily exceed it while waiting for files to be
18 | deleted. The limit does not include filesystem overhead or the cache journal so
19 | space-sensitive applications should set a conservative limit.
20 |
21 | Clients call `edit` to create or update the values of an entry. An entry may
22 | have only one editor at one time; if a value is not available to be edited then
23 | `edit` will return null.
24 |
25 | * When an entry is being **created** it is necessary to supply a full set of
26 | values; the empty value should be used as a placeholder if necessary.
27 | * When an entry is being **edited**, it is not necessary to supply data for
28 | every value; values default to their previous value.
29 |
30 | Every `edit` call must be matched by a call to `Editor.commit` or
31 | `Editor.abort`. Committing is atomic: a read observes the full set of values as
32 | they were before or after the commit, but never a mix of values.
33 |
34 | Clients call `get` to read a snapshot of an entry. The read will observe the
35 | value at the time that `get` was called. Updates and removals after the call do
36 | not impact ongoing reads.
37 |
38 | This class is tolerant of some I/O errors. If files are missing from the
39 | filesystem, the corresponding entries will be dropped from the cache. If an
40 | error occurs while writing a cache value, the edit will fail silently. Callers
41 | should handle other problems by catching `IOException` and responding
42 | appropriately.
43 |
44 | *Note: This implementation specifically targets Android compatibility.*
45 |
46 | License
47 | =======
48 |
49 | Copyright 2012 Jake Wharton
50 | Copyright 2011 The Android Open Source Project
51 |
52 | Licensed under the Apache License, Version 2.0 (the "License");
53 | you may not use this file except in compliance with the License.
54 | You may obtain a copy of the License at
55 |
56 | http://www.apache.org/licenses/LICENSE-2.0
57 |
58 | Unless required by applicable law or agreed to in writing, software
59 | distributed under the License is distributed on an "AS IS" BASIS,
60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
61 | See the License for the specific language governing permissions and
62 | limitations under the License.
63 |
64 |
--------------------------------------------------------------------------------
/src/test/java/com/bumptech/glide/disklrucache/StrictLineReaderTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.bumptech.glide.disklrucache;
18 |
19 | import org.junit.Assert;
20 | import org.junit.Test;
21 | import org.junit.runner.RunWith;
22 | import org.junit.runners.JUnit4;
23 |
24 | import java.io.ByteArrayInputStream;
25 | import java.io.EOFException;
26 | import java.io.IOException;
27 | import java.io.InputStream;
28 |
29 | @RunWith(JUnit4.class)
30 | public class StrictLineReaderTest {
31 | @Test public void lineReaderConsistencyWithReadAsciiLine() {
32 | try {
33 | // Testing with LineReader buffer capacity 32 to check some corner cases.
34 | StrictLineReader lineReader =
35 | new StrictLineReader(createTestInputStream(), 32, Util.US_ASCII);
36 | InputStream refStream = createTestInputStream();
37 | while (true) {
38 | try {
39 | String refLine = readAsciiLine(refStream);
40 | try {
41 | String line = lineReader.readLine();
42 | if (!refLine.equals(line)) {
43 | Assert.fail("line (\"" + line + "\") differs from expected (\"" + refLine + "\").");
44 | }
45 | } catch (EOFException eof) {
46 | Assert.fail("line reader threw EOFException too early.");
47 | }
48 | } catch (EOFException refEof) {
49 | try {
50 | lineReader.readLine();
51 | Assert.fail("line reader didn't throw the expected EOFException.");
52 | } catch (EOFException expected) {
53 | break;
54 | }
55 | }
56 | }
57 | refStream.close();
58 | lineReader.close();
59 | } catch (IOException ioe) {
60 | Assert.fail("Unexpected IOException " + ioe.toString());
61 | }
62 | }
63 |
64 | /* XXX From libcore.io.Streams */
65 | private static String readAsciiLine(InputStream in) throws IOException {
66 | // TODO: support UTF-8 here instead
67 |
68 | StringBuilder result = new StringBuilder(80);
69 | while (true) {
70 | int c = in.read();
71 | if (c == -1) {
72 | throw new EOFException();
73 | } else if (c == '\n') {
74 | break;
75 | }
76 |
77 | result.append((char) c);
78 | }
79 | int length = result.length();
80 | if (length > 0 && result.charAt(length - 1) == '\r') {
81 | result.setLength(length - 1);
82 | }
83 | return result.toString();
84 | }
85 |
86 | private static InputStream createTestInputStream() {
87 | return new ByteArrayInputStream((""
88 | // Each source lines below should represent 32 bytes, until the next comment.
89 | + "12 byte line\n18 byte line......\n"
90 | + "pad\nline spanning two 32-byte bu"
91 | + "ffers\npad......................\n"
92 | + "pad\nline spanning three 32-byte "
93 | + "buffers and ending with LF at th"
94 | + "e end of a 32 byte buffer......\n"
95 | + "pad\nLine ending with CRLF split"
96 | + " at the end of a 32-byte buffer\r"
97 | + "\npad...........................\n"
98 | // End of 32-byte lines.
99 | + "line ending with CRLF\r\n"
100 | + "this is a long line with embedded CR \r ending with CRLF and having more than "
101 | + "32 characters\r\n"
102 | + "unterminated line - should be dropped").getBytes());
103 | }
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/src/main/java/com/bumptech/glide/disklrucache/StrictLineReader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.bumptech.glide.disklrucache;
18 |
19 | import java.io.ByteArrayOutputStream;
20 | import java.io.Closeable;
21 | import java.io.EOFException;
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import java.io.UnsupportedEncodingException;
25 | import java.nio.charset.Charset;
26 |
27 | /**
28 | * Buffers input from an {@link InputStream} for reading lines.
29 | *
30 | *
This class is used for buffered reading of lines. For purposes of this class, a line ends
31 | * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
32 | * line at end of input is invalid and will be ignored, the caller may use {@code
33 | * hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
34 | *
35 | *
This class is intended for reading input that strictly consists of lines, such as line-based
36 | * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
37 | * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
38 | * end-of-input reporting and a more restrictive definition of a line.
39 | *
40 | *
This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
41 | * and 10, respectively, and the representation of no other character contains these values.
42 | * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
43 | * The default charset is US_ASCII.
44 | */
45 | class StrictLineReader implements Closeable {
46 | private static final byte CR = (byte) '\r';
47 | private static final byte LF = (byte) '\n';
48 |
49 | private final InputStream in;
50 | private final Charset charset;
51 |
52 | /*
53 | * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
54 | * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
55 | * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
56 | * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
57 | */
58 | private byte[] buf;
59 | private int pos;
60 | private int end;
61 |
62 | /**
63 | * Constructs a new {@code LineReader} with the specified charset and the default capacity.
64 | *
65 | * @param in the {@code InputStream} to read data from.
66 | * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
67 | * supported.
68 | * @throws NullPointerException if {@code in} or {@code charset} is null.
69 | * @throws IllegalArgumentException if the specified charset is not supported.
70 | */
71 | public StrictLineReader(InputStream in, Charset charset) {
72 | this(in, 8192, charset);
73 | }
74 |
75 | /**
76 | * Constructs a new {@code LineReader} with the specified capacity and charset.
77 | *
78 | * @param in the {@code InputStream} to read data from.
79 | * @param capacity the capacity of the buffer.
80 | * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
81 | * supported.
82 | * @throws NullPointerException if {@code in} or {@code charset} is null.
83 | * @throws IllegalArgumentException if {@code capacity} is negative or zero
84 | * or the specified charset is not supported.
85 | */
86 | public StrictLineReader(InputStream in, int capacity, Charset charset) {
87 | if (in == null || charset == null) {
88 | throw new NullPointerException();
89 | }
90 | if (capacity < 0) {
91 | throw new IllegalArgumentException("capacity <= 0");
92 | }
93 | if (!(charset.equals(Util.US_ASCII))) {
94 | throw new IllegalArgumentException("Unsupported encoding");
95 | }
96 |
97 | this.in = in;
98 | this.charset = charset;
99 | buf = new byte[capacity];
100 | }
101 |
102 | /**
103 | * Closes the reader by closing the underlying {@code InputStream} and
104 | * marking this reader as closed.
105 | *
106 | * @throws IOException for errors when closing the underlying {@code InputStream}.
107 | */
108 | public void close() throws IOException {
109 | synchronized (in) {
110 | if (buf != null) {
111 | buf = null;
112 | in.close();
113 | }
114 | }
115 | }
116 |
117 | /**
118 | * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
119 | * this end of line marker is not included in the result.
120 | *
121 | * @return the next line from the input.
122 | * @throws IOException for underlying {@code InputStream} errors.
123 | * @throws EOFException for the end of source stream.
124 | */
125 | public String readLine() throws IOException {
126 | synchronized (in) {
127 | if (buf == null) {
128 | throw new IOException("LineReader is closed");
129 | }
130 |
131 | // Read more data if we are at the end of the buffered data.
132 | // Though it's an error to read after an exception, we will let {@code fillBuf()}
133 | // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
134 | if (pos >= end) {
135 | fillBuf();
136 | }
137 | // Try to find LF in the buffered data and return the line if successful.
138 | for (int i = pos; i != end; ++i) {
139 | if (buf[i] == LF) {
140 | int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
141 | String res = new String(buf, pos, lineEnd - pos, charset.name());
142 | pos = i + 1;
143 | return res;
144 | }
145 | }
146 |
147 | // Let's anticipate up to 80 characters on top of those already read.
148 | ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
149 | @Override
150 | public String toString() {
151 | int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
152 | try {
153 | return new String(buf, 0, length, charset.name());
154 | } catch (UnsupportedEncodingException e) {
155 | throw new AssertionError(e); // Since we control the charset this will never happen.
156 | }
157 | }
158 | };
159 |
160 | while (true) {
161 | out.write(buf, pos, end - pos);
162 | // Mark unterminated line in case fillBuf throws EOFException or IOException.
163 | end = -1;
164 | fillBuf();
165 | // Try to find LF in the buffered data and return the line if successful.
166 | for (int i = pos; i != end; ++i) {
167 | if (buf[i] == LF) {
168 | if (i != pos) {
169 | out.write(buf, pos, i - pos);
170 | }
171 | pos = i + 1;
172 | return out.toString();
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
179 | public boolean hasUnterminatedLine() {
180 | return end == -1;
181 | }
182 |
183 | /**
184 | * Reads new input data into the buffer. Call only with pos == end or end == -1,
185 | * depending on the desired outcome if the function throws.
186 | */
187 | private void fillBuf() throws IOException {
188 | int result = in.read(buf, 0, buf.length);
189 | if (result == -1) {
190 | throw new EOFException();
191 | }
192 | pos = 0;
193 | end = result;
194 | }
195 | }
196 |
197 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2012 Jake Wharton
191 | Copyright 2011 The Android Open Source Project
192 |
193 | Licensed under the Apache License, Version 2.0 (the "License");
194 | you may not use this file except in compliance with the License.
195 | You may obtain a copy of the License at
196 |
197 | http://www.apache.org/licenses/LICENSE-2.0
198 |
199 | Unless required by applicable law or agreed to in writing, software
200 | distributed under the License is distributed on an "AS IS" BASIS,
201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202 | See the License for the specific language governing permissions and
203 | limitations under the License.
204 |
--------------------------------------------------------------------------------
/src/main/java/com/bumptech/glide/disklrucache/DiskLruCache.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.bumptech.glide.disklrucache;
18 |
19 | import java.io.BufferedWriter;
20 | import java.io.Closeable;
21 | import java.io.EOFException;
22 | import java.io.File;
23 | import java.io.FileInputStream;
24 | import java.io.FileNotFoundException;
25 | import java.io.FileOutputStream;
26 | import java.io.IOException;
27 | import java.io.InputStream;
28 | import java.io.InputStreamReader;
29 | import java.io.OutputStream;
30 | import java.io.OutputStreamWriter;
31 | import java.io.Writer;
32 | import java.util.ArrayList;
33 | import java.util.Iterator;
34 | import java.util.LinkedHashMap;
35 | import java.util.Map;
36 | import java.util.concurrent.Callable;
37 | import java.util.concurrent.LinkedBlockingQueue;
38 | import java.util.concurrent.ThreadFactory;
39 | import java.util.concurrent.ThreadPoolExecutor;
40 | import java.util.concurrent.TimeUnit;
41 |
42 | /**
43 | * A cache that uses a bounded amount of space on a filesystem. Each cache
44 | * entry has a string key and a fixed number of values. Each key must match
45 | * the regex [a-z0-9_-]{1,120}. Values are byte sequences,
46 | * accessible as streams or files. Each value must be between {@code 0} and
47 | * {@code Integer.MAX_VALUE} bytes in length.
48 | *
49 | *
The cache stores its data in a directory on the filesystem. This
50 | * directory must be exclusive to the cache; the cache may delete or overwrite
51 | * files from its directory. It is an error for multiple processes to use the
52 | * same cache directory at the same time.
53 | *
54 | *
This cache limits the number of bytes that it will store on the
55 | * filesystem. When the number of stored bytes exceeds the limit, the cache will
56 | * remove entries in the background until the limit is satisfied. The limit is
57 | * not strict: the cache may temporarily exceed it while waiting for files to be
58 | * deleted. The limit does not include filesystem overhead or the cache
59 | * journal so space-sensitive applications should set a conservative limit.
60 | *
61 | *
Clients call {@link #edit} to create or update the values of an entry. An
62 | * entry may have only one editor at one time; if a value is not available to be
63 | * edited then {@link #edit} will return null.
64 | *
65 | * - When an entry is being created it is necessary to
66 | * supply a full set of values; the empty value should be used as a
67 | * placeholder if necessary.
68 | *
- When an entry is being edited, it is not necessary
69 | * to supply data for every value; values default to their previous
70 | * value.
71 | *
72 | * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
73 | * or {@link Editor#abort}. Committing is atomic: a read observes the full set
74 | * of values as they were before or after the commit, but never a mix of values.
75 | *
76 | * Clients call {@link #get} to read a snapshot of an entry. The read will
77 | * observe the value at the time that {@link #get} was called. Updates and
78 | * removals after the call do not impact ongoing reads.
79 | *
80 | *
This class is tolerant of some I/O errors. If files are missing from the
81 | * filesystem, the corresponding entries will be dropped from the cache. If
82 | * an error occurs while writing a cache value, the edit will fail silently.
83 | * Callers should handle other problems by catching {@code IOException} and
84 | * responding appropriately.
85 | */
86 | public final class DiskLruCache implements Closeable {
87 | static final String JOURNAL_FILE = "journal";
88 | static final String JOURNAL_FILE_TEMP = "journal.tmp";
89 | static final String JOURNAL_FILE_BACKUP = "journal.bkp";
90 | static final String MAGIC = "libcore.io.DiskLruCache";
91 | static final String VERSION_1 = "1";
92 | static final long ANY_SEQUENCE_NUMBER = -1;
93 | private static final String CLEAN = "CLEAN";
94 | private static final String DIRTY = "DIRTY";
95 | private static final String REMOVE = "REMOVE";
96 | private static final String READ = "READ";
97 |
98 | /*
99 | * This cache uses a journal file named "journal". A typical journal file
100 | * looks like this:
101 | * libcore.io.DiskLruCache
102 | * 1
103 | * 100
104 | * 2
105 | *
106 | * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
107 | * DIRTY 335c4c6028171cfddfbaae1a9c313c52
108 | * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
109 | * REMOVE 335c4c6028171cfddfbaae1a9c313c52
110 | * DIRTY 1ab96a171faeeee38496d8b330771a7a
111 | * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
112 | * READ 335c4c6028171cfddfbaae1a9c313c52
113 | * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
114 | *
115 | * The first five lines of the journal form its header. They are the
116 | * constant string "libcore.io.DiskLruCache", the disk cache's version,
117 | * the application's version, the value count, and a blank line.
118 | *
119 | * Each of the subsequent lines in the file is a record of the state of a
120 | * cache entry. Each line contains space-separated values: a state, a key,
121 | * and optional state-specific values.
122 | * o DIRTY lines track that an entry is actively being created or updated.
123 | * Every successful DIRTY action should be followed by a CLEAN or REMOVE
124 | * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
125 | * temporary files may need to be deleted.
126 | * o CLEAN lines track a cache entry that has been successfully published
127 | * and may be read. A publish line is followed by the lengths of each of
128 | * its values.
129 | * o READ lines track accesses for LRU.
130 | * o REMOVE lines track entries that have been deleted.
131 | *
132 | * The journal file is appended to as cache operations occur. The journal may
133 | * occasionally be compacted by dropping redundant lines. A temporary file named
134 | * "journal.tmp" will be used during compaction; that file should be deleted if
135 | * it exists when the cache is opened.
136 | */
137 |
138 | private final File directory;
139 | private final File journalFile;
140 | private final File journalFileTmp;
141 | private final File journalFileBackup;
142 | private final int appVersion;
143 | private long maxSize;
144 | private final int valueCount;
145 | private long size = 0;
146 | private Writer journalWriter;
147 | private final LinkedHashMap lruEntries =
148 | new LinkedHashMap(0, 0.75f, true);
149 | private int redundantOpCount;
150 |
151 | /**
152 | * To differentiate between old and current snapshots, each entry is given
153 | * a sequence number each time an edit is committed. A snapshot is stale if
154 | * its sequence number is not equal to its entry's sequence number.
155 | */
156 | private long nextSequenceNumber = 0;
157 |
158 | /** This cache uses a single background thread to evict entries. */
159 | final ThreadPoolExecutor executorService =
160 | new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(),
161 | new DiskLruCacheThreadFactory());
162 | private final Callable cleanupCallable = new Callable() {
163 | public Void call() throws Exception {
164 | synchronized (DiskLruCache.this) {
165 | if (journalWriter == null) {
166 | return null; // Closed.
167 | }
168 | trimToSize();
169 | if (journalRebuildRequired()) {
170 | rebuildJournal();
171 | redundantOpCount = 0;
172 | }
173 | }
174 | return null;
175 | }
176 | };
177 |
178 | private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
179 | this.directory = directory;
180 | this.appVersion = appVersion;
181 | this.journalFile = new File(directory, JOURNAL_FILE);
182 | this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
183 | this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
184 | this.valueCount = valueCount;
185 | this.maxSize = maxSize;
186 | }
187 |
188 | /**
189 | * Opens the cache in {@code directory}, creating a cache if none exists
190 | * there.
191 | *
192 | * @param directory a writable directory
193 | * @param valueCount the number of values per cache entry. Must be positive.
194 | * @param maxSize the maximum number of bytes this cache should use to store
195 | * @throws IOException if reading or writing the cache directory fails
196 | */
197 | public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
198 | throws IOException {
199 | if (maxSize <= 0) {
200 | throw new IllegalArgumentException("maxSize <= 0");
201 | }
202 | if (valueCount <= 0) {
203 | throw new IllegalArgumentException("valueCount <= 0");
204 | }
205 |
206 | // If a bkp file exists, use it instead.
207 | File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
208 | if (backupFile.exists()) {
209 | File journalFile = new File(directory, JOURNAL_FILE);
210 | // If journal file also exists just delete backup file.
211 | if (journalFile.exists()) {
212 | backupFile.delete();
213 | } else {
214 | renameTo(backupFile, journalFile, false);
215 | }
216 | }
217 |
218 | // Prefer to pick up where we left off.
219 | DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
220 | if (cache.journalFile.exists()) {
221 | try {
222 | cache.readJournal();
223 | cache.processJournal();
224 | return cache;
225 | } catch (IOException journalIsCorrupt) {
226 | System.out
227 | .println("DiskLruCache "
228 | + directory
229 | + " is corrupt: "
230 | + journalIsCorrupt.getMessage()
231 | + ", removing");
232 | cache.delete();
233 | }
234 | }
235 |
236 | // Create a new empty cache.
237 | directory.mkdirs();
238 | cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
239 | cache.rebuildJournal();
240 | return cache;
241 | }
242 |
243 | private void readJournal() throws IOException {
244 | StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
245 | try {
246 | String magic = reader.readLine();
247 | String version = reader.readLine();
248 | String appVersionString = reader.readLine();
249 | String valueCountString = reader.readLine();
250 | String blank = reader.readLine();
251 | if (!MAGIC.equals(magic)
252 | || !VERSION_1.equals(version)
253 | || !Integer.toString(appVersion).equals(appVersionString)
254 | || !Integer.toString(valueCount).equals(valueCountString)
255 | || !"".equals(blank)) {
256 | throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
257 | + valueCountString + ", " + blank + "]");
258 | }
259 |
260 | int lineCount = 0;
261 | while (true) {
262 | try {
263 | readJournalLine(reader.readLine());
264 | lineCount++;
265 | } catch (EOFException endOfJournal) {
266 | break;
267 | }
268 | }
269 | redundantOpCount = lineCount - lruEntries.size();
270 |
271 | // If we ended on a truncated line, rebuild the journal before appending to it.
272 | if (reader.hasUnterminatedLine()) {
273 | rebuildJournal();
274 | } else {
275 | journalWriter = new BufferedWriter(new OutputStreamWriter(
276 | new FileOutputStream(journalFile, true), Util.US_ASCII));
277 | }
278 | } finally {
279 | Util.closeQuietly(reader);
280 | }
281 | }
282 |
283 | private void readJournalLine(String line) throws IOException {
284 | int firstSpace = line.indexOf(' ');
285 | if (firstSpace == -1) {
286 | throw new IOException("unexpected journal line: " + line);
287 | }
288 |
289 | int keyBegin = firstSpace + 1;
290 | int secondSpace = line.indexOf(' ', keyBegin);
291 | final String key;
292 | if (secondSpace == -1) {
293 | key = line.substring(keyBegin);
294 | if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
295 | lruEntries.remove(key);
296 | return;
297 | }
298 | } else {
299 | key = line.substring(keyBegin, secondSpace);
300 | }
301 |
302 | Entry entry = lruEntries.get(key);
303 | if (entry == null) {
304 | entry = new Entry(key);
305 | lruEntries.put(key, entry);
306 | }
307 |
308 | if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
309 | String[] parts = line.substring(secondSpace + 1).split(" ");
310 | entry.readable = true;
311 | entry.currentEditor = null;
312 | entry.setLengths(parts);
313 | } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
314 | entry.currentEditor = new Editor(entry);
315 | } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
316 | // This work was already done by calling lruEntries.get().
317 | } else {
318 | throw new IOException("unexpected journal line: " + line);
319 | }
320 | }
321 |
322 | /**
323 | * Computes the initial size and collects garbage as a part of opening the
324 | * cache. Dirty entries are assumed to be inconsistent and will be deleted.
325 | */
326 | private void processJournal() throws IOException {
327 | deleteIfExists(journalFileTmp);
328 | for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) {
329 | Entry entry = i.next();
330 | if (entry.currentEditor == null) {
331 | for (int t = 0; t < valueCount; t++) {
332 | size += entry.lengths[t];
333 | }
334 | } else {
335 | entry.currentEditor = null;
336 | for (int t = 0; t < valueCount; t++) {
337 | deleteIfExists(entry.getCleanFile(t));
338 | deleteIfExists(entry.getDirtyFile(t));
339 | }
340 | i.remove();
341 | }
342 | }
343 | }
344 |
345 | /**
346 | * Creates a new journal that omits redundant information. This replaces the
347 | * current journal if it exists.
348 | */
349 | private synchronized void rebuildJournal() throws IOException {
350 | if (journalWriter != null) {
351 | journalWriter.close();
352 | }
353 |
354 | Writer writer = new BufferedWriter(
355 | new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
356 | try {
357 | writer.write(MAGIC);
358 | writer.write("\n");
359 | writer.write(VERSION_1);
360 | writer.write("\n");
361 | writer.write(Integer.toString(appVersion));
362 | writer.write("\n");
363 | writer.write(Integer.toString(valueCount));
364 | writer.write("\n");
365 | writer.write("\n");
366 |
367 | for (Entry entry : lruEntries.values()) {
368 | if (entry.currentEditor != null) {
369 | writer.write(DIRTY + ' ' + entry.key + '\n');
370 | } else {
371 | writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
372 | }
373 | }
374 | } finally {
375 | writer.close();
376 | }
377 |
378 | if (journalFile.exists()) {
379 | renameTo(journalFile, journalFileBackup, true);
380 | }
381 | renameTo(journalFileTmp, journalFile, false);
382 | journalFileBackup.delete();
383 |
384 | journalWriter = new BufferedWriter(
385 | new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
386 | }
387 |
388 | private static void deleteIfExists(File file) throws IOException {
389 | if (file.exists() && !file.delete()) {
390 | throw new IOException();
391 | }
392 | }
393 |
394 | private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
395 | if (deleteDestination) {
396 | deleteIfExists(to);
397 | }
398 | if (!from.renameTo(to)) {
399 | throw new IOException();
400 | }
401 | }
402 |
403 | /**
404 | * Returns a snapshot of the entry named {@code key}, or null if it doesn't
405 | * exist is not currently readable. If a value is returned, it is moved to
406 | * the head of the LRU queue.
407 | */
408 | public synchronized Value get(String key) throws IOException {
409 | checkNotClosed();
410 | Entry entry = lruEntries.get(key);
411 | if (entry == null) {
412 | return null;
413 | }
414 |
415 | if (!entry.readable) {
416 | return null;
417 | }
418 |
419 | for (File file : entry.cleanFiles) {
420 | // A file must have been deleted manually!
421 | if (!file.exists()) {
422 | return null;
423 | }
424 | }
425 |
426 | redundantOpCount++;
427 | journalWriter.append(READ);
428 | journalWriter.append(' ');
429 | journalWriter.append(key);
430 | journalWriter.append('\n');
431 | if (journalRebuildRequired()) {
432 | executorService.submit(cleanupCallable);
433 | }
434 |
435 | return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
436 | }
437 |
438 | /**
439 | * Returns an editor for the entry named {@code key}, or null if another
440 | * edit is in progress.
441 | */
442 | public Editor edit(String key) throws IOException {
443 | return edit(key, ANY_SEQUENCE_NUMBER);
444 | }
445 |
446 | private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
447 | checkNotClosed();
448 | Entry entry = lruEntries.get(key);
449 | if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
450 | || entry.sequenceNumber != expectedSequenceNumber)) {
451 | return null; // Value is stale.
452 | }
453 | if (entry == null) {
454 | entry = new Entry(key);
455 | lruEntries.put(key, entry);
456 | } else if (entry.currentEditor != null) {
457 | return null; // Another edit is in progress.
458 | }
459 |
460 | Editor editor = new Editor(entry);
461 | entry.currentEditor = editor;
462 |
463 | // Flush the journal before creating files to prevent file leaks.
464 | journalWriter.append(DIRTY);
465 | journalWriter.append(' ');
466 | journalWriter.append(key);
467 | journalWriter.append('\n');
468 | journalWriter.flush();
469 | return editor;
470 | }
471 |
472 | /** Returns the directory where this cache stores its data. */
473 | public File getDirectory() {
474 | return directory;
475 | }
476 |
477 | /**
478 | * Returns the maximum number of bytes that this cache should use to store
479 | * its data.
480 | */
481 | public synchronized long getMaxSize() {
482 | return maxSize;
483 | }
484 |
485 | /**
486 | * Changes the maximum number of bytes the cache can store and queues a job
487 | * to trim the existing store, if necessary.
488 | */
489 | public synchronized void setMaxSize(long maxSize) {
490 | this.maxSize = maxSize;
491 | executorService.submit(cleanupCallable);
492 | }
493 |
494 | /**
495 | * Returns the number of bytes currently being used to store the values in
496 | * this cache. This may be greater than the max size if a background
497 | * deletion is pending.
498 | */
499 | public synchronized long size() {
500 | return size;
501 | }
502 |
503 | private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
504 | Entry entry = editor.entry;
505 | if (entry.currentEditor != editor) {
506 | throw new IllegalStateException();
507 | }
508 |
509 | // If this edit is creating the entry for the first time, every index must have a value.
510 | if (success && !entry.readable) {
511 | for (int i = 0; i < valueCount; i++) {
512 | if (!editor.written[i]) {
513 | editor.abort();
514 | throw new IllegalStateException("Newly created entry didn't create value for index " + i);
515 | }
516 | if (!entry.getDirtyFile(i).exists()) {
517 | editor.abort();
518 | return;
519 | }
520 | }
521 | }
522 |
523 | for (int i = 0; i < valueCount; i++) {
524 | File dirty = entry.getDirtyFile(i);
525 | if (success) {
526 | if (dirty.exists()) {
527 | File clean = entry.getCleanFile(i);
528 | dirty.renameTo(clean);
529 | long oldLength = entry.lengths[i];
530 | long newLength = clean.length();
531 | entry.lengths[i] = newLength;
532 | size = size - oldLength + newLength;
533 | }
534 | } else {
535 | deleteIfExists(dirty);
536 | }
537 | }
538 |
539 | redundantOpCount++;
540 | entry.currentEditor = null;
541 | if (entry.readable | success) {
542 | entry.readable = true;
543 | journalWriter.append(CLEAN);
544 | journalWriter.append(' ');
545 | journalWriter.append(entry.key);
546 | journalWriter.append(entry.getLengths());
547 | journalWriter.append('\n');
548 |
549 | if (success) {
550 | entry.sequenceNumber = nextSequenceNumber++;
551 | }
552 | } else {
553 | lruEntries.remove(entry.key);
554 | journalWriter.append(REMOVE);
555 | journalWriter.append(' ');
556 | journalWriter.append(entry.key);
557 | journalWriter.append('\n');
558 | }
559 | journalWriter.flush();
560 |
561 | if (size > maxSize || journalRebuildRequired()) {
562 | executorService.submit(cleanupCallable);
563 | }
564 | }
565 |
566 | /**
567 | * We only rebuild the journal when it will halve the size of the journal
568 | * and eliminate at least 2000 ops.
569 | */
570 | private boolean journalRebuildRequired() {
571 | final int redundantOpCompactThreshold = 2000;
572 | return redundantOpCount >= redundantOpCompactThreshold //
573 | && redundantOpCount >= lruEntries.size();
574 | }
575 |
576 | /**
577 | * Drops the entry for {@code key} if it exists and can be removed. Entries
578 | * actively being edited cannot be removed.
579 | *
580 | * @return true if an entry was removed.
581 | */
582 | public synchronized boolean remove(String key) throws IOException {
583 | checkNotClosed();
584 | Entry entry = lruEntries.get(key);
585 | if (entry == null || entry.currentEditor != null) {
586 | return false;
587 | }
588 |
589 | for (int i = 0; i < valueCount; i++) {
590 | File file = entry.getCleanFile(i);
591 | if (file.exists() && !file.delete()) {
592 | throw new IOException("failed to delete " + file);
593 | }
594 | size -= entry.lengths[i];
595 | entry.lengths[i] = 0;
596 | }
597 |
598 | redundantOpCount++;
599 | journalWriter.append(REMOVE);
600 | journalWriter.append(' ');
601 | journalWriter.append(key);
602 | journalWriter.append('\n');
603 |
604 | lruEntries.remove(key);
605 |
606 | if (journalRebuildRequired()) {
607 | executorService.submit(cleanupCallable);
608 | }
609 |
610 | return true;
611 | }
612 |
613 | /** Returns true if this cache has been closed. */
614 | public synchronized boolean isClosed() {
615 | return journalWriter == null;
616 | }
617 |
618 | private void checkNotClosed() {
619 | if (journalWriter == null) {
620 | throw new IllegalStateException("cache is closed");
621 | }
622 | }
623 |
624 | /** Force buffered operations to the filesystem. */
625 | public synchronized void flush() throws IOException {
626 | checkNotClosed();
627 | trimToSize();
628 | journalWriter.flush();
629 | }
630 |
631 | /** Closes this cache. Stored values will remain on the filesystem. */
632 | public synchronized void close() throws IOException {
633 | if (journalWriter == null) {
634 | return; // Already closed.
635 | }
636 | for (Entry entry : new ArrayList(lruEntries.values())) {
637 | if (entry.currentEditor != null) {
638 | entry.currentEditor.abort();
639 | }
640 | }
641 | trimToSize();
642 | journalWriter.close();
643 | journalWriter = null;
644 | }
645 |
646 | private void trimToSize() throws IOException {
647 | while (size > maxSize) {
648 | Map.Entry toEvict = lruEntries.entrySet().iterator().next();
649 | remove(toEvict.getKey());
650 | }
651 | }
652 |
653 | /**
654 | * Closes the cache and deletes all of its stored values. This will delete
655 | * all files in the cache directory including files that weren't created by
656 | * the cache.
657 | */
658 | public void delete() throws IOException {
659 | close();
660 | Util.deleteContents(directory);
661 | }
662 |
663 | private static String inputStreamToString(InputStream in) throws IOException {
664 | return Util.readFully(new InputStreamReader(in, Util.UTF_8));
665 | }
666 |
667 | /** A snapshot of the values for an entry. */
668 | public final class Value {
669 | private final String key;
670 | private final long sequenceNumber;
671 | private final long[] lengths;
672 | private final File[] files;
673 |
674 | private Value(String key, long sequenceNumber, File[] files, long[] lengths) {
675 | this.key = key;
676 | this.sequenceNumber = sequenceNumber;
677 | this.files = files;
678 | this.lengths = lengths;
679 | }
680 |
681 | /**
682 | * Returns an editor for this snapshot's entry, or null if either the
683 | * entry has changed since this snapshot was created or if another edit
684 | * is in progress.
685 | */
686 | public Editor edit() throws IOException {
687 | return DiskLruCache.this.edit(key, sequenceNumber);
688 | }
689 |
690 | public File getFile(int index) {
691 | return files[index];
692 | }
693 |
694 | /** Returns the string value for {@code index}. */
695 | public String getString(int index) throws IOException {
696 | InputStream is = new FileInputStream(files[index]);
697 | return inputStreamToString(is);
698 | }
699 |
700 | /** Returns the byte length of the value for {@code index}. */
701 | public long getLength(int index) {
702 | return lengths[index];
703 | }
704 | }
705 |
706 | /** Edits the values for an entry. */
707 | public final class Editor {
708 | private final Entry entry;
709 | private final boolean[] written;
710 | private boolean committed;
711 |
712 | private Editor(Entry entry) {
713 | this.entry = entry;
714 | this.written = (entry.readable) ? null : new boolean[valueCount];
715 | }
716 |
717 | /**
718 | * Returns an unbuffered input stream to read the last committed value,
719 | * or null if no value has been committed.
720 | */
721 | private InputStream newInputStream(int index) throws IOException {
722 | synchronized (DiskLruCache.this) {
723 | if (entry.currentEditor != this) {
724 | throw new IllegalStateException();
725 | }
726 | if (!entry.readable) {
727 | return null;
728 | }
729 | try {
730 | return new FileInputStream(entry.getCleanFile(index));
731 | } catch (FileNotFoundException e) {
732 | return null;
733 | }
734 | }
735 | }
736 |
737 | /**
738 | * Returns the last committed value as a string, or null if no value
739 | * has been committed.
740 | */
741 | public String getString(int index) throws IOException {
742 | InputStream in = newInputStream(index);
743 | return in != null ? inputStreamToString(in) : null;
744 | }
745 |
746 | public File getFile(int index) throws IOException {
747 | synchronized (DiskLruCache.this) {
748 | if (entry.currentEditor != this) {
749 | throw new IllegalStateException();
750 | }
751 | if (!entry.readable) {
752 | written[index] = true;
753 | }
754 | File dirtyFile = entry.getDirtyFile(index);
755 | if (!directory.exists()) {
756 | directory.mkdirs();
757 | }
758 | return dirtyFile;
759 | }
760 | }
761 |
762 | /** Sets the value at {@code index} to {@code value}. */
763 | public void set(int index, String value) throws IOException {
764 | Writer writer = null;
765 | try {
766 | OutputStream os = new FileOutputStream(getFile(index));
767 | writer = new OutputStreamWriter(os, Util.UTF_8);
768 | writer.write(value);
769 | } finally {
770 | Util.closeQuietly(writer);
771 | }
772 | }
773 |
774 | /**
775 | * Commits this edit so it is visible to readers. This releases the
776 | * edit lock so another edit may be started on the same key.
777 | */
778 | public void commit() throws IOException {
779 | // The object using this Editor must catch and handle any errors
780 | // during the write. If there is an error and they call commit
781 | // anyway, we will assume whatever they managed to write was valid.
782 | // Normally they should call abort.
783 | completeEdit(this, true);
784 | committed = true;
785 | }
786 |
787 | /**
788 | * Aborts this edit. This releases the edit lock so another edit may be
789 | * started on the same key.
790 | */
791 | public void abort() throws IOException {
792 | completeEdit(this, false);
793 | }
794 |
795 | public void abortUnlessCommitted() {
796 | if (!committed) {
797 | try {
798 | abort();
799 | } catch (IOException ignored) {
800 | }
801 | }
802 | }
803 | }
804 |
805 | private final class Entry {
806 | private final String key;
807 |
808 | /** Lengths of this entry's files. */
809 | private final long[] lengths;
810 |
811 | /** Memoized File objects for this entry to avoid char[] allocations. */
812 | File[] cleanFiles;
813 | File[] dirtyFiles;
814 |
815 | /** True if this entry has ever been published. */
816 | private boolean readable;
817 |
818 | /** The ongoing edit or null if this entry is not being edited. */
819 | private Editor currentEditor;
820 |
821 | /** The sequence number of the most recently committed edit to this entry. */
822 | private long sequenceNumber;
823 |
824 | private Entry(String key) {
825 | this.key = key;
826 | this.lengths = new long[valueCount];
827 | cleanFiles = new File[valueCount];
828 | dirtyFiles = new File[valueCount];
829 |
830 | // The names are repetitive so re-use the same builder to avoid allocations.
831 | StringBuilder fileBuilder = new StringBuilder(key).append('.');
832 | int truncateTo = fileBuilder.length();
833 | for (int i = 0; i < valueCount; i++) {
834 | fileBuilder.append(i);
835 | cleanFiles[i] = new File(directory, fileBuilder.toString());
836 | fileBuilder.append(".tmp");
837 | dirtyFiles[i] = new File(directory, fileBuilder.toString());
838 | fileBuilder.setLength(truncateTo);
839 | }
840 | }
841 |
842 | public String getLengths() throws IOException {
843 | StringBuilder result = new StringBuilder();
844 | for (long size : lengths) {
845 | result.append(' ').append(size);
846 | }
847 | return result.toString();
848 | }
849 |
850 | /** Set lengths using decimal numbers like "10123". */
851 | private void setLengths(String[] strings) throws IOException {
852 | if (strings.length != valueCount) {
853 | throw invalidLengths(strings);
854 | }
855 |
856 | try {
857 | for (int i = 0; i < strings.length; i++) {
858 | lengths[i] = Long.parseLong(strings[i]);
859 | }
860 | } catch (NumberFormatException e) {
861 | throw invalidLengths(strings);
862 | }
863 | }
864 |
865 | private IOException invalidLengths(String[] strings) throws IOException {
866 | throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
867 | }
868 |
869 | public File getCleanFile(int i) {
870 | return cleanFiles[i];
871 | }
872 |
873 | public File getDirtyFile(int i) {
874 | return dirtyFiles[i];
875 | }
876 | }
877 |
878 | /**
879 | * A {@link java.util.concurrent.ThreadFactory} that builds a thread with a specific thread name
880 | * and with minimum priority.
881 | */
882 | private static final class DiskLruCacheThreadFactory implements ThreadFactory {
883 | @Override
884 | public synchronized Thread newThread(Runnable runnable) {
885 | Thread result = new Thread(runnable, "glide-disk-lru-cache-thread");
886 | result.setPriority(Thread.MIN_PRIORITY);
887 | return result;
888 | }
889 | }
890 | }
891 |
--------------------------------------------------------------------------------
/src/test/java/com/bumptech/glide/disklrucache/DiskLruCacheTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.bumptech.glide.disklrucache;
18 |
19 | import static com.bumptech.glide.disklrucache.DiskLruCache.JOURNAL_FILE;
20 | import static com.bumptech.glide.disklrucache.DiskLruCache.JOURNAL_FILE_BACKUP;
21 | import static com.bumptech.glide.disklrucache.DiskLruCache.MAGIC;
22 | import static com.bumptech.glide.disklrucache.DiskLruCache.VERSION_1;
23 | import static org.fest.assertions.api.Assertions.assertThat;
24 | import static org.hamcrest.core.IsNot.not;
25 | import static org.junit.Assume.assumeThat;
26 |
27 | import org.apache.commons.io.FileUtils;
28 | import org.hamcrest.core.StringStartsWith;
29 | import org.junit.After;
30 | import org.junit.Assert;
31 | import org.junit.Before;
32 | import org.junit.BeforeClass;
33 | import org.junit.Rule;
34 | import org.junit.Test;
35 | import org.junit.rules.TemporaryFolder;
36 | import org.junit.runner.RunWith;
37 | import org.junit.runners.JUnit4;
38 |
39 | import java.io.BufferedReader;
40 | import java.io.File;
41 | import java.io.FileReader;
42 | import java.io.FileWriter;
43 | import java.io.Reader;
44 | import java.io.StringWriter;
45 | import java.io.Writer;
46 | import java.util.ArrayList;
47 | import java.util.Arrays;
48 | import java.util.List;
49 | import java.util.concurrent.TimeUnit;
50 |
51 | @RunWith(JUnit4.class)
52 | public final class DiskLruCacheTest {
53 | private final int appVersion = 100;
54 | private File cacheDir;
55 | private File journalFile;
56 | private File journalBkpFile;
57 | private DiskLruCache cache;
58 |
59 | @Rule public TemporaryFolder tempDir = new TemporaryFolder();
60 |
61 | @BeforeClass
62 | public static void setUpClass() {
63 | assumeThat(System.getProperty("os.name"), not(StringStartsWith.startsWith("Windows")));
64 | }
65 |
66 | @Before public void setUp() throws Exception {
67 | cacheDir = tempDir.newFolder("DiskLruCacheTest");
68 | journalFile = new File(cacheDir, JOURNAL_FILE);
69 | journalBkpFile = new File(cacheDir, JOURNAL_FILE_BACKUP);
70 | for (File file : cacheDir.listFiles()) {
71 | file.delete();
72 | }
73 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
74 | }
75 |
76 | @After public void tearDown() throws Exception {
77 | cache.close();
78 | }
79 |
80 | @Test public void emptyCache() throws Exception {
81 | cache.close();
82 | assertJournalEquals();
83 | }
84 |
85 | @Test public void writeAndReadEntry() throws Exception {
86 | DiskLruCache.Editor creator = cache.edit("k1");
87 | creator.set(0, "ABC");
88 | creator.set(1, "DE");
89 | assertThat(creator.getString(0)).isNull();
90 | assertThat(creator.getString(1)).isNull();
91 | creator.commit();
92 |
93 | DiskLruCache.Value value = cache.get("k1");
94 | assertThat(value.getString(0)).isEqualTo("ABC");
95 | assertThat(value.getLength(0)).isEqualTo(3);
96 | assertThat(value.getString(1)).isEqualTo("DE");
97 | assertThat(value.getLength(1)).isEqualTo(2);
98 | }
99 |
100 | @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
101 | DiskLruCache.Editor creator = cache.edit("k1");
102 | creator.set(0, "A");
103 | creator.set(1, "B");
104 | creator.commit();
105 | cache.close();
106 |
107 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
108 | DiskLruCache.Value value = cache.get("k1");
109 | assertThat(value.getString(0)).isEqualTo("A");
110 | assertThat(value.getLength(0)).isEqualTo(1);
111 | assertThat(value.getString(1)).isEqualTo("B");
112 | assertThat(value.getLength(1)).isEqualTo(1);
113 | }
114 |
115 | @Test public void readAndWriteEntryWithoutProperClose() throws Exception {
116 | DiskLruCache.Editor creator = cache.edit("k1");
117 | creator.set(0, "A");
118 | creator.set(1, "B");
119 | creator.commit();
120 |
121 | // Simulate a dirty close of 'cache' by opening the cache directory again.
122 | DiskLruCache cache2 = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
123 | DiskLruCache.Value value = cache2.get("k1");
124 | assertThat(value.getString(0)).isEqualTo("A");
125 | assertThat(value.getLength(0)).isEqualTo(1);
126 | assertThat(value.getString(1)).isEqualTo("B");
127 | assertThat(value.getLength(1)).isEqualTo(1);
128 | cache2.close();
129 | }
130 |
131 | @Test public void journalWithEditAndPublish() throws Exception {
132 | DiskLruCache.Editor creator = cache.edit("k1");
133 | assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
134 | creator.set(0, "AB");
135 | creator.set(1, "C");
136 | creator.commit();
137 | cache.close();
138 | assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
139 | }
140 |
141 | @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
142 | DiskLruCache.Editor creator = cache.edit("k1");
143 | assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
144 | creator.set(0, "AB");
145 | creator.set(1, "C");
146 | creator.abort();
147 | cache.close();
148 | assertJournalEquals("DIRTY k1", "REMOVE k1");
149 | }
150 |
151 | @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
152 | cache.edit("k1");
153 | cache.close();
154 | assertJournalEquals("DIRTY k1", "REMOVE k1");
155 | }
156 |
157 | @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
158 | DiskLruCache.Editor creator = cache.edit("k1");
159 | assertThat(cache.get("k1")).isNull();
160 | creator.set(0, "A");
161 | creator.set(1, "BC");
162 | creator.commit();
163 | cache.close();
164 | assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
165 | }
166 |
167 | @Test public void journalWithEditAndPublishAndRead() throws Exception {
168 | DiskLruCache.Editor k1Creator = cache.edit("k1");
169 | k1Creator.set(0, "AB");
170 | k1Creator.set(1, "C");
171 | k1Creator.commit();
172 | DiskLruCache.Editor k2Creator = cache.edit("k2");
173 | k2Creator.set(0, "DEF");
174 | k2Creator.set(1, "G");
175 | k2Creator.commit();
176 | DiskLruCache.Value k1Value = cache.get("k1");
177 | cache.close();
178 | assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
179 | }
180 |
181 | @Test public void cannotOperateOnEditAfterPublish() throws Exception {
182 | DiskLruCache.Editor editor = cache.edit("k1");
183 | editor.set(0, "A");
184 | editor.set(1, "B");
185 | editor.commit();
186 | assertInoperable(editor);
187 | }
188 |
189 | @Test public void cannotOperateOnEditAfterRevert() throws Exception {
190 | DiskLruCache.Editor editor = cache.edit("k1");
191 | editor.set(0, "A");
192 | editor.set(1, "B");
193 | editor.abort();
194 | assertInoperable(editor);
195 | }
196 |
197 | @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
198 | DiskLruCache.Editor editor = cache.edit("k1");
199 | editor.set(0, "ABC");
200 | editor.set(1, "B");
201 | editor.commit();
202 | File k1 = getCleanFile("k1", 0);
203 | assertThat(readFile(k1)).isEqualTo("ABC");
204 | cache.remove("k1");
205 | assertThat(k1.exists()).isFalse();
206 | }
207 |
208 | @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
209 | cache.close();
210 | File cleanFile0 = getCleanFile("k1", 0);
211 | File cleanFile1 = getCleanFile("k1", 1);
212 | File dirtyFile0 = getDirtyFile("k1", 0);
213 | File dirtyFile1 = getDirtyFile("k1", 1);
214 | writeFile(cleanFile0, "A");
215 | writeFile(cleanFile1, "B");
216 | writeFile(dirtyFile0, "C");
217 | writeFile(dirtyFile1, "D");
218 | createJournal("CLEAN k1 1 1", "DIRTY k1");
219 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
220 | assertThat(cleanFile0.exists()).isFalse();
221 | assertThat(cleanFile1.exists()).isFalse();
222 | assertThat(dirtyFile0.exists()).isFalse();
223 | assertThat(dirtyFile1.exists()).isFalse();
224 | assertThat(cache.get("k1")).isNull();
225 | }
226 |
227 | @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
228 | cache.close();
229 | generateSomeGarbageFiles();
230 | createJournalWithHeader(MAGIC, "0", "100", "2", "");
231 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
232 | assertGarbageFilesAllDeleted();
233 | }
234 |
235 | @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
236 | cache.close();
237 | generateSomeGarbageFiles();
238 | createJournalWithHeader(MAGIC, "1", "101", "2", "");
239 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
240 | assertGarbageFilesAllDeleted();
241 | }
242 |
243 | @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
244 | cache.close();
245 | generateSomeGarbageFiles();
246 | createJournalWithHeader(MAGIC, "1", "100", "1", "");
247 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
248 | assertGarbageFilesAllDeleted();
249 | }
250 |
251 | @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
252 | cache.close();
253 | generateSomeGarbageFiles();
254 | createJournalWithHeader(MAGIC, "1", "100", "2", "x");
255 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
256 | assertGarbageFilesAllDeleted();
257 | }
258 |
259 | @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
260 | cache.close();
261 | generateSomeGarbageFiles();
262 | createJournal("CLEAN k1 1 1", "BOGUS");
263 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
264 | assertGarbageFilesAllDeleted();
265 | assertThat(cache.get("k1")).isNull();
266 | }
267 |
268 | @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
269 | cache.close();
270 | generateSomeGarbageFiles();
271 | createJournal("CLEAN k1 0000x001 1");
272 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
273 | assertGarbageFilesAllDeleted();
274 | assertThat(cache.get("k1")).isNull();
275 | }
276 |
277 | @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
278 | cache.close();
279 | writeFile(getCleanFile("k1", 0), "A");
280 | writeFile(getCleanFile("k1", 1), "B");
281 | Writer writer = new FileWriter(journalFile);
282 | writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
283 | writer.close();
284 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
285 | assertThat(cache.get("k1")).isNull();
286 |
287 | // The journal is not corrupt when editing after a truncated line.
288 | set("k1", "C", "D");
289 | cache.close();
290 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
291 | assertValue("k1", "C", "D");
292 | }
293 |
294 | @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
295 | cache.close();
296 | generateSomeGarbageFiles();
297 | createJournal("CLEAN k1 1 1 1");
298 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
299 | assertGarbageFilesAllDeleted();
300 | assertThat(cache.get("k1")).isNull();
301 | }
302 |
303 | @Test public void nullKeyThrows() throws Exception {
304 | try {
305 | cache.edit(null);
306 | Assert.fail();
307 | } catch (NullPointerException expected) {
308 | }
309 | }
310 |
311 | @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
312 | DiskLruCache.Editor creator = cache.edit("k1");
313 | creator.set(1, "A");
314 | try {
315 | creator.commit();
316 | Assert.fail();
317 | } catch (IllegalStateException expected) {
318 | }
319 |
320 | assertThat(getCleanFile("k1", 0).exists()).isFalse();
321 | assertThat(getCleanFile("k1", 1).exists()).isFalse();
322 | assertThat(getDirtyFile("k1", 0).exists()).isFalse();
323 | assertThat(getDirtyFile("k1", 1).exists()).isFalse();
324 | assertThat(cache.get("k1")).isNull();
325 |
326 | DiskLruCache.Editor creator2 = cache.edit("k1");
327 | creator2.set(0, "B");
328 | creator2.set(1, "C");
329 | creator2.commit();
330 | }
331 |
332 | @Test public void revertWithTooFewValues() throws Exception {
333 | DiskLruCache.Editor creator = cache.edit("k1");
334 | creator.set(1, "A");
335 | creator.abort();
336 | assertThat(getCleanFile("k1", 0).exists()).isFalse();
337 | assertThat(getCleanFile("k1", 1).exists()).isFalse();
338 | assertThat(getDirtyFile("k1", 0).exists()).isFalse();
339 | assertThat(getDirtyFile("k1", 1).exists()).isFalse();
340 | assertThat(cache.get("k1")).isNull();
341 | }
342 |
343 | @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
344 | DiskLruCache.Editor creator = cache.edit("k1");
345 | creator.set(0, "A");
346 | creator.set(1, "B");
347 | creator.commit();
348 |
349 | DiskLruCache.Editor updater = cache.edit("k1");
350 | updater.set(0, "C");
351 | updater.commit();
352 |
353 | DiskLruCache.Value value = cache.get("k1");
354 | assertThat(value.getString(0)).isEqualTo("C");
355 | assertThat(value.getLength(0)).isEqualTo(1);
356 | assertThat(value.getString(1)).isEqualTo("B");
357 | assertThat(value.getLength(1)).isEqualTo(1);
358 | }
359 |
360 | @Test public void growMaxSize() throws Exception {
361 | cache.close();
362 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
363 | set("a", "a", "aaa"); // size 4
364 | set("b", "bb", "bbbb"); // size 6
365 | cache.setMaxSize(20);
366 | set("c", "c", "c"); // size 12
367 | assertThat(cache.size()).isEqualTo(12);
368 | }
369 |
370 | @Test public void shrinkMaxSizeEvicts() throws Exception {
371 | cache.close();
372 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 20);
373 | set("a", "a", "aaa"); // size 4
374 | set("b", "bb", "bbbb"); // size 6
375 | set("c", "c", "c"); // size 12
376 | cache.setMaxSize(10);
377 | cache.executorService.shutdown();
378 | cache.executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
379 | assertThat(cache.size()).isEqualTo(8 /* 12 - 4 */);
380 | }
381 |
382 | @Test public void evictOnInsert() throws Exception {
383 | cache.close();
384 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
385 |
386 | set("a", "a", "aaa"); // size 4
387 | set("b", "bb", "bbbb"); // size 6
388 | assertThat(cache.size()).isEqualTo(10);
389 |
390 | // Cause the size to grow to 12 should evict 'A'.
391 | set("c", "c", "c");
392 | cache.flush();
393 | assertThat(cache.size()).isEqualTo(8);
394 | assertAbsent("a");
395 | assertValue("b", "bb", "bbbb");
396 | assertValue("c", "c", "c");
397 |
398 | // Causing the size to grow to 10 should evict nothing.
399 | set("d", "d", "d");
400 | cache.flush();
401 | assertThat(cache.size()).isEqualTo(10);
402 | assertAbsent("a");
403 | assertValue("b", "bb", "bbbb");
404 | assertValue("c", "c", "c");
405 | assertValue("d", "d", "d");
406 |
407 | // Causing the size to grow to 18 should evict 'B' and 'C'.
408 | set("e", "eeee", "eeee");
409 | cache.flush();
410 | assertThat(cache.size()).isEqualTo(10);
411 | assertAbsent("a");
412 | assertAbsent("b");
413 | assertAbsent("c");
414 | assertValue("d", "d", "d");
415 | assertValue("e", "eeee", "eeee");
416 | }
417 |
418 | @Test public void evictOnUpdate() throws Exception {
419 | cache.close();
420 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
421 |
422 | set("a", "a", "aa"); // size 3
423 | set("b", "b", "bb"); // size 3
424 | set("c", "c", "cc"); // size 3
425 | assertThat(cache.size()).isEqualTo(9);
426 |
427 | // Causing the size to grow to 11 should evict 'A'.
428 | set("b", "b", "bbbb");
429 | cache.flush();
430 | assertThat(cache.size()).isEqualTo(8);
431 | assertAbsent("a");
432 | assertValue("b", "b", "bbbb");
433 | assertValue("c", "c", "cc");
434 | }
435 |
436 | @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
437 | cache.close();
438 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
439 | set("a", "a", "a");
440 | set("b", "b", "b");
441 | set("c", "c", "c");
442 | set("d", "d", "d");
443 | set("e", "e", "e");
444 | cache.get("b"); // 'B' is now least recently used.
445 |
446 | // Causing the size to grow to 12 should evict 'A'.
447 | set("f", "f", "f");
448 | // Causing the size to grow to 12 should evict 'C'.
449 | set("g", "g", "g");
450 | cache.flush();
451 | assertThat(cache.size()).isEqualTo(10);
452 | assertAbsent("a");
453 | assertValue("b", "b", "b");
454 | assertAbsent("c");
455 | assertValue("d", "d", "d");
456 | assertValue("e", "e", "e");
457 | assertValue("f", "f", "f");
458 | }
459 |
460 | @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
461 | set("a", "a", "a");
462 | set("b", "b", "b");
463 | set("c", "c", "c");
464 | set("d", "d", "d");
465 | set("e", "e", "e");
466 | set("f", "f", "f");
467 | cache.get("b"); // 'B' is now least recently used.
468 | assertThat(cache.size()).isEqualTo(12);
469 | cache.close();
470 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
471 |
472 | set("g", "g", "g");
473 | cache.flush();
474 | assertThat(cache.size()).isEqualTo(10);
475 | assertAbsent("a");
476 | assertValue("b", "b", "b");
477 | assertAbsent("c");
478 | assertValue("d", "d", "d");
479 | assertValue("e", "e", "e");
480 | assertValue("f", "f", "f");
481 | assertValue("g", "g", "g");
482 | }
483 |
484 | @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
485 | cache.close();
486 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
487 | set("a", "aaaaa", "aaaaaa"); // size=11
488 | cache.flush();
489 | assertAbsent("a");
490 | }
491 |
492 | @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
493 | cache.close();
494 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
495 | set("a", "aaaaaaaaaaa", "a"); // size=12
496 | cache.flush();
497 | assertAbsent("a");
498 | }
499 |
500 | @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
501 | try {
502 | DiskLruCache.open(cacheDir, appVersion, 2, 0);
503 | Assert.fail();
504 | } catch (IllegalArgumentException expected) {
505 | }
506 | }
507 |
508 | @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
509 | try {
510 | DiskLruCache.open(cacheDir, appVersion, 0, 10);
511 | Assert.fail();
512 | } catch (IllegalArgumentException expected) {
513 | }
514 | }
515 |
516 | @Test public void removeAbsentElement() throws Exception {
517 | cache.remove("a");
518 | }
519 |
520 | @Test public void readingTheSameFileMultipleTimes() throws Exception {
521 | set("a", "a", "b");
522 | DiskLruCache.Value value = cache.get("a");
523 | assertThat(value.getFile(0)).isSameAs(value.getFile(0));
524 | }
525 |
526 | @Test public void rebuildJournalOnRepeatedReads() throws Exception {
527 | set("a", "a", "a");
528 | set("b", "b", "b");
529 | long lastJournalLength = 0;
530 | while (true) {
531 | long journalLength = journalFile.length();
532 | assertValue("a", "a", "a");
533 | assertValue("b", "b", "b");
534 | if (journalLength < lastJournalLength) {
535 | System.out
536 | .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
537 | journalLength);
538 | break; // Test passed!
539 | }
540 | lastJournalLength = journalLength;
541 | }
542 | }
543 |
544 | @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
545 | long lastJournalLength = 0;
546 | while (true) {
547 | long journalLength = journalFile.length();
548 | set("a", "a", "a");
549 | set("b", "b", "b");
550 | if (journalLength < lastJournalLength) {
551 | System.out
552 | .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
553 | journalLength);
554 | break;
555 | }
556 | lastJournalLength = journalLength;
557 | }
558 |
559 | // Sanity check that a rebuilt journal behaves normally.
560 | assertValue("a", "a", "a");
561 | assertValue("b", "b", "b");
562 | }
563 |
564 | /** @see Issue #28 */
565 | @Test public void rebuildJournalOnRepeatedReadsWithOpenAndClose() throws Exception {
566 | set("a", "a", "a");
567 | set("b", "b", "b");
568 | long lastJournalLength = 0;
569 | while (true) {
570 | long journalLength = journalFile.length();
571 | assertValue("a", "a", "a");
572 | assertValue("b", "b", "b");
573 | cache.close();
574 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
575 | if (journalLength < lastJournalLength) {
576 | System.out
577 | .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
578 | journalLength);
579 | break; // Test passed!
580 | }
581 | lastJournalLength = journalLength;
582 | }
583 | }
584 |
585 | /** @see Issue #28 */
586 | @Test public void rebuildJournalOnRepeatedEditsWithOpenAndClose() throws Exception {
587 | long lastJournalLength = 0;
588 | while (true) {
589 | long journalLength = journalFile.length();
590 | set("a", "a", "a");
591 | set("b", "b", "b");
592 | cache.close();
593 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
594 | if (journalLength < lastJournalLength) {
595 | System.out
596 | .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
597 | journalLength);
598 | break;
599 | }
600 | lastJournalLength = journalLength;
601 | }
602 | }
603 |
604 | @Test public void restoreBackupFile() throws Exception {
605 | DiskLruCache.Editor creator = cache.edit("k1");
606 | creator.set(0, "ABC");
607 | creator.set(1, "DE");
608 | creator.commit();
609 | cache.close();
610 |
611 | assertThat(journalFile.renameTo(journalBkpFile)).isTrue();
612 | assertThat(journalFile.exists()).isFalse();
613 |
614 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
615 |
616 | DiskLruCache.Value value = cache.get("k1");
617 | assertThat(value.getString(0)).isEqualTo("ABC");
618 | assertThat(value.getLength(0)).isEqualTo(3);
619 | assertThat(value.getString(1)).isEqualTo("DE");
620 | assertThat(value.getLength(1)).isEqualTo(2);
621 |
622 | assertThat(journalBkpFile.exists()).isFalse();
623 | assertThat(journalFile.exists()).isTrue();
624 | }
625 |
626 | @Test public void journalFileIsPreferredOverBackupFile() throws Exception {
627 | DiskLruCache.Editor creator = cache.edit("k1");
628 | creator.set(0, "ABC");
629 | creator.set(1, "DE");
630 | creator.commit();
631 | cache.flush();
632 |
633 | FileUtils.copyFile(journalFile, journalBkpFile);
634 |
635 | creator = cache.edit("k2");
636 | creator.set(0, "F");
637 | creator.set(1, "GH");
638 | creator.commit();
639 | cache.close();
640 |
641 | assertThat(journalFile.exists()).isTrue();
642 | assertThat(journalBkpFile.exists()).isTrue();
643 |
644 | cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
645 |
646 | DiskLruCache.Value valueA = cache.get("k1");
647 | assertThat(valueA.getString(0)).isEqualTo("ABC");
648 | assertThat(valueA.getLength(0)).isEqualTo(3);
649 | assertThat(valueA.getString(1)).isEqualTo("DE");
650 | assertThat(valueA.getLength(1)).isEqualTo(2);
651 |
652 | DiskLruCache.Value valueB = cache.get("k2");
653 | assertThat(valueB.getString(0)).isEqualTo("F");
654 | assertThat(valueB.getLength(0)).isEqualTo(1);
655 | assertThat(valueB.getString(1)).isEqualTo("GH");
656 | assertThat(valueB.getLength(1)).isEqualTo(2);
657 |
658 | assertThat(journalBkpFile.exists()).isFalse();
659 | assertThat(journalFile.exists()).isTrue();
660 | }
661 |
662 | @Test public void openCreatesDirectoryIfNecessary() throws Exception {
663 | cache.close();
664 | File dir = tempDir.newFolder("testOpenCreatesDirectoryIfNecessary");
665 | cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE);
666 | set("a", "a", "a");
667 | assertThat(new File(dir, "a.0").exists()).isTrue();
668 | assertThat(new File(dir, "a.1").exists()).isTrue();
669 | assertThat(new File(dir, "journal").exists()).isTrue();
670 | }
671 |
672 | @Test public void fileDeletedExternally() throws Exception {
673 | set("a", "a", "a");
674 | getCleanFile("a", 1).delete();
675 | assertThat(cache.get("a")).isNull();
676 | }
677 |
678 | @Test public void editSameVersion() throws Exception {
679 | set("a", "a", "a");
680 | DiskLruCache.Value value = cache.get("a");
681 | DiskLruCache.Editor editor = value.edit();
682 | editor.set(1, "a2");
683 | editor.commit();
684 | assertValue("a", "a", "a2");
685 | }
686 |
687 | @Test public void editSnapshotAfterChangeAborted() throws Exception {
688 | set("a", "a", "a");
689 | DiskLruCache.Value value = cache.get("a");
690 | DiskLruCache.Editor toAbort = value.edit();
691 | toAbort.set(0, "b");
692 | toAbort.abort();
693 | DiskLruCache.Editor editor = value.edit();
694 | editor.set(1, "a2");
695 | editor.commit();
696 | assertValue("a", "a", "a2");
697 | }
698 |
699 | @Test public void editSnapshotAfterChangeCommitted() throws Exception {
700 | set("a", "a", "a");
701 | DiskLruCache.Value value = cache.get("a");
702 | DiskLruCache.Editor toAbort = value.edit();
703 | toAbort.set(0, "b");
704 | toAbort.commit();
705 | assertThat(value.edit()).isNull();
706 | }
707 |
708 | @Test public void editSinceEvicted() throws Exception {
709 | cache.close();
710 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
711 | set("a", "aa", "aaa"); // size 5
712 | DiskLruCache.Value value = cache.get("a");
713 | set("b", "bb", "bbb"); // size 5
714 | set("c", "cc", "ccc"); // size 5; will evict 'A'
715 | cache.flush();
716 | assertThat(value.edit()).isNull();
717 | }
718 |
719 | @Test public void editSinceEvictedAndRecreated() throws Exception {
720 | cache.close();
721 | cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
722 | set("a", "aa", "aaa"); // size 5
723 | DiskLruCache.Value value = cache.get("a");
724 | set("b", "bb", "bbb"); // size 5
725 | set("c", "cc", "ccc"); // size 5; will evict 'A'
726 | set("a", "a", "aaaa"); // size 5; will evict 'B'
727 | cache.flush();
728 | assertThat(value.edit()).isNull();
729 | }
730 |
731 | /** @see Issue #2 */
732 | @Test public void aggressiveClearingHandlesWrite() throws Exception {
733 | FileUtils.deleteDirectory(cacheDir);
734 | set("a", "a", "a");
735 | assertValue("a", "a", "a");
736 | }
737 |
738 | /** @see Issue #2 */
739 | @Test public void aggressiveClearingHandlesEdit() throws Exception {
740 | set("a", "a", "a");
741 | DiskLruCache.Editor a = cache.get("a").edit();
742 | FileUtils.deleteDirectory(cacheDir);
743 | a.set(1, "a2");
744 | a.commit();
745 | }
746 |
747 | @Test public void removeHandlesMissingFile() throws Exception {
748 | set("a", "a", "a");
749 | getCleanFile("a", 0).delete();
750 | cache.remove("a");
751 | }
752 |
753 | /** @see Issue #2 */
754 | @Test public void aggressiveClearingHandlesPartialEdit() throws Exception {
755 | set("a", "a", "a");
756 | set("b", "b", "b");
757 | DiskLruCache.Editor a = cache.get("a").edit();
758 | a.set(0, "a1");
759 | FileUtils.deleteDirectory(cacheDir);
760 | a.set(1, "a2");
761 | a.commit();
762 | assertThat(cache.get("a")).isNull();
763 | }
764 |
765 | /** @see Issue #2 */
766 | @Test public void aggressiveClearingHandlesRead() throws Exception {
767 | FileUtils.deleteDirectory(cacheDir);
768 | assertThat(cache.get("a")).isNull();
769 | }
770 |
771 | private void assertJournalEquals(String... expectedBodyLines) throws Exception {
772 | List expectedLines = new ArrayList();
773 | expectedLines.add(MAGIC);
774 | expectedLines.add(VERSION_1);
775 | expectedLines.add("100");
776 | expectedLines.add("2");
777 | expectedLines.add("");
778 | expectedLines.addAll(Arrays.asList(expectedBodyLines));
779 | assertThat(readJournalLines()).isEqualTo(expectedLines);
780 | }
781 |
782 | private void createJournal(String... bodyLines) throws Exception {
783 | createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
784 | }
785 |
786 | private void createJournalWithHeader(String magic, String version, String appVersion,
787 | String valueCount, String blank, String... bodyLines) throws Exception {
788 | Writer writer = new FileWriter(journalFile);
789 | writer.write(magic + "\n");
790 | writer.write(version + "\n");
791 | writer.write(appVersion + "\n");
792 | writer.write(valueCount + "\n");
793 | writer.write(blank + "\n");
794 | for (String line : bodyLines) {
795 | writer.write(line);
796 | writer.write('\n');
797 | }
798 | writer.close();
799 | }
800 |
801 | private List readJournalLines() throws Exception {
802 | List result = new ArrayList();
803 | BufferedReader reader = new BufferedReader(new FileReader(journalFile));
804 | String line;
805 | while ((line = reader.readLine()) != null) {
806 | result.add(line);
807 | }
808 | reader.close();
809 | return result;
810 | }
811 |
812 | private File getCleanFile(String key, int index) {
813 | return new File(cacheDir, key + "." + index);
814 | }
815 |
816 | private File getDirtyFile(String key, int index) {
817 | return new File(cacheDir, key + "." + index + ".tmp");
818 | }
819 |
820 | private static String readFile(File file) throws Exception {
821 | Reader reader = new FileReader(file);
822 | StringWriter writer = new StringWriter();
823 | char[] buffer = new char[1024];
824 | int count;
825 | while ((count = reader.read(buffer)) != -1) {
826 | writer.write(buffer, 0, count);
827 | }
828 | reader.close();
829 | return writer.toString();
830 | }
831 |
832 | public static void writeFile(File file, String content) throws Exception {
833 | FileWriter writer = new FileWriter(file);
834 | writer.write(content);
835 | writer.close();
836 | }
837 |
838 | private static void assertInoperable(DiskLruCache.Editor editor) throws Exception {
839 | try {
840 | editor.getString(0);
841 | Assert.fail();
842 | } catch (IllegalStateException expected) {
843 | }
844 | try {
845 | editor.set(0, "A");
846 | Assert.fail();
847 | } catch (IllegalStateException expected) {
848 | }
849 | try {
850 | editor.getFile(0);
851 | Assert.fail();
852 | } catch (IllegalStateException expected) {
853 | }
854 | try {
855 | editor.commit();
856 | Assert.fail();
857 | } catch (IllegalStateException expected) {
858 | }
859 | try {
860 | editor.abort();
861 | Assert.fail();
862 | } catch (IllegalStateException expected) {
863 | }
864 | }
865 |
866 | private void generateSomeGarbageFiles() throws Exception {
867 | File dir1 = new File(cacheDir, "dir1");
868 | File dir2 = new File(dir1, "dir2");
869 | writeFile(getCleanFile("g1", 0), "A");
870 | writeFile(getCleanFile("g1", 1), "B");
871 | writeFile(getCleanFile("g2", 0), "C");
872 | writeFile(getCleanFile("g2", 1), "D");
873 | writeFile(getCleanFile("g2", 1), "D");
874 | writeFile(new File(cacheDir, "otherFile0"), "E");
875 | dir1.mkdir();
876 | dir2.mkdir();
877 | writeFile(new File(dir2, "otherFile1"), "F");
878 | }
879 |
880 | private void assertGarbageFilesAllDeleted() throws Exception {
881 | assertThat(getCleanFile("g1", 0)).doesNotExist();
882 | assertThat(getCleanFile("g1", 1)).doesNotExist();
883 | assertThat(getCleanFile("g2", 0)).doesNotExist();
884 | assertThat(getCleanFile("g2", 1)).doesNotExist();
885 | assertThat(new File(cacheDir, "otherFile0")).doesNotExist();
886 | assertThat(new File(cacheDir, "dir1")).doesNotExist();
887 | }
888 |
889 | private void set(String key, String value0, String value1) throws Exception {
890 | DiskLruCache.Editor editor = cache.edit(key);
891 | editor.set(0, value0);
892 | editor.set(1, value1);
893 | editor.commit();
894 | }
895 |
896 | private void assertAbsent(String key) throws Exception {
897 | DiskLruCache.Value value = cache.get(key);
898 | if (value != null) {
899 | Assert.fail();
900 | }
901 | assertThat(getCleanFile(key, 0)).doesNotExist();
902 | assertThat(getCleanFile(key, 1)).doesNotExist();
903 | assertThat(getDirtyFile(key, 0)).doesNotExist();
904 | assertThat(getDirtyFile(key, 1)).doesNotExist();
905 | }
906 |
907 | private void assertValue(String key, String value0, String value1) throws Exception {
908 | DiskLruCache.Value value = cache.get(key);
909 | assertThat(value.getString(0)).isEqualTo(value0);
910 | assertThat(value.getLength(0)).isEqualTo(value0.length());
911 | assertThat(value.getString(1)).isEqualTo(value1);
912 | assertThat(value.getLength(1)).isEqualTo(value1.length());
913 | assertThat(getCleanFile(key, 0)).exists();
914 | assertThat(getCleanFile(key, 1)).exists();
915 | }
916 | }
917 |
--------------------------------------------------------------------------------