diff --git a/pom.xml b/pom.xml index b1cbbd1..35d50af 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,11 @@ snakeyaml 1.10 + + com.google.guava + guava + 23.0 + junit junit diff --git a/src/main/java/ua_parser/ConcurrentCachingParser.java b/src/main/java/ua_parser/ConcurrentCachingParser.java new file mode 100644 index 0000000..e67cbb1 --- /dev/null +++ b/src/main/java/ua_parser/ConcurrentCachingParser.java @@ -0,0 +1,127 @@ +package ua_parser; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.TimeUnit; + +/** + * This class is concurrent version of CachingParser. + * + * @author kyryloholodnov + */ +public class ConcurrentCachingParser extends Parser { + + private static long cacheKeyExpiresAfterAccessMs = 24 * 60L * 60 * 1000; // 24 hours + private static long cacheMaximumSize = 1000; + + private final LoadingCache cacheClient = CacheBuilder.newBuilder() + .expireAfterAccess(cacheKeyExpiresAfterAccessMs, TimeUnit.MILLISECONDS) + .maximumSize(cacheMaximumSize) + .build(new CacheLoader() { + @Override + @ParametersAreNonnullByDefault + public Client load(String agentString) throws Exception { + return ConcurrentCachingParser.super.parse(agentString); + } + }); + + private final LoadingCache cacheUserAgent = CacheBuilder.newBuilder() + .expireAfterAccess(cacheKeyExpiresAfterAccessMs, TimeUnit.MILLISECONDS) + .maximumSize(cacheMaximumSize) + .build(new CacheLoader() { + @Override + @ParametersAreNonnullByDefault + public UserAgent load(String agentString) throws Exception { + return ConcurrentCachingParser.super.parseUserAgent(agentString); + } + }); + + private final LoadingCache cacheDevice = CacheBuilder.newBuilder() + .expireAfterAccess(cacheKeyExpiresAfterAccessMs, TimeUnit.MILLISECONDS) + .maximumSize(cacheMaximumSize) + .build(new CacheLoader() { + @Override + @ParametersAreNonnullByDefault + public Device load(String agentString) throws Exception { + return ConcurrentCachingParser.super.parseDevice(agentString); + } + }); + + private final LoadingCache cacheOS = CacheBuilder.newBuilder() + .expireAfterAccess(cacheKeyExpiresAfterAccessMs, TimeUnit.MILLISECONDS) + .maximumSize(cacheMaximumSize) + .build(new CacheLoader() { + @Override + @ParametersAreNonnullByDefault + public OS load(String agentString) throws Exception { + return ConcurrentCachingParser.super.parseOS(agentString); + } + }); + + public ConcurrentCachingParser() throws IOException { + super(); + } + + public ConcurrentCachingParser(InputStream regexYaml) { + super(regexYaml); + } + + @Override + public Client parse(String agentString) { + if (agentString == null) { + return null; + } + return cacheClient.getUnchecked(agentString); + } + + @Override + public UserAgent parseUserAgent(String agentString) { + if (agentString == null) { + return null; + } + return cacheUserAgent.getUnchecked(agentString); + } + + @Override + public Device parseDevice(String agentString) { + if (agentString == null) { + return null; + } + return cacheDevice.getUnchecked(agentString); + } + + @Override + public OS parseOS(String agentString) { + if (agentString == null) { + return null; + } + return cacheOS.getUnchecked(agentString); + } + + public static long getCacheKeyExpiresAfterAccessMs() { + return cacheKeyExpiresAfterAccessMs; + } + + public static long getCacheMaximumSize() { + return cacheMaximumSize; + } + + public static void setCacheMaximumSize(long cacheMaximumSize) { + if (cacheMaximumSize < 1) { + throw new IllegalArgumentException("Cache size should be positive value"); + } + ConcurrentCachingParser.cacheMaximumSize = cacheMaximumSize; + } + + public static void setCacheKeyExpiresAfterAccessMs(long cacheKeyExpiresAfterAccessMs) { + if (cacheKeyExpiresAfterAccessMs < 1) { + throw new IllegalArgumentException("Cache key expiration should be positive value"); + } + ConcurrentCachingParser.cacheKeyExpiresAfterAccessMs = cacheKeyExpiresAfterAccessMs; + } +} diff --git a/src/test/java/ua_parser/ConcurrentCachingParserTest.java b/src/test/java/ua_parser/ConcurrentCachingParserTest.java new file mode 100644 index 0000000..198af63 --- /dev/null +++ b/src/test/java/ua_parser/ConcurrentCachingParserTest.java @@ -0,0 +1,86 @@ +package ua_parser; + +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * These tests really only redo the same tests as in ParserTest but with a + * different Parser subclass Also the same tests will be run several times on + * the same user agents to validate the caching works correctly. + * + * @author kyryloholodnov + */ +public class ConcurrentCachingParserTest extends ParserTest { + + @Before + public void initParser() throws Exception { + parser = new ConcurrentCachingParser(); + } + + @Override + Parser parserFromStringConfig(String configYamlAsString) throws Exception { + InputStream yamlInput = new ByteArrayInputStream( + configYamlAsString.getBytes("UTF8")); + return new CachingParser(yamlInput); + } + + @Test + public void testCachedParseUserAgent() { + super.testParseUserAgent(); + super.testParseUserAgent(); + super.testParseUserAgent(); + } + + @Test + public void testCachedParseOS() { + super.testParseOS(); + super.testParseOS(); + super.testParseOS(); + } + + @Test + public void testCachedParseAdditionalOS() { + super.testParseAdditionalOS(); + super.testParseAdditionalOS(); + super.testParseAdditionalOS(); + } + + @Test + public void testCachedParseDevice() { + super.testParseDevice(); + super.testParseDevice(); + super.testParseDevice(); + } + + @Test + public void testCachedParseFirefox() { + super.testParseFirefox(); + super.testParseFirefox(); + super.testParseFirefox(); + } + + @Test + public void testCachedParsePGTS() { + super.testParsePGTS(); + super.testParsePGTS(); + super.testParsePGTS(); + } + + @Test + public void testCachedParseAll() { + super.testParseAll(); + super.testParseAll(); + super.testParseAll(); + } + + @Test + public void testCachedReplacementQuoting() throws Exception { + super.testReplacementQuoting(); + super.testReplacementQuoting(); + super.testReplacementQuoting(); + } + +}