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();
+ }
+
+}