├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── LICENSE.txt ├── README.md ├── README.third_party ├── build.gradle ├── checkstyle.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew └── src ├── main └── java │ └── com │ └── bumptech │ └── glide │ └── disklrucache │ ├── DiskLruCache.java │ ├── StrictLineReader.java │ └── Util.java └── test └── java └── com └── bumptech └── glide └── disklrucache ├── DiskLruCacheTest.java └── StrictLineReaderTest.java /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'checkstyle' 3 | 4 | repositories { 5 | jcenter() 6 | } 7 | 8 | checkstyle { 9 | toolVersion = "6.6" 10 | } 11 | 12 | checkstyle { 13 | configFile = new File(projectDir, 'checkstyle.xml') 14 | } 15 | 16 | dependencies { 17 | def junitVersion = hasProperty('JUNIT_VERSION') ? JUNIT_VERSION : '4.11'; 18 | testCompile "junit:junit:${junitVersion}" 19 | testCompile 'commons-io:commons-io:2.1' 20 | testCompile 'org.easytesting:fest-assert-core:2.0M10' 21 | } 22 | 23 | def uploaderScript = "${rootProject.projectDir}/scripts/upload.gradle" 24 | if (file(uploaderScript).exists()) { 25 | apply from: uploaderScript 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjudd/DiskLruCache/273f119c607eb55da0627ebb4a0c1b0d1a15b2dc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------