├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ └── java │ │ └── com │ │ └── fewlaps │ │ └── quitnowcache │ │ ├── bean │ │ ├── ObjectTestOne.java │ │ └── ObjectTestTwo.java │ │ ├── MockDateProvider.java │ │ ├── MassiveDataTest.java │ │ ├── util │ │ └── RandomGenerator.java │ │ ├── GetOrDefaultTest.java │ │ ├── CastTest.java │ │ ├── BaseTest.java │ │ ├── CaseSensitiveKeyTest.java │ │ ├── ShutdownTest.java │ │ ├── CacheBeanTest.java │ │ ├── MemoryReleaseTest.java │ │ ├── SizeTest.java │ │ ├── IntroducingQNCacheTest.java │ │ ├── DefaultKeepaliveTest.java │ │ ├── KeySetTest.java │ │ ├── BuilderTest.java │ │ ├── DeprecatedBuilderTest.java │ │ ├── SetAndGetValuesTest.java │ │ └── ThreadSafeTest.java └── main │ └── java │ └── com │ └── fewlaps │ └── quitnowcache │ ├── DateProvider.java │ ├── QNCacheBean.kt │ ├── QNCacheBuilder.java │ └── QNCache.kt ├── .travis.yml ├── Dockerfile ├── .gitignore ├── LICENSE.md ├── gradlew.bat ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'quitnow-cache' 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fewlaps/quitnow-cache/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/bean/ObjectTestOne.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache.bean; 2 | 3 | /** 4 | * Created by Bernat on 24/09/2015. 5 | */ 6 | public class ObjectTestOne { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/bean/ObjectTestTwo.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache.bean; 2 | 3 | /** 4 | * Created by Bernat on 24/09/2015. 5 | */ 6 | public class ObjectTestTwo { 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 6 | -------------------------------------------------------------------------------- /src/main/java/com/fewlaps/quitnowcache/DateProvider.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | public interface DateProvider { 4 | 5 | DateProvider SYSTEM = new DateProvider() { 6 | @Override 7 | public long now() { 8 | return System.currentTimeMillis(); 9 | } 10 | }; 11 | 12 | long now(); 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | before_install: 6 | - chmod +x gradlew 7 | 8 | after_success: 9 | - ./gradlew cobertura coveralls 10 | 11 | deploy: 12 | - provider: script 13 | script: ./gradlew bintrayUpload -PbintrayUser="${BINTRAY_USER}" -PbintrayKey="${BINTRAY_KEY}" -PdryRun=false 14 | skip_cleanup: true 15 | on: 16 | tags: true 17 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/MockDateProvider.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | public class MockDateProvider implements DateProvider { 4 | 5 | private Long fakeNow; 6 | 7 | public void setFixed(long fakeNow) { 8 | this.fakeNow = fakeNow; 9 | } 10 | 11 | public void setSystem() { 12 | this.fakeNow = null; 13 | } 14 | 15 | @Override 16 | public long now() { 17 | return fakeNow != null ? fakeNow : DateProvider.SYSTEM.now(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/fewlaps/quitnowcache/QNCacheBean.kt: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache 2 | 3 | /** 4 | * This class is not public 'cause we'll hide the implementation of the QNCache itself 5 | */ 6 | internal class QNCacheBean( 7 | val value: T, 8 | private val creationDate: Long, 9 | private val keepAliveInMillis: Long) { 10 | 11 | fun isAlive(now: Long): Boolean { 12 | return when (keepAliveInMillis) { 13 | QNCache.KEEPALIVE_FOREVER -> true 14 | else -> creationDate + keepAliveInMillis > now 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/MassiveDataTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | public class MassiveDataTest extends BaseTest { 9 | QNCache cache; 10 | 11 | @Before 12 | public void init() { 13 | cache = new QNCache.Builder().build(); 14 | } 15 | 16 | @Test 17 | public void worksWithLotsOfEntries() { 18 | int iterations = 10000; 19 | for (int i = 0; i < iterations; i++) { 20 | cache.set(String.valueOf(i), i, 0); 21 | } 22 | for (Integer i = 0; i < iterations; i++) { 23 | assertEquals(i, cache.get(String.valueOf(i))); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | 3 | ENV GRADLE_VERSION 2.13 4 | ENV GRADLE_URL https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip 5 | #ENV GRADLE_SHA1 6 | ENV GRADLE_HOME /usr/lib/gradle-${GRADLE_VERSION} 7 | ENV GRADLE_REF /usr/lib/gradle-ref 8 | ENV PATH $PATH:${GRADLE_HOME}/bin 9 | 10 | ENV GRADLE_CONFIG /root/.gradle 11 | VOLUME $GRADLE_CONFIG 12 | 13 | ENV COPY_REFERENCE_FILE_LOG $GRADLE_CONFIG/copy_reference_file.log 14 | 15 | 16 | RUN cd /usr/lib && \ 17 | curl -fsSL $GRADLE_URL -o gradle-bin.zip && \ 18 | unzip gradle-bin.zip && \ 19 | ln -s "/usr/lib/gradle-${GRADLE_VERSION}/bin/gradle" /usr/bin/gradle && \ 20 | rm gradle-bin.zip && \ 21 | mkdir -p /src $GRADLE_REF 22 | 23 | WORKDIR /src 24 | 25 | COPY . /src 26 | 27 | RUN /src/gradlew test -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/util/RandomGenerator.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache.util; 2 | 3 | import java.util.Date; 4 | import java.util.Random; 5 | 6 | public class RandomGenerator { 7 | 8 | private static Random random = new Random((new Date()).getTime()); 9 | 10 | public static String generateRandomString() { 11 | char[] values = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 12 | 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 13 | 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', 14 | '4', '5', '6', '7', '8', '9'}; 15 | 16 | String out = ""; 17 | 18 | for (int i = 0; i < 100; i++) { 19 | int idx = random.nextInt(values.length); 20 | out += values[idx]; 21 | } 22 | return out; 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/GetOrDefaultTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | public class GetOrDefaultTest extends BaseTest { 9 | 10 | QNCache cache; 11 | 12 | @Before 13 | public void init() { 14 | cache = new QNCache.Builder().build(); 15 | } 16 | 17 | @Test 18 | public void valueIsReturnedIfItsPresent() { 19 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 20 | 21 | String result = cache.getOrDefault(A_KEY, "nothing"); 22 | 23 | assertEquals(A_VALUE, result); 24 | } 25 | 26 | @Test 27 | public void defaultIsReturnedIfValueIsNotPresent() { 28 | cache.remove(A_KEY); 29 | 30 | String result = cache.getOrDefault(A_KEY, "nothing"); 31 | 32 | assertEquals("nothing", result); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/CastTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import com.fewlaps.quitnowcache.bean.ObjectTestOne; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import static org.hamcrest.core.IsInstanceOf.instanceOf; 9 | import static org.junit.Assert.assertThat; 10 | 11 | public class CastTest extends BaseTest { 12 | QNCache cache; 13 | 14 | @Before 15 | public void init() { 16 | cache = new QNCache.Builder().build(); 17 | } 18 | 19 | @Test 20 | public void saveObjectAndReturnSameInstance() { 21 | cache.set(A_KEY, new ObjectTestOne()); 22 | 23 | assertThat(cache.get(A_KEY), instanceOf(ObjectTestOne.class)); 24 | } 25 | 26 | @Test 27 | public void shouldCastObject() { 28 | cache.set(A_KEY, new ObjectTestOne()); 29 | 30 | ObjectTestOne objectTestOne = cache.get(A_KEY); 31 | 32 | assertThat(objectTestOne, instanceOf(ObjectTestOne.class)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 2 | 3 | *.iml 4 | 5 | ## Directory-based project format: 6 | .idea/ 7 | # if you remove the above rule, at least ignore the following: 8 | 9 | # User-specific stuff: 10 | # .idea/workspace.xml 11 | # .idea/tasks.xml 12 | # .idea/dictionaries 13 | 14 | # Sensitive or high-churn files: 15 | # .idea/dataSources.ids 16 | # .idea/dataSources.xml 17 | # .idea/sqlDataSources.xml 18 | # .idea/dynamic.xml 19 | # .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | # .idea/gradle.xml 23 | # .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | # .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.ipr 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | /out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | 40 | # JIRA plugin 41 | atlassian-ide-plugin.xml 42 | 43 | # Crashlytics plugin (for Android Studio and IntelliJ) 44 | com_crashlytics_export_strings.xml 45 | crashlytics.properties 46 | crashlytics-build.properties 47 | target/ 48 | release.properties 49 | .gradle/ 50 | build/ 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Fewlaps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/BaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | /** 4 | * Testing dates is a madness. This class will improve the readability of tests. 5 | */ 6 | public abstract class BaseTest { 7 | public static final String A_KEY = "aLovelyKey"; 8 | public static final String A_VALUE = "aLovelyValue"; 9 | public static final String ANOTHER_KEY = "anotherLovelyKey"; 10 | public static final String ANOTHER_VALUE = "anotherLovelyValue"; 11 | public static final String JUST_A = "a"; 12 | 13 | public static final long FOREVER = 0; 14 | public static final long ONE_SECOND = 1000; 15 | public static final long TWO_HOURS = 2 * 60 * 1000; 16 | public static final long THREE_DAYS = 3 * 24 * 60 * 1000; 17 | 18 | protected long now() { 19 | return System.currentTimeMillis(); 20 | } 21 | 22 | protected long oneSecondFromNow() { 23 | return now() + ONE_SECOND; 24 | } 25 | 26 | protected long twoHoursFromNow() { 27 | return now() + TWO_HOURS; 28 | } 29 | 30 | protected long threeDaysFromNow() { 31 | return now() + THREE_DAYS; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/CaseSensitiveKeyTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNull; 7 | 8 | public class CaseSensitiveKeyTest extends BaseTest { 9 | 10 | @Test 11 | public void shouldReturnTheSameIfIgnoringCaseSensitive() { 12 | QNCache cache = new QNCache.Builder().caseSensitiveKeys(false).build(); 13 | cache.set(A_KEY.toLowerCase(), A_VALUE, FOREVER); 14 | 15 | assertEquals(A_VALUE, cache.get(A_KEY.toUpperCase())); 16 | } 17 | 18 | @Test 19 | public void shouldReturnNullIfUsingCaseSensitive() { 20 | QNCache cache = new QNCache.Builder().caseSensitiveKeys(true).build(); 21 | cache.set(A_KEY.toLowerCase(), A_VALUE, FOREVER); 22 | 23 | assertNull(cache.get(A_KEY.toUpperCase())); 24 | } 25 | 26 | @Test 27 | public void shouldReturnNullIfUsingDefaultBuilder() { 28 | QNCache cache = new QNCache.Builder().build(); 29 | cache.set(A_KEY.toLowerCase(), A_VALUE, FOREVER); 30 | 31 | assertNull(cache.get(A_KEY.toUpperCase())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/ShutdownTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | import static junit.framework.TestCase.assertEquals; 6 | import static junit.framework.TestCase.assertTrue; 7 | 8 | public class ShutdownTest extends BaseTest { 9 | 10 | @Test 11 | public void shutdownClearsTheCache() { 12 | QNCache cache = new QNCache.Builder().autoReleaseInSeconds(1).build(); 13 | cache.shutdown(); 14 | assertEquals(0, cache.size()); 15 | } 16 | 17 | @Test 18 | public void shutdownFinishesTheAutoreleaserThread() throws InterruptedException { 19 | QNCache cache = new QNCache.Builder().autoReleaseInSeconds(1).build(); 20 | 21 | int threadsBeforeShutdown = Thread.activeCount(); 22 | 23 | cache.shutdown(); 24 | waitUntilThreadDies(); 25 | 26 | int threadsAfterShutdown = Thread.activeCount(); 27 | 28 | assertTrue(threadsBeforeShutdown > threadsAfterShutdown); 29 | } 30 | 31 | @Test 32 | public void shutdownDoesntCrashIfAutoreleaserIsOff() { 33 | QNCache cache = new QNCache.Builder().build(); 34 | cache.shutdown(); 35 | } 36 | 37 | private void waitUntilThreadDies() throws InterruptedException { 38 | Thread.sleep(1000); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/CacheBeanTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertFalse; 6 | import static org.junit.Assert.assertTrue; 7 | 8 | public class CacheBeanTest extends BaseTest { 9 | 10 | @Test 11 | public void aJustCreatedBeanWithMaxKeepaliveIsAliveRightNow() { 12 | QNCacheBean bean = new QNCacheBean(A_VALUE, now(), FOREVER); 13 | assertTrue(bean.isAlive(now())); 14 | } 15 | 16 | @Test 17 | public void aJustCreatedBeanWithMaxKeepaliveIsAliveAfterThreeDays() { 18 | QNCacheBean bean = new QNCacheBean(A_VALUE, now(), FOREVER); 19 | assertTrue(bean.isAlive(threeDaysFromNow())); 20 | } 21 | 22 | @Test 23 | public void aJustCreatedBeanWithOneSecondOfKeepaliveIsAliveRightNow() { 24 | QNCacheBean bean = new QNCacheBean(A_VALUE, now(), ONE_SECOND); 25 | assertTrue(bean.isAlive(now())); 26 | } 27 | 28 | @Test 29 | public void aJustCreatedBeanWithTwoHoursOfKeepaliveIsAliveAfterASecond() { 30 | QNCacheBean bean = new QNCacheBean(A_VALUE, now(), TWO_HOURS); 31 | assertTrue(bean.isAlive(oneSecondFromNow())); 32 | } 33 | 34 | @Test 35 | public void aJustCreatedBeanWithOneSecondOfKeepaliveIsNotAliveAfterTwoSeconds() { 36 | QNCacheBean bean = new QNCacheBean(A_VALUE, now(), ONE_SECOND); 37 | assertFalse(bean.isAlive(twoHoursFromNow())); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/fewlaps/quitnowcache/QNCacheBuilder.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import static com.fewlaps.quitnowcache.QNCache.KEEPALIVE_FOREVER; 6 | import static com.fewlaps.quitnowcache.QNCache.WITHOUT_AUTORELEASE; 7 | 8 | /** 9 | * @deprecated Use QNCache.builder() instead. 10 | */ 11 | @Deprecated 12 | public class QNCacheBuilder { 13 | private boolean caseSensitiveKeys = true; 14 | private int autoReleaseInSeconds = QNCache.WITHOUT_AUTORELEASE; 15 | private long defaultKeepaliveInMillis = QNCache.KEEPALIVE_FOREVER; 16 | 17 | public QNCacheBuilder setCaseSensitiveKeys(boolean caseSensitiveKeys) { 18 | this.caseSensitiveKeys = caseSensitiveKeys; 19 | return this; 20 | } 21 | 22 | public QNCacheBuilder setAutoRelease(int units, TimeUnit timeUnit) { 23 | this.autoReleaseInSeconds = Long.valueOf(timeUnit.toSeconds(units)).intValue(); 24 | return this; 25 | } 26 | 27 | public QNCacheBuilder setAutoReleaseInSeconds(int autoReleaseInSeconds) { 28 | this.autoReleaseInSeconds = autoReleaseInSeconds; 29 | return this; 30 | } 31 | 32 | public QNCacheBuilder setDefaultKeepalive(int units, TimeUnit timeUnit) { 33 | this.defaultKeepaliveInMillis = timeUnit.toMillis(units); 34 | return this; 35 | } 36 | 37 | public QNCacheBuilder setDefaultKeepaliveInMillis(long defaultKeepaliveInMillis) { 38 | this.defaultKeepaliveInMillis = defaultKeepaliveInMillis; 39 | return this; 40 | } 41 | 42 | public QNCache createQNCache() { 43 | if (autoReleaseInSeconds < 0) { 44 | autoReleaseInSeconds = WITHOUT_AUTORELEASE; 45 | } 46 | if (defaultKeepaliveInMillis < 0) { 47 | defaultKeepaliveInMillis = KEEPALIVE_FOREVER; 48 | } 49 | return new QNCache(caseSensitiveKeys, autoReleaseInSeconds, defaultKeepaliveInMillis); 50 | } 51 | } -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/MemoryReleaseTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static junit.framework.TestCase.assertTrue; 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class MemoryReleaseTest extends BaseTest { 10 | 11 | @Before 12 | public void init() { 13 | } 14 | 15 | @Test 16 | public void manualReleaseMemoryWorks() throws InterruptedException { 17 | QNCache cache = new QNCache.Builder().build(); 18 | 19 | //adding 3 values that will be alive for 1 second, 2 seconds, 3 seconds. 20 | cache.set("1", A_VALUE, 1000); 21 | cache.set("2", A_VALUE, 2000); 22 | cache.set("3", A_VALUE, 3000); 23 | 24 | //checking that forgettingOldValues work 25 | cache.purge(); 26 | assertEquals(3, cache.sizeDeadAndAliveElements()); 27 | 28 | Thread.sleep(1000); 29 | cache.purge(); 30 | assertEquals(2, cache.sizeDeadAndAliveElements()); 31 | 32 | Thread.sleep(1000); 33 | cache.purge(); 34 | assertEquals(1, cache.sizeDeadAndAliveElements()); 35 | 36 | Thread.sleep(1000); 37 | cache.purge(); 38 | assertEquals(0, cache.sizeDeadAndAliveElements()); 39 | } 40 | 41 | @Test 42 | public void autoReleaseMemoryWorks() throws InterruptedException { 43 | QNCache cache = new QNCache.Builder().autoReleaseInSeconds(1).build(); 44 | 45 | //adding 3 values that will be alive for 1 second, 2 seconds, 3 seconds. 46 | cache.set("1", A_VALUE, 1000); 47 | cache.set("2", A_VALUE, 2000); 48 | cache.set("3", A_VALUE, 3000); 49 | 50 | //checking that forgettingOldValues work 51 | assertEquals(3, cache.sizeDeadAndAliveElements()); 52 | Thread.sleep(5000); 53 | assertEquals(0, cache.sizeDeadAndAliveElements()); 54 | } 55 | 56 | @Test 57 | public void autoReleaserCreatesAThread() { 58 | int threadsBefore = Thread.activeCount(); 59 | 60 | new QNCache.Builder().autoReleaseInSeconds(1).build(); 61 | 62 | int threadsAfter = Thread.activeCount(); 63 | 64 | assertTrue(threadsBefore < threadsAfter); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/SizeTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.*; 7 | 8 | public class SizeTest extends BaseTest { 9 | 10 | QNCache cache; 11 | MockDateProvider dateProvider; 12 | 13 | @Before 14 | public void init() { 15 | cache = new QNCache.Builder().build(); 16 | dateProvider = new MockDateProvider(); 17 | cache.setDateProvider(dateProvider); 18 | } 19 | 20 | @Test 21 | public void sizeWorksForZeroElements() { 22 | assertTrue(cache.isEmpty()); 23 | assertEquals(0, cache.size()); 24 | } 25 | 26 | @Test 27 | public void sizeWorksForOneElement() { 28 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 29 | 30 | assertFalse(cache.isEmpty()); 31 | assertEquals(1, cache.size()); 32 | } 33 | 34 | @Test 35 | public void sizeWorksForTwoElements() { 36 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 37 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 38 | 39 | assertFalse(cache.isEmpty()); 40 | assertEquals(2, cache.size()); 41 | } 42 | 43 | @Test 44 | public void sizeWorksAfterRemovingAllTheElements() { 45 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 46 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 47 | 48 | cache.clear(); 49 | 50 | assertNull(cache.get(A_KEY)); 51 | assertNull(cache.get(ANOTHER_KEY)); 52 | 53 | assertEquals(0, cache.size()); 54 | assertEquals(0, cache.sizeAliveElements()); 55 | assertEquals(0, cache.sizeDeadElements()); 56 | assertEquals(0, cache.sizeDeadAndAliveElements()); 57 | assertTrue(cache.isEmpty()); 58 | } 59 | 60 | @Test 61 | public void sizeWorksAfterRemovingOldElements() { 62 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 63 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 64 | dateProvider.setFixed(twoHoursFromNow()); 65 | 66 | cache.purge(); 67 | 68 | assertNull(cache.get(A_KEY)); 69 | assertEquals(ANOTHER_VALUE, cache.get(ANOTHER_KEY)); 70 | 71 | assertEquals(1, cache.size()); 72 | assertEquals(1, cache.sizeAliveElements()); 73 | assertEquals(0, cache.sizeDeadElements()); 74 | assertEquals(1, cache.sizeDeadAndAliveElements()); 75 | assertFalse(cache.isEmpty()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/IntroducingQNCacheTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | public class IntroducingQNCacheTest { 6 | 7 | @SuppressWarnings("UnusedAssignment") 8 | @Test 9 | public void theCodeOfTheReadmeWorks() { 10 | //##The sample 11 | 12 | QNCache cache = new QNCache.Builder().build(); 13 | 14 | cache.set("key", "value", 60 * 1000); // It can store things for a minute, 15 | cache.set("key", "value", 60 * 60 * 1000); // for an hour, 16 | cache.set("key", "value", 0); // or forever. 17 | cache.set("key", "value"); // And also for the short version of forever. 18 | 19 | cache.get("key"); // It can get them again, 20 | cache.remove("key"); // and remove it if you want. 21 | 22 | cache.get("unExistingKey"); // If something doesn't exists, it returns null 23 | cache.get("tooOldKey"); // The same if a key is too old 24 | 25 | cache.clear(); // You can also clean it, 26 | cache.size(); // and ask it how many elements it has 27 | 28 | QNCache stringCache = new QNCache.Builder().build(); //You can also make it typesafe 29 | //stringCache.set("key", 42); //so this will not compile :) 30 | 31 | //##Let's talk about the memory 32 | cache = new QNCache.Builder().autoReleaseInSeconds(1).build(); //frees the memory every second 33 | cache = new QNCache.Builder().autoReleaseInSeconds(60).build(); //frees the memory every minute 34 | cache = new QNCache.Builder().autoReleaseInSeconds(60*60).build(); //frees the memory every hour 35 | cache = new QNCache.Builder().build(); //never frees the memory 36 | 37 | //##Are the keys case sensitive? 38 | cache = new QNCache.Builder().caseSensitiveKeys(true).build(); //"key" and "KEY" will be different items 39 | cache = new QNCache.Builder().caseSensitiveKeys(false).build(); //"key" and "KEY" will be the same 40 | cache = new QNCache.Builder().build(); //"key" and "KEY" will be different items 41 | 42 | //##It's possible to change the default keepalive? 43 | cache = new QNCache.Builder().defaultKeepaliveInMillis(1000).build(); //a keepalive of one second 44 | cache = new QNCache.Builder().defaultKeepaliveInMillis(1000 * 60).build(); //a keepalive of one minute 45 | cache = new QNCache.Builder().build(); //the default keepalive: remember it forever! 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/DefaultKeepaliveTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertNotNull; 6 | import static org.junit.Assert.assertNull; 7 | 8 | public class DefaultKeepaliveTest extends BaseTest { 9 | 10 | @Test 11 | public void shouldHaveInfiniteKeepaliveByDefault() { 12 | QNCache cache = new QNCache.Builder().build(); 13 | MockDateProvider dateProvider = new MockDateProvider(); 14 | cache.setDateProvider(dateProvider); 15 | 16 | cache.set(A_KEY, A_VALUE); 17 | 18 | dateProvider.setFixed(threeDaysFromNow()); 19 | 20 | assertNotNull(cache.get(A_KEY)); 21 | } 22 | 23 | @Test 24 | public void testOneSecondKeepaliveRightNow() { 25 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(ONE_SECOND).build(); 26 | MockDateProvider dateProvider = new MockDateProvider(); 27 | cache.setDateProvider(dateProvider); 28 | 29 | cache.set(A_KEY, A_VALUE); 30 | 31 | assertNotNull(cache.get(A_KEY)); 32 | } 33 | 34 | @Test 35 | public void testOneSecondKeepaliveAfterOneSecond() { 36 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(ONE_SECOND).build(); 37 | MockDateProvider dateProvider = new MockDateProvider(); 38 | cache.setDateProvider(dateProvider); 39 | 40 | cache.set(A_KEY, A_VALUE); 41 | 42 | dateProvider.setFixed(oneSecondFromNow()); 43 | 44 | assertNull(cache.get(A_KEY)); 45 | } 46 | 47 | @Test 48 | public void testOneSecondKeepaliveAfterTwoHours() { 49 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(ONE_SECOND).build(); 50 | MockDateProvider dateProvider = new MockDateProvider(); 51 | cache.setDateProvider(dateProvider); 52 | 53 | cache.set(A_KEY, A_VALUE); 54 | 55 | dateProvider.setFixed(twoHoursFromNow()); 56 | 57 | assertNull(cache.get(A_KEY)); 58 | } 59 | 60 | @Test 61 | public void testTwoSecondsKeepaliveAfterOneSecond() { 62 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(2 * ONE_SECOND).build(); 63 | MockDateProvider dateProvider = new MockDateProvider(); 64 | cache.setDateProvider(dateProvider); 65 | 66 | cache.set(A_KEY, A_VALUE); 67 | 68 | dateProvider.setFixed(oneSecondFromNow()); 69 | 70 | assertNotNull(cache.get(A_KEY)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/KeySetTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static junit.framework.TestCase.assertTrue; 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertFalse; 9 | 10 | public class KeySetTest extends BaseTest { 11 | 12 | QNCache cache; 13 | MockDateProvider dateProvider; 14 | 15 | @Before 16 | public void init() { 17 | cache = new QNCache.Builder().build(); 18 | dateProvider = new MockDateProvider(); 19 | cache.setDateProvider(dateProvider); 20 | } 21 | 22 | @Test 23 | public void keySetIsEmptyWhenCacheHasBeenJustCreated() { 24 | assertEquals(0, cache.keySetDeadAndAlive().size()); 25 | } 26 | 27 | @Test 28 | public void keySetReturnsTheSameNumberAsAddedItems() { 29 | cache.set(A_KEY, A_VALUE); 30 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 31 | 32 | assertEquals(2, cache.keySetDeadAndAlive().size()); 33 | } 34 | 35 | @Test 36 | public void keySetReturnsOnlyAliveKeys() { 37 | cache.set("a", A_VALUE); 38 | cache.set("b", A_VALUE); 39 | cache.set("c", A_VALUE, ONE_SECOND); 40 | 41 | dateProvider.setFixed(threeDaysFromNow()); 42 | 43 | assertEquals(2, cache.keySet().size()); 44 | } 45 | 46 | @Test 47 | public void keySetAliveReturnsOnlyAliveKeys() { 48 | cache.set("a", A_VALUE); 49 | cache.set("b", A_VALUE); 50 | cache.set("c", A_VALUE, ONE_SECOND); 51 | 52 | dateProvider.setFixed(threeDaysFromNow()); 53 | 54 | assertEquals(2, cache.keySetAlive().size()); 55 | } 56 | 57 | @Test 58 | public void keySetDeadReturnsOnlyDeadKeys() { 59 | cache.set("a", A_VALUE); 60 | cache.set("b", A_VALUE); 61 | cache.set("c", A_VALUE, ONE_SECOND); 62 | 63 | dateProvider.setFixed(threeDaysFromNow()); 64 | 65 | assertEquals(1, cache.keySetDead().size()); 66 | } 67 | 68 | @Test 69 | public void keySetIsEmptyWhenClearingTheCache() { 70 | cache.set(A_KEY, A_VALUE); 71 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 72 | cache.clear(); 73 | 74 | assertEquals(0, cache.keySetStartingWith(A_KEY).size()); 75 | } 76 | 77 | @Test 78 | public void keySetStartingWithShouldNeverCrash() { 79 | assertEquals(0, cache.keySetStartingWith(null).size()); 80 | assertEquals(0, cache.keySetStartingWith("").size()); 81 | } 82 | 83 | @Test 84 | public void setAndFindNone() { 85 | cache.set(A_KEY, A_VALUE); 86 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 87 | 88 | assertEquals(0, cache.keySetStartingWith(A_VALUE).size()); 89 | } 90 | 91 | @Test 92 | public void setAndFindOne() { 93 | cache.set(A_KEY, A_VALUE); 94 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 95 | 96 | assertEquals(1, cache.keySetStartingWith(A_KEY).size()); 97 | } 98 | 99 | @Test 100 | public void setAndFindMoreThanOne() { 101 | cache.set("weLoveAndroid", A_VALUE); 102 | cache.set("weLoveLinux", ANOTHER_VALUE, THREE_DAYS); 103 | cache.set("weHateNothing", ANOTHER_VALUE, THREE_DAYS); 104 | 105 | assertEquals(2, cache.keySetStartingWith("weLove").size()); 106 | } 107 | 108 | @Test 109 | public void setAndFindIfAlive() { 110 | cache.set(A_KEY, A_VALUE, -1); 111 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 112 | 113 | assertEquals(0, cache.keySetAliveStartingWith(A_KEY).size()); 114 | assertEquals(1, cache.keySetAliveStartingWith(ANOTHER_KEY).size()); 115 | } 116 | 117 | @Test 118 | public void keySetAlive() { 119 | cache.set(A_KEY, A_VALUE); 120 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, ONE_SECOND); 121 | 122 | dateProvider.setFixed(threeDaysFromNow()); 123 | 124 | assertTrue(cache.isKeyAlive(A_KEY)); 125 | assertFalse(cache.isKeyAlive(ANOTHER_KEY)); 126 | 127 | assertFalse(cache.isKeyDead(A_KEY)); 128 | assertTrue(cache.isKeyDead(ANOTHER_KEY)); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/BuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import static com.fewlaps.quitnowcache.QNCache.KEEPALIVE_FOREVER; 8 | import static com.fewlaps.quitnowcache.QNCache.WITHOUT_AUTORELEASE; 9 | import static org.junit.Assert.*; 10 | 11 | public class BuilderTest { 12 | 13 | @Test 14 | public void testDefaultBuilder() { 15 | QNCache cache = new QNCache.Builder().build(); 16 | 17 | assertTrue(cache.getCaseSensitiveKeys()); 18 | assertEquals(WITHOUT_AUTORELEASE, cache.getAutoReleaseInSeconds()); 19 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 20 | } 21 | 22 | @Test 23 | public void testSettingTrueCaseSensitiveKeysBuilder() { 24 | QNCache cache = new QNCache.Builder().caseSensitiveKeys(true).build(); 25 | 26 | assertTrue(cache.getCaseSensitiveKeys()); 27 | } 28 | 29 | @Test 30 | public void testSettingFalseCaseSensitiveKeysBuilder() { 31 | QNCache cache = new QNCache.Builder().caseSensitiveKeys(false).build(); 32 | 33 | assertFalse(cache.getCaseSensitiveKeys()); 34 | } 35 | 36 | @Test 37 | public void testSettingMinusOneAutoReleaseSecondsBuilder() { 38 | QNCache cache = new QNCache.Builder().autoReleaseInSeconds(-1).build(); 39 | 40 | assertEquals(WITHOUT_AUTORELEASE, cache.getAutoReleaseInSeconds()); 41 | } 42 | 43 | @Test 44 | public void testSetting10AutoReleaseSecondsBuilder() { 45 | QNCache cache = new QNCache.Builder().autoReleaseInSeconds(10).build(); 46 | 47 | assertEquals(10, cache.getAutoReleaseInSeconds()); 48 | } 49 | 50 | @Test 51 | public void testSetting10AutoReleaseSecondsBuilder_usingSecondsTimeUnit() { 52 | QNCache cache = new QNCache.Builder().autoRelease(10, TimeUnit.SECONDS).build(); 53 | 54 | assertEquals(10, cache.getAutoReleaseInSeconds()); 55 | } 56 | 57 | @Test 58 | public void testSetting10AutoReleaseSecondsBuilder_usingMillisTimeUnit() { 59 | QNCache cache = new QNCache.Builder().autoRelease(10000, TimeUnit.MILLISECONDS).build(); 60 | 61 | assertEquals(10, cache.getAutoReleaseInSeconds()); 62 | } 63 | 64 | @Test 65 | public void testSetting10AutoReleaseSecondsBuilder_usingMicrosTimeUnit() { 66 | QNCache cache = new QNCache.Builder().autoRelease(10000000, TimeUnit.MICROSECONDS).build(); 67 | 68 | assertEquals(10, cache.getAutoReleaseInSeconds()); 69 | } 70 | 71 | @Test 72 | public void testSettingDefaultKeepaliveBuilder_with10() { 73 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(10).build(); 74 | 75 | assertEquals(10, cache.getDefaultKeepaliveInMillis()); 76 | } 77 | 78 | @Test 79 | public void testSettingDefaultKeepaliveBuilder_with10000MillisTimeUnits() { 80 | QNCache cache = new QNCache.Builder().defaultKeepalive(10000, TimeUnit.MILLISECONDS).build(); 81 | 82 | assertEquals(10000, cache.getDefaultKeepaliveInMillis()); 83 | } 84 | 85 | @Test 86 | public void testSettingDefaultKeepaliveBuilder_with10SecondsTimeUnits() { 87 | QNCache cache = new QNCache.Builder().defaultKeepalive(10, TimeUnit.SECONDS).build(); 88 | 89 | assertEquals(10000, cache.getDefaultKeepaliveInMillis()); 90 | } 91 | 92 | @Test 93 | public void testSettingDefaultKeepaliveBuilder_withZero() { 94 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(0).build(); 95 | 96 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 97 | } 98 | 99 | @Test 100 | public void testSettingDefaultKeepaliveBuilder_withZeroSecondsTimeUnit() { 101 | QNCache cache = new QNCache.Builder().defaultKeepalive(0, TimeUnit.SECONDS).build(); 102 | 103 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 104 | } 105 | 106 | @Test 107 | public void testSettingDefaultKeepaliveBuilder_withMinusTen() { 108 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(-10).build(); 109 | 110 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 111 | } 112 | 113 | @Test 114 | public void testSettingDefaultKeepaliveBuilder_withMinusTenSecondsTimeUnit() { 115 | QNCache cache = new QNCache.Builder().defaultKeepalive(-10, TimeUnit.SECONDS).build(); 116 | 117 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 118 | } 119 | 120 | @Test 121 | public void testQNCacheDefaultKeepaliveIsForever() { 122 | QNCache cache = new QNCache.Builder().defaultKeepaliveInMillis(KEEPALIVE_FOREVER).build(); 123 | 124 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/DeprecatedBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import static com.fewlaps.quitnowcache.QNCache.KEEPALIVE_FOREVER; 8 | import static com.fewlaps.quitnowcache.QNCache.WITHOUT_AUTORELEASE; 9 | import static org.junit.Assert.*; 10 | 11 | public class DeprecatedBuilderTest { 12 | 13 | @Test 14 | public void testDefaultBuilder() { 15 | QNCache cache = new QNCacheBuilder().createQNCache(); 16 | 17 | assertTrue(cache.getCaseSensitiveKeys()); 18 | assertEquals(WITHOUT_AUTORELEASE, cache.getAutoReleaseInSeconds()); 19 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 20 | } 21 | 22 | @Test 23 | public void testSettingTruesetCaseSensitiveKeysBuilder() { 24 | QNCache cache = new QNCacheBuilder().setCaseSensitiveKeys(true).createQNCache(); 25 | 26 | assertTrue(cache.getCaseSensitiveKeys()); 27 | } 28 | 29 | @Test 30 | public void testSettingFalsesetCaseSensitiveKeysBuilder() { 31 | QNCache cache = new QNCacheBuilder().setCaseSensitiveKeys(false).createQNCache(); 32 | 33 | assertFalse(cache.getCaseSensitiveKeys()); 34 | } 35 | 36 | @Test 37 | public void testSettingMinusOnesetAutoReleaseSecondsBuilder() { 38 | QNCache cache = new QNCacheBuilder().setAutoReleaseInSeconds(-1).createQNCache(); 39 | 40 | assertEquals(WITHOUT_AUTORELEASE, cache.getAutoReleaseInSeconds()); 41 | } 42 | 43 | @Test 44 | public void testSetting10setAutoReleaseSecondsBuilder() { 45 | QNCache cache = new QNCacheBuilder().setAutoReleaseInSeconds(10).createQNCache(); 46 | 47 | assertEquals(10, cache.getAutoReleaseInSeconds()); 48 | } 49 | 50 | @Test 51 | public void testSetting10setAutoReleaseSecondsBuilder_usingSecondsTimeUnit() { 52 | QNCache cache = new QNCacheBuilder().setAutoRelease(10, TimeUnit.SECONDS).createQNCache(); 53 | 54 | assertEquals(10, cache.getAutoReleaseInSeconds()); 55 | } 56 | 57 | @Test 58 | public void testSetting10setAutoReleaseSecondsBuilder_usingMillisTimeUnit() { 59 | QNCache cache = new QNCacheBuilder().setAutoRelease(10000, TimeUnit.MILLISECONDS).createQNCache(); 60 | 61 | assertEquals(10, cache.getAutoReleaseInSeconds()); 62 | } 63 | 64 | @Test 65 | public void testSetting10setAutoReleaseSecondsBuilder_usingMicrosTimeUnit() { 66 | QNCache cache = new QNCacheBuilder().setAutoRelease(10000000, TimeUnit.MICROSECONDS).createQNCache(); 67 | 68 | assertEquals(10, cache.getAutoReleaseInSeconds()); 69 | } 70 | 71 | @Test 72 | public void testSettingsetDefaultKeepaliveBuilder_with10() { 73 | QNCache cache = new QNCacheBuilder().setDefaultKeepaliveInMillis(10).createQNCache(); 74 | 75 | assertEquals(10, cache.getDefaultKeepaliveInMillis()); 76 | } 77 | 78 | @Test 79 | public void testSettingsetDefaultKeepaliveBuilder_with10000MillisTimeUnits() { 80 | QNCache cache = new QNCacheBuilder().setDefaultKeepalive(10000, TimeUnit.MILLISECONDS).createQNCache(); 81 | 82 | assertEquals(10000, cache.getDefaultKeepaliveInMillis()); 83 | } 84 | 85 | @Test 86 | public void testSettingsetDefaultKeepaliveBuilder_with10SecondsTimeUnits() { 87 | QNCache cache = new QNCacheBuilder().setDefaultKeepalive(10, TimeUnit.SECONDS).createQNCache(); 88 | 89 | assertEquals(10000, cache.getDefaultKeepaliveInMillis()); 90 | } 91 | 92 | @Test 93 | public void testSettingsetDefaultKeepaliveBuilder_withZero() { 94 | QNCache cache = new QNCacheBuilder().setDefaultKeepaliveInMillis(0).createQNCache(); 95 | 96 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 97 | } 98 | 99 | @Test 100 | public void testSettingsetDefaultKeepaliveBuilder_withZeroSecondsTimeUnit() { 101 | QNCache cache = new QNCacheBuilder().setDefaultKeepalive(0, TimeUnit.SECONDS).createQNCache(); 102 | 103 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 104 | } 105 | 106 | @Test 107 | public void testSettingsetDefaultKeepaliveBuilder_withMinusTen() { 108 | QNCache cache = new QNCacheBuilder().setDefaultKeepaliveInMillis(-10).createQNCache(); 109 | 110 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 111 | } 112 | 113 | @Test 114 | public void testSettingsetDefaultKeepaliveBuilder_withMinusTenSecondsTimeUnit() { 115 | QNCache cache = new QNCacheBuilder().setDefaultKeepalive(-10, TimeUnit.SECONDS).createQNCache(); 116 | 117 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 118 | } 119 | 120 | @Test 121 | public void testQNCachesetDefaultKeepaliveIsForever() { 122 | QNCache cache = new QNCacheBuilder().setDefaultKeepaliveInMillis(KEEPALIVE_FOREVER).createQNCache(); 123 | 124 | assertEquals(KEEPALIVE_FOREVER, cache.getDefaultKeepaliveInMillis()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage Status](https://coveralls.io/repos/Fewlaps/quitnow-cache/badge.svg?branch=master&service=github)](https://coveralls.io/github/Fewlaps/quitnow-cache?branch=master) 2 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Fewlaps/quitnow-cache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 3 | 4 | # [QuitNow](http://quitnow.app)'s cache 5 | A temporary cache for JVM applications. 6 | 7 | Before this library, caching data for a limited time was a hard task to do. The developer had to save the last time the data was stored, and then, check it everytime the data was read. So, we decided to return the work to the open source community by writing this really simple cache, allowing developers to keep information for a limited time. 8 | 9 | We've done it using TDD, so it's totally tested. [Check the tests!](https://github.com/Fewlaps/quitnow-cache/tree/master/src/test/java/com/fewlaps/quitnowcache) :·) 10 | 11 | The sample 12 | ---------- 13 | 14 | ```java 15 | QNCache cache = new QNCache.Builder().build(); 16 | 17 | cache.set("key", "value", 60 * 1000); // It can store things for a minute, 18 | cache.set("key", "value", 60 * 60 * 1000); // for an hour, 19 | cache.set("key", "value", 0); // or forever. 20 | cache.set("key", "value"); // And also for the short version of forever. 21 | 22 | cache.get("key"); // It can get them again, 23 | cache.remove("key"); // and remove it if you want. 24 | 25 | cache.get("unexistingKey"); // If something doesn't exist, it returns null 26 | cache.get("tooOldKey"); // The same if a key is too old 27 | 28 | cache.clear(); // You can also clean it, 29 | cache.size(); // and ask it how many elements it has 30 | 31 | QNCache stringCache = new QNCache.Builder().build(); // You can also make it typesafe 32 | stringCache.set("key", 42); // so this won't compile :) 33 | ``` 34 | 35 | Let's talk about the memory 36 | --------------------------- 37 | By default, the cache stores a reference to all stored instances, doesn't matter if they're fresh or not. If you plan to store huge instances, like an Android's Bitmap, you can create it with an auto releaser. Then the cache will remove the old elements after the given amount of time. 38 | 39 | ```java 40 | new QNCache.Builder().autoReleaseInSeconds(1).build(); // frees the memory every second 41 | new QNCache.Builder().autoReleaseInSeconds(60).build(); // frees the memory every minute 42 | new QNCache.Builder().autoReleaseInSeconds(60*60).build(); // frees the memory every hour 43 | new QNCache.Builder().build(); // never frees the memory 44 | 45 | ``` 46 | 47 | By the way, if you use the auto releaser, you should know that you can stop it. You should do it when your Servlet container notifies you that it wants to finish the application. It's the same you should do with any database connection, for example. In Android environments you can avoid it, but you might be interested in [this extended explanation](https://github.com/Fewlaps/quitnow-cache/releases/tag/v3.2.0). 48 | 49 | ```java 50 | cache.shutdown(); 51 | ``` 52 | 53 | Are the keys case sensitive? 54 | --------------------------- 55 | By default, yes. But you can specify it at building time. 56 | 57 | ```java 58 | new QNCache.Builder().caseSensitiveKeys(true).build(); // "key" and "KEY" will be different items 59 | new QNCache.Builder().caseSensitiveKeys(false).build(); // "key" and "KEY" will be the same 60 | new QNCache.Builder().build(); // "key" and "KEY" will be different items 61 | ``` 62 | 63 | It's possible to change the default keepalive? 64 | --------------------------- 65 | Of course! But, again, you have to specify it at building time. 66 | 67 | ```java 68 | new QNCache.Builder().defaultKeepaliveInMillis(1000).build(); // a keepalive of one second 69 | new QNCache.Builder().defaultKeepaliveInMillis(1000 * 60).build(); // a keepalive of one minute 70 | new QNCache.Builder().build(); // the default keepalive: remember it forever! 71 | ``` 72 | 73 | Why working with millis and seconds? 74 | --------------------------- 75 | Good question! If you prefer to work with TimeUnit, you're free to do it. 76 | 77 | ```java 78 | new QNCache.Builder().autoRelease(2, TimeUnit.HOURS).build(); 79 | new QNCache.Builder().defaultKeepalive(5, TimeUnit.MINUTES).build(); 80 | cache.set("key", "value", 42, TimeUnit.SECONDS); 81 | ``` 82 | 83 | # Download 84 | 85 | * Grab via Gradle: 86 | ```groovy 87 | repositories { jcenter() } 88 | 89 | compile 'com.fewlaps.quitnowcache:quitnow-cache:3.4.0' 90 | ``` 91 | * Grab via Maven: 92 | ```xml 93 | 94 | jcenter 95 | http://jcenter.bintray.com 96 | 97 | 98 | 99 | com.fewlaps.quitnowcache 100 | quitnow-cache 101 | 3.4.0 102 | 103 | ``` 104 | 105 | 106 | ## LICENSE ## 107 | 108 | The MIT License (MIT) 109 | 110 | Copyright (c) 2018 Fewlaps 111 | 112 | Permission is hereby granted, free of charge, to any person obtaining a copy 113 | of this software and associated documentation files (the "Software"), to deal 114 | in the Software without restriction, including without limitation the rights 115 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 116 | copies of the Software, and to permit persons to whom the Software is 117 | furnished to do so, subject to the following conditions: 118 | 119 | The above copyright notice and this permission notice shall be included in all 120 | copies or substantial portions of the Software. 121 | 122 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 123 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 124 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 125 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 126 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 127 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 128 | SOFTWARE. 129 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/SetAndGetValuesTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertNull; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | public class SetAndGetValuesTest extends BaseTest { 14 | 15 | QNCache cache; 16 | MockDateProvider dateProvider; 17 | 18 | @Before 19 | public void init() { 20 | cache = new QNCache.Builder().build(); 21 | dateProvider = new MockDateProvider(); 22 | cache.setDateProvider(dateProvider); 23 | } 24 | 25 | @Test 26 | public void savingSomethingForOneSecondsShouldReturnTheSameImmediatelly() { 27 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 28 | 29 | assertEquals(A_VALUE, cache.get(A_KEY)); 30 | } 31 | 32 | @Test 33 | public void savingSomethingForeverShouldReturnTheSameImmediatelly() { 34 | cache.set(A_KEY, A_VALUE, FOREVER); 35 | 36 | assertEquals(A_VALUE, cache.get(A_KEY)); 37 | } 38 | 39 | @Test 40 | public void savingSomethingWithoudSpecifyingTheKeepaliveValueShouldReturnTheSameAfterThreeDays() { 41 | cache.set(A_KEY, A_VALUE); 42 | 43 | dateProvider.setFixed(threeDaysFromNow()); 44 | 45 | assertEquals(A_VALUE, cache.get(A_KEY)); 46 | } 47 | 48 | @Test 49 | public void savingSomethingForeverShouldReturnTheSameAfterThreeDays() { 50 | cache.set(A_KEY, A_VALUE, FOREVER); 51 | 52 | dateProvider.setFixed(threeDaysFromNow()); 53 | 54 | assertEquals(A_VALUE, cache.get(A_KEY)); 55 | } 56 | 57 | @Test 58 | public void savingAValueForTwoHoursShouldReturnNullAfterThreeDays() { 59 | cache.set(A_KEY, A_VALUE, TWO_HOURS); 60 | 61 | dateProvider.setFixed(threeDaysFromNow()); 62 | 63 | assertNull(cache.get(A_KEY)); 64 | } 65 | 66 | @Test 67 | public void savingSomethingForANegativeTimeWillBeIgnored() { 68 | cache.set(A_KEY, A_VALUE, -1); 69 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, ONE_SECOND); 70 | 71 | assertNull(cache.get(A_KEY)); 72 | assertEquals(1, cache.size()); 73 | assertEquals(1, cache.sizeDeadAndAliveElements()); 74 | } 75 | 76 | @Test 77 | public void gettingAIgnoredValueWithGetAndRemoveIfDeadWorks() { 78 | cache.set(A_KEY, A_VALUE, -1); 79 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 80 | 81 | assertNull(cache.getAndPurgeIfDead(A_KEY)); 82 | assertEquals(1, cache.size()); 83 | assertEquals(1, cache.sizeDeadAndAliveElements()); 84 | } 85 | 86 | @Test 87 | public void gettingADeadValueWithGetAndRemoveIfDeadWorks() { 88 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 89 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 90 | 91 | dateProvider.setFixed(twoHoursFromNow()); 92 | 93 | assertNull(cache.getAndPurgeIfDead(A_KEY)); 94 | assertEquals(1, cache.size()); 95 | assertEquals(1, cache.sizeDeadAndAliveElements()); 96 | } 97 | 98 | @Test 99 | public void gettingAnAliveValueWithGetAndRemoveIfDeadWorks() { 100 | cache.set(A_KEY, A_VALUE, ONE_SECOND); 101 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 102 | 103 | dateProvider.setFixed(twoHoursFromNow()); 104 | 105 | assertEquals(ANOTHER_VALUE, cache.getAndPurgeIfDead(ANOTHER_KEY)); 106 | assertEquals(1, cache.size()); 107 | assertEquals(2, cache.sizeDeadAndAliveElements()); 108 | } 109 | 110 | @Test 111 | public void removingAValueWorks() { 112 | cache.set(A_KEY, A_VALUE, TWO_HOURS); 113 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 114 | 115 | cache.remove(A_KEY); 116 | 117 | assertNull(cache.get(A_KEY)); 118 | assertEquals(ANOTHER_VALUE, cache.get(ANOTHER_KEY)); 119 | } 120 | 121 | @Test 122 | public void removingAllValuesWorks() { 123 | cache.set(A_KEY, A_VALUE, TWO_HOURS); 124 | cache.set(ANOTHER_KEY, ANOTHER_VALUE, THREE_DAYS); 125 | 126 | cache.clear(); 127 | 128 | assertNull(cache.get(A_KEY)); 129 | assertNull(cache.get(ANOTHER_KEY)); 130 | } 131 | 132 | @Test 133 | public void replacingAValueForANewOneReturnTheNewOne() { 134 | cache.set(A_KEY, A_VALUE, TWO_HOURS); 135 | cache.set(A_KEY, ANOTHER_VALUE, THREE_DAYS); 136 | 137 | assertEquals(ANOTHER_VALUE, cache.get(A_KEY)); 138 | } 139 | 140 | @Test 141 | public void replacingAValueForANewOneCanMakeItDead() { 142 | cache.set(A_KEY, A_VALUE, THREE_DAYS); 143 | cache.set(A_KEY, ANOTHER_VALUE, ONE_SECOND); 144 | 145 | dateProvider.setFixed(twoHoursFromNow()); 146 | 147 | assertNull(cache.get(A_KEY)); 148 | } 149 | 150 | @Test 151 | public void replacingAValueForNullWillRemoveIt() { 152 | cache.set(A_KEY, A_VALUE, THREE_DAYS); 153 | cache.set(A_KEY, null, ONE_SECOND); 154 | 155 | assertNull(cache.get(A_KEY)); 156 | } 157 | 158 | @Test 159 | public void containsReturnsTrueIfSomethingExists() { 160 | cache.set(A_KEY, A_VALUE, THREE_DAYS); 161 | 162 | assertTrue(cache.contains(A_KEY)); 163 | } 164 | 165 | @Test 166 | public void containsReturnsFalseSomethingDoesntExist() { 167 | assertFalse(cache.contains(A_KEY)); 168 | } 169 | 170 | @Test 171 | public void savingSomethingForOneSecondsShouldReturnTheSameImmediatelly_usingSecondsTimeUnit() { 172 | cache.set(A_KEY, A_VALUE, 1, TimeUnit.SECONDS); 173 | 174 | assertEquals(A_VALUE, cache.get(A_KEY)); 175 | } 176 | 177 | @Test 178 | public void savingSomethingForOneSecondsShouldReturnTheSameImmediatelly_usingMillisTimeUnit() { 179 | cache.set(A_KEY, A_VALUE, 1000, TimeUnit.MILLISECONDS); 180 | 181 | assertEquals(A_VALUE, cache.get(A_KEY)); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/test/java/com/fewlaps/quitnowcache/ThreadSafeTest.java: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache; 2 | 3 | import com.fewlaps.quitnowcache.util.RandomGenerator; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class ThreadSafeTest extends BaseTest { 14 | 15 | static final int ITERATIONS = 10000; 16 | 17 | QNCache cache; 18 | 19 | @Before 20 | public void init() { 21 | cache = new QNCache.Builder().build(); 22 | } 23 | 24 | @Test 25 | public void canEditWhileIteratingWithRandomKeys() throws InterruptedException { 26 | final AtomicInteger errors = new AtomicInteger(0); 27 | final AtomicBoolean writerFinished1 = new AtomicBoolean(false); 28 | final AtomicBoolean writerFinished2 = new AtomicBoolean(false); 29 | final AtomicBoolean readerFinished = new AtomicBoolean(false); 30 | final AtomicBoolean removerFinished = new AtomicBoolean(false); 31 | 32 | Thread writer1 = new Thread() { 33 | @Override 34 | public void run() { 35 | for (int i = 0; i < ITERATIONS; i++) { 36 | try { 37 | cache.set(RandomGenerator.generateRandomString(), A_VALUE, 0); 38 | } catch (Exception e) { 39 | errors.set(errors.get() + 1); 40 | } 41 | } 42 | writerFinished1.set(true); 43 | } 44 | }; 45 | Thread writer2 = new Thread() { 46 | @Override 47 | public void run() { 48 | for (int i = 0; i < ITERATIONS; i++) { 49 | try { 50 | cache.set(RandomGenerator.generateRandomString(), ANOTHER_VALUE, 0); 51 | } catch (Exception e) { 52 | errors.set(errors.get() + 1); 53 | } 54 | } 55 | writerFinished2.set(true); 56 | } 57 | }; 58 | Thread reader = new Thread() { 59 | @Override 60 | public void run() { 61 | while (!writerFinished1.get() || !writerFinished2.get()) { 62 | try { 63 | cache.get(RandomGenerator.generateRandomString()); 64 | } catch (Exception e) { 65 | errors.set(errors.get() + 1); 66 | } 67 | } 68 | readerFinished.set(true); 69 | } 70 | }; 71 | Thread remover = new Thread() { 72 | @Override 73 | public void run() { 74 | while (!writerFinished1.get() || !writerFinished2.get()) { 75 | try { 76 | cache.remove(RandomGenerator.generateRandomString()); 77 | } catch (Exception e) { 78 | errors.set(errors.get() + 1); 79 | } 80 | } 81 | removerFinished.set(true); 82 | } 83 | }; 84 | writer1.start(); 85 | writer2.start(); 86 | reader.start(); 87 | remover.start(); 88 | 89 | while (!removerFinished.get() || !readerFinished.get() || !writerFinished1.get() || !writerFinished2.get()) { 90 | Thread.sleep(100); 91 | assertEquals(0, errors.get()); 92 | } 93 | } 94 | 95 | @Test 96 | public void canEditWhileIteratingWithOneKey() throws InterruptedException { 97 | final AtomicInteger errors = new AtomicInteger(0); 98 | final AtomicBoolean writerFinished1 = new AtomicBoolean(false); 99 | final AtomicBoolean writerFinished2 = new AtomicBoolean(false); 100 | final AtomicBoolean readerFinished = new AtomicBoolean(false); 101 | final AtomicBoolean removerFinished = new AtomicBoolean(false); 102 | 103 | Thread writer1 = new Thread() { 104 | @Override 105 | public void run() { 106 | for (int i = 0; i < ITERATIONS; i++) { 107 | try { 108 | cache.set(A_KEY, RandomGenerator.generateRandomString(), 0); 109 | } catch (Exception e) { 110 | errors.set(errors.get() + 1); 111 | } 112 | } 113 | writerFinished1.set(true); 114 | } 115 | }; 116 | Thread writer2 = new Thread() { 117 | @Override 118 | public void run() { 119 | for (int i = 0; i < ITERATIONS; i++) { 120 | try { 121 | cache.set(A_KEY, RandomGenerator.generateRandomString(), 0); 122 | } catch (Exception e) { 123 | errors.set(errors.get() + 1); 124 | } 125 | } 126 | writerFinished2.set(true); 127 | } 128 | }; 129 | Thread reader = new Thread() { 130 | @Override 131 | public void run() { 132 | while (!writerFinished1.get() || !writerFinished2.get()) { 133 | try { 134 | cache.get(A_KEY); 135 | } catch (Exception e) { 136 | errors.set(errors.get() + 1); 137 | } 138 | } 139 | readerFinished.set(true); 140 | } 141 | }; 142 | Thread remover = new Thread() { 143 | @Override 144 | public void run() { 145 | while (!writerFinished1.get() || !writerFinished2.get()) { 146 | try { 147 | cache.remove(A_KEY); 148 | } catch (Exception e) { 149 | errors.set(errors.get() + 1); 150 | } 151 | } 152 | removerFinished.set(true); 153 | } 154 | }; 155 | writer1.start(); 156 | writer2.start(); 157 | reader.start(); 158 | remover.start(); 159 | 160 | while (!removerFinished.get() || !readerFinished.get() || !writerFinished1.get() || !writerFinished2.get()) { 161 | Thread.sleep(100); 162 | assertEquals(0, errors.get()); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/com/fewlaps/quitnowcache/QNCache.kt: -------------------------------------------------------------------------------- 1 | package com.fewlaps.quitnowcache 2 | 3 | import java.util.* 4 | import java.util.concurrent.ConcurrentHashMap 5 | import java.util.concurrent.Executors 6 | import java.util.concurrent.ScheduledExecutorService 7 | import java.util.concurrent.TimeUnit 8 | 9 | class QNCache( 10 | val caseSensitiveKeys: Boolean = true, 11 | val autoReleaseInSeconds: Int = WITHOUT_AUTORELEASE, 12 | val defaultKeepaliveInMillis: Long = KEEPALIVE_FOREVER) { 13 | 14 | private val cache: ConcurrentHashMap> = ConcurrentHashMap() 15 | 16 | private var executorService: ScheduledExecutorService? = null 17 | 18 | var dateProvider = DateProvider.SYSTEM!! 19 | 20 | init { 21 | startAutoReleaseServiceIfNeeded() 22 | } 23 | 24 | /** 25 | * The common isEmpty() method, but only looking for alive elements 26 | */ 27 | fun isEmpty() = sizeAliveElements() == 0 28 | 29 | fun shutdown() { 30 | clear() 31 | executorService?.shutdown() 32 | } 33 | 34 | private fun startAutoReleaseServiceIfNeeded() { 35 | if (autoReleaseInSeconds > 0) { 36 | executorService = Executors.newSingleThreadScheduledExecutor() 37 | executorService?.scheduleAtFixedRate({ purge() }, autoReleaseInSeconds.toLong(), autoReleaseInSeconds.toLong(), TimeUnit.SECONDS) 38 | } 39 | } 40 | 41 | operator fun set(key: String, value: T) { 42 | set(key, value, defaultKeepaliveInMillis) 43 | } 44 | 45 | operator fun set(key: String, value: T, keepAliveUnits: Long, timeUnit: TimeUnit) { 46 | set(key, value, timeUnit.toMillis(keepAliveUnits)) 47 | } 48 | 49 | operator fun set(key: String, value: T, keepAliveInMillis: Long) { 50 | if (keepAliveInMillis >= 0) { 51 | cache[getEffectiveKey(key)] = QNCacheBean(value, now(), keepAliveInMillis) 52 | } 53 | } 54 | 55 | /** 56 | * Gets an element from the cache. If it's null, the default value will be returned instead. 57 | */ 58 | fun getOrDefault(key: String, defaultValue: T): T = get(key) ?: defaultValue 59 | 60 | /** 61 | * Gets an element from the cache. 62 | */ 63 | operator fun get(key: String) = get(key, purgeIfDead = false) 64 | 65 | /** 66 | * Gets an element from the cache. If the element exists but is dead, 67 | * it will be removed from the cache to free memory. It could call 68 | * an internal synchronized method, so avoid calling this method if 69 | * you are not storing huge objects in terms of memory. 70 | */ 71 | fun getAndPurgeIfDead(key: String) = get(key, purgeIfDead = true) 72 | 73 | private fun get(key: String, purgeIfDead: Boolean): T? { 74 | val effectiveKey = getEffectiveKey(key) 75 | val retrievedValue = cache[effectiveKey] 76 | 77 | return when { 78 | retrievedValue == null -> null 79 | retrievedValue.isAlive(now()) -> retrievedValue.value 80 | else -> { 81 | if (purgeIfDead) { 82 | cache.remove(effectiveKey) 83 | } 84 | null 85 | } 86 | } 87 | } 88 | 89 | fun remove(key: String) { 90 | val effectiveKey = getEffectiveKey(key) 91 | cache.remove(effectiveKey) 92 | } 93 | 94 | /** 95 | * Removes all the elements of the cache, ignoring if they're dead or alive 96 | */ 97 | fun clear() = cache.clear() 98 | 99 | fun keySet() = keySetAlive() 100 | 101 | fun keySetDeadAndAlive(): List = cache.keys().toList() 102 | 103 | fun keySetAlive(): List { 104 | return keySetDeadAndAlive().filter { 105 | isKeyAlive(it) 106 | } 107 | } 108 | 109 | fun keySetDead(): List { 110 | return keySetDeadAndAlive().filter { 111 | isKeyDead(it) 112 | } 113 | } 114 | 115 | fun keySetStartingWith(start: String?): List { 116 | if (start == null) return Collections.emptyList() 117 | 118 | val effectiveKeyStartingWith = getEffectiveKey(start) 119 | 120 | return keySetDeadAndAlive().filter { 121 | it.startsWith(effectiveKeyStartingWith) 122 | } 123 | } 124 | 125 | fun keySetAliveStartingWith(start: String?): List { 126 | if (start == null) return Collections.emptyList() 127 | 128 | return keySetStartingWith(start).filter { 129 | isKeyAlive(it) 130 | } 131 | } 132 | 133 | fun isKeyAlive(key: String): Boolean { 134 | val value = cache[key] ?: return false 135 | return value.isAlive(now()) 136 | } 137 | 138 | fun isKeyDead(key: String) = !isKeyAlive(key) 139 | 140 | /** 141 | * Counts how much alive elements are living in the cache 142 | */ 143 | fun size() = sizeAliveElements() 144 | 145 | /** 146 | * Counts how much alive elements are living in the cache 147 | */ 148 | fun sizeAliveElements(): Int { 149 | return cache.values.filter { it.isAlive(now()) }.count() 150 | } 151 | 152 | /** 153 | * Counts how much dead elements exist in the cache 154 | */ 155 | fun sizeDeadElements() = cache.size - sizeAliveElements() 156 | 157 | /** 158 | * Counts how much elements are living in the cache, ignoring if they are dead or alive 159 | */ 160 | fun sizeDeadAndAliveElements() = cache.size 161 | 162 | /** 163 | * The common contains() method that looks for alive elements 164 | */ 165 | operator fun contains(key: String) = get(getEffectiveKey(key)) != null 166 | 167 | /** 168 | * Removes the dead elements of the cache to free memory 169 | */ 170 | fun purge() = cache.entries.removeIf { isKeyDead(it.key) } 171 | 172 | /** 173 | * If caseSensitiveKeys is false, it returns a key in lowercase. It will be 174 | * the key of all stored values, so the cache will be totally caseinsensitive 175 | */ 176 | private fun getEffectiveKey(key: String): String { 177 | return when { 178 | caseSensitiveKeys -> key 179 | else -> key.toLowerCase() 180 | } 181 | } 182 | 183 | private fun now(): Long { 184 | return dateProvider.now() 185 | } 186 | 187 | class Builder { 188 | private var caseSensitiveKeys = true 189 | private var autoReleaseInSeconds: Int = QNCache.WITHOUT_AUTORELEASE 190 | private var defaultKeepaliveInMillis = QNCache.KEEPALIVE_FOREVER 191 | 192 | fun caseSensitiveKeys(caseSensitiveKeys: Boolean): Builder { 193 | this.caseSensitiveKeys = caseSensitiveKeys 194 | return this 195 | } 196 | 197 | fun autoRelease(units: Int, timeUnit: TimeUnit): Builder { 198 | this.autoReleaseInSeconds = java.lang.Long.valueOf(timeUnit.toSeconds(units.toLong())).toInt() 199 | return this 200 | } 201 | 202 | fun autoReleaseInSeconds(autoReleaseInSeconds: Int): Builder { 203 | this.autoReleaseInSeconds = autoReleaseInSeconds 204 | return this 205 | } 206 | 207 | fun defaultKeepalive(units: Int, timeUnit: TimeUnit): Builder { 208 | this.defaultKeepaliveInMillis = timeUnit.toMillis(units.toLong()) 209 | return this 210 | } 211 | 212 | fun defaultKeepaliveInMillis(defaultKeepaliveInMillis: Long): Builder { 213 | this.defaultKeepaliveInMillis = defaultKeepaliveInMillis 214 | return this 215 | } 216 | 217 | fun build(): QNCache { 218 | if (autoReleaseInSeconds < 0) { 219 | this.autoReleaseInSeconds = WITHOUT_AUTORELEASE 220 | } 221 | if (defaultKeepaliveInMillis < 0) { 222 | this.defaultKeepaliveInMillis = KEEPALIVE_FOREVER 223 | } 224 | return QNCache(caseSensitiveKeys, autoReleaseInSeconds, defaultKeepaliveInMillis) 225 | } 226 | } 227 | 228 | companion object { 229 | const val WITHOUT_AUTORELEASE: Int = 0 230 | const val KEEPALIVE_FOREVER: Long = 0 231 | } 232 | } --------------------------------------------------------------------------------