diff --git a/README.md b/README.md index ce2da13..ac88fd4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Javadocs](http://www.javadoc.io/badge/com.qwazr/jdbc-cache-driver.svg)](http://www.javadoc.io/doc/com.qwazr/jdbc-cache-driver) [![Coverage Status](https://coveralls.io/repos/github/qwazr/jdbc-cache-driver/badge.svg?branch=master)](https://coveralls.io/github/qwazr/jdbc-cache-driver?branch=master) -JDBC-Driver-Cache is JDBC cache which store the result of a SQL query (ResultSet) in files. +JDBC-Driver-Cache is JDBC cache which store the result of a SQL query (ResultSet) in files or in memory. The same query requested again will be read from the file, the database is no more requested again. You may use it to easily mock ResultSets from a database. @@ -42,17 +42,29 @@ Class.forName("com.qwazr.jdbc.cache.Driver"); Properties info = new Properties(); info.setProperty("cache.driver.url", "jdbc:derby:memory:myDB;create=true"); info.setProperty("cache.driver.class", "org.apache.derby.jdbc.EmbeddedDriver"); +``` + +Use the file cache implementation: +```java // Get your JDBC connection Connection cnx = DriverManager.getConnection("jdbc:cache:file:/var/jdbc/cache", info); ``` +Or use the in memory cache implementation: + +```java +// Get your JDBC connection +Connection cnx = DriverManager.getConnection("jdbc:cache:mem:my-memory-cache", info); +``` + To build a connection you have to provide the URL and some properties. The URL tells the driver where to store the cached ResultSet. -The syntax of the URL is: +The syntax of the URL can be: -*jdbc:cache:file:{path-to-the-cache-directory}* +* *jdbc:cache:file:{path-to-the-cache-directory}* for on disk cache +* *jdbc:cache:mem:{name-of-the-cache}* for in memory cache Two possible properties: - **cache.driver.url** contains the typical JDBC URL of the backend driver. diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedCallableStatement.java b/src/main/java/com/qwazr/jdbc/cache/CachedCallableStatement.java index 59d5564..76a929c 100644 --- a/src/main/java/com/qwazr/jdbc/cache/CachedCallableStatement.java +++ b/src/main/java/com/qwazr/jdbc/cache/CachedCallableStatement.java @@ -29,7 +29,7 @@ class CachedCallableStatement extends CachedPreparedStatement private final SortedMap namedParameters; - CachedCallableStatement(final CachedConnection connection, final ResultSetCacheImpl resultSetCache, + CachedCallableStatement(final CachedConnection connection, final ResultSetCache resultSetCache, final CallableStatement backendStatement, final String sql, final int resultSetConcurrency, final int resultSetType, final int resultSetHoldability) { super(connection, resultSetCache, backendStatement, sql, resultSetConcurrency, resultSetType, @@ -37,7 +37,7 @@ class CachedCallableStatement extends CachedPreparedStatement this.namedParameters = new TreeMap<>(); } - CachedCallableStatement(final CachedConnection connection, final ResultSetCacheImpl resultSetCache, + CachedCallableStatement(final CachedConnection connection, final ResultSetCache resultSetCache, final CallableStatement backendStatement, final String sql) { this(connection, resultSetCache, backendStatement, sql, 0, 0, 0); } diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedConnection.java b/src/main/java/com/qwazr/jdbc/cache/CachedConnection.java index 1bd31a8..87ed7e1 100644 --- a/src/main/java/com/qwazr/jdbc/cache/CachedConnection.java +++ b/src/main/java/com/qwazr/jdbc/cache/CachedConnection.java @@ -33,9 +33,9 @@ class CachedConnection implements Connection { private volatile String schema; private final Connection connection; - private final ResultSetCacheImpl resultSetCache; + private final ResultSetCache resultSetCache; - CachedConnection(final Connection backendConnection, final ResultSetCacheImpl resultSetCache) throws SQLException { + CachedConnection(final Connection backendConnection, final ResultSetCache resultSetCache) throws SQLException { this.connection = backendConnection; this.resultSetCache = resultSetCache; this.autocommit = false; @@ -49,7 +49,7 @@ class CachedConnection implements Connection { this.schema = null; } - ResultSetCacheImpl getResultSetCache() { + ResultSetCache getResultSetCache() { return resultSetCache; } diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedInMemoryResultSet.java b/src/main/java/com/qwazr/jdbc/cache/CachedInMemoryResultSet.java new file mode 100644 index 0000000..0e0910c --- /dev/null +++ b/src/main/java/com/qwazr/jdbc/cache/CachedInMemoryResultSet.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2017 Emmanuel Keller / QWAZR + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qwazr.jdbc.cache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.sql.SQLException; + +/** + * Uses ByteArrayInputStream/ByteArrayOutputStream as the storage implementation. + * Everything is hold in memory. + * Warning: this is a super naive implementation. Not designed to run in production + * as lot of memory is going to be used by converting to byte[]. + */ +class CachedInMemoryResultSet extends CachedResultSet { + CachedInMemoryResultSet(final CachedStatement statement, ByteArrayOutputStream outputStream) throws SQLException { + super(statement, new DataInputStream(new ByteArrayInputStream(outputStream.toByteArray()))); + } +} diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedOnDiskResultSet.java b/src/main/java/com/qwazr/jdbc/cache/CachedOnDiskResultSet.java new file mode 100644 index 0000000..1b6dd09 --- /dev/null +++ b/src/main/java/com/qwazr/jdbc/cache/CachedOnDiskResultSet.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2017 Emmanuel Keller / QWAZR + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qwazr.jdbc.cache; + +import java.io.DataInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.zip.GZIPInputStream; + +/** + * Uses disk persistence for caching + */ +class CachedOnDiskResultSet extends CachedResultSet { + CachedOnDiskResultSet(final CachedStatement statement, final Path resultSetPath) throws SQLException, IOException { + super(statement, new DataInputStream(new GZIPInputStream(new FileInputStream(resultSetPath.toFile())))); + } +} diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedPreparedStatement.java b/src/main/java/com/qwazr/jdbc/cache/CachedPreparedStatement.java index 308aae3..7ddd24d 100644 --- a/src/main/java/com/qwazr/jdbc/cache/CachedPreparedStatement.java +++ b/src/main/java/com/qwazr/jdbc/cache/CachedPreparedStatement.java @@ -28,7 +28,7 @@ class CachedPreparedStatement extends CachedStateme final SortedMap parameters; - CachedPreparedStatement(final CachedConnection connection, final ResultSetCacheImpl resultSetCache, + CachedPreparedStatement(final CachedConnection connection, final ResultSetCache resultSetCache, final T backendStatement, final String sql, final int resultSetConcurrency, final int resultSetType, final int resultSetHoldability) { super(connection, resultSetCache, backendStatement, resultSetConcurrency, resultSetType, resultSetHoldability); @@ -36,7 +36,7 @@ class CachedPreparedStatement extends CachedStateme this.executedSql = sql; } - CachedPreparedStatement(final CachedConnection connection, final ResultSetCacheImpl resultSetCache, + CachedPreparedStatement(final CachedConnection connection, final ResultSetCache resultSetCache, final T backendStatement, final String sql) { this(connection, resultSetCache, backendStatement, sql, 0, 0, 0); } @@ -55,7 +55,7 @@ protected void generateKey() throws SQLException { @Override public ResultSet executeQuery() throws SQLException { generateKey(); - return resultSetCache.get(this, generatedKey, backendStatement != null ? backendStatement::executeQuery : null); + return resultSetCache.get(this, generatedKey, backendStatement != null ? () -> backendStatement.executeQuery() : null); } @Override diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedResultSet.java b/src/main/java/com/qwazr/jdbc/cache/CachedResultSet.java index b5aaeac..03b086e 100644 --- a/src/main/java/com/qwazr/jdbc/cache/CachedResultSet.java +++ b/src/main/java/com/qwazr/jdbc/cache/CachedResultSet.java @@ -15,20 +15,40 @@ */ package com.qwazr.jdbc.cache; -import java.io.*; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URL; -import java.nio.file.Path; -import java.sql.*; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; import java.text.DateFormat; import java.text.ParseException; import java.util.Calendar; import java.util.HashMap; import java.util.Map; -import java.util.zip.GZIPInputStream; -class CachedResultSet implements ResultSet { +/** + * Cached ResultSet + */ +abstract class CachedResultSet implements ResultSet { private final CachedStatement statement; private final DataInputStream input; @@ -41,17 +61,13 @@ class CachedResultSet implements ResultSet { private volatile int nextPos; private volatile boolean closed; - CachedResultSet(final CachedStatement statement, final Path resultSetPath) throws SQLException { + CachedResultSet(final CachedStatement statement, DataInputStream input) throws SQLException { this.statement = statement; this.wasNull = false; this.currentPos = 0; this.nextPos = 0; this.closed = false; - try { - this.input = new DataInputStream(new GZIPInputStream(new FileInputStream(resultSetPath.toFile()))); - } catch (IOException e) { - throw new SQLException("Error while opening the file " + resultSetPath, e); - } + this.input = input; try { this.metaData = new CachedResultSetMetaData(ResultSetWriter.readColumns(input)); this.currentRow = new Object[metaData.columns.length]; @@ -67,7 +83,7 @@ class CachedResultSet implements ResultSet { } catch (Exception ex) { //Close quietly } - throw new SQLException("Cannot read the cache: " + resultSetPath, e); + throw new SQLException("Cannot read the cache for statement " + statement, e); } } diff --git a/src/main/java/com/qwazr/jdbc/cache/CachedStatement.java b/src/main/java/com/qwazr/jdbc/cache/CachedStatement.java index 9810caa..f659e39 100644 --- a/src/main/java/com/qwazr/jdbc/cache/CachedStatement.java +++ b/src/main/java/com/qwazr/jdbc/cache/CachedStatement.java @@ -16,6 +16,7 @@ package com.qwazr.jdbc.cache; import javax.xml.bind.DatatypeConverter; +import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.Connection; @@ -28,7 +29,7 @@ class CachedStatement implements Statement { private final CachedConnection connection; - final ResultSetCacheImpl resultSetCache; + final ResultSetCache resultSetCache; final T backendStatement; private final int resultSetConcurrency; @@ -47,7 +48,7 @@ class CachedStatement implements Statement { volatile String executedSql; volatile String generatedKey; - CachedStatement(final CachedConnection connection, final ResultSetCacheImpl resultSetCache, + CachedStatement(final CachedConnection connection, final ResultSetCache resultSetCache, final T backendStatement, final int resultSetConcurrency, final int resultSetType, final int resultSetHoldability) { this.connection = connection; @@ -67,7 +68,7 @@ class CachedStatement implements Statement { this.executedSql = null; } - CachedStatement(final CachedConnection connection, final ResultSetCacheImpl resultSetCache, + CachedStatement(final CachedConnection connection, final ResultSetCache resultSetCache, final T backendStatement) { this(connection, resultSetCache, backendStatement, 0, 0, 0); } diff --git a/src/main/java/com/qwazr/jdbc/cache/Driver.java b/src/main/java/com/qwazr/jdbc/cache/Driver.java index 5caf6aa..42b1ab7 100644 --- a/src/main/java/com/qwazr/jdbc/cache/Driver.java +++ b/src/main/java/com/qwazr/jdbc/cache/Driver.java @@ -31,7 +31,8 @@ public class Driver implements java.sql.Driver { final static Logger LOGGER = Logger.getLogger(Driver.class.getPackage().getName()); - public final static String URL_PREFIX = "jdbc:cache:file:"; + public final static String URL_FILE_PREFIX = "jdbc:cache:file:"; + public final static String URL_MEM_PREFIX = "jdbc:cache:mem:"; public final static String CACHE_DRIVER_URL = "cache.driver.url"; public final static String CACHE_DRIVER_CLASS = "cache.driver.class"; public final static String CACHE_DRIVER_ACTIVE = "cache.driver.active"; @@ -44,7 +45,7 @@ public class Driver implements java.sql.Driver { } } - private final ConcurrentHashMap resultSetCacheMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap resultSetCacheMap = new ConcurrentHashMap<>(); public Connection connect(String url, Properties info) throws SQLException { @@ -68,28 +69,35 @@ public Connection connect(String url, Properties info) throws SQLException { null : DriverManager.getConnection(cacheDriverUrl, info); - if (url.length() <= URL_PREFIX.length()) - throw new SQLException("The path is empty: " + url); - - if (!active) + if (!active) { return new CachedConnection(backendConnection, null); + } - // Check the cache directory - final Path cacheDirectory = FileSystems.getDefault().getPath(url.substring(URL_PREFIX.length())); - - final ResultSetCacheImpl resultSetCache; - - try { - resultSetCache = resultSetCacheMap.computeIfAbsent(cacheDirectory, ResultSetCacheImpl::new); - } catch (CacheException e) { - throw e.getSQLException(); + final ResultSetCache resultSetCache; + if (url.startsWith(URL_FILE_PREFIX)) { + if (url.length() <= URL_FILE_PREFIX.length()) { + throw new SQLException("The path is empty: " + url); + } + // Check the cache directory + final String cacheName = url.substring(URL_FILE_PREFIX.length()); + final Path cacheDirectory = FileSystems.getDefault().getPath(cacheName); + resultSetCache = resultSetCacheMap.computeIfAbsent(cacheName, (foo) -> new ResultSetOnDiskCacheImpl(cacheDirectory)); + } else if (url.startsWith(URL_MEM_PREFIX)) { + if (url.length() <= URL_MEM_PREFIX.length()) { + throw new SQLException("The name is empty: " + url); + } + // Check the cache directory + final String cacheName = url.substring(URL_MEM_PREFIX.length()); + resultSetCache = resultSetCacheMap.computeIfAbsent(cacheName, (foo) -> new ResultSetInMemoryCacheImpl()); + } else { + throw new IllegalArgumentException("Can not find cache implementation for " + url); } return new CachedConnection(backendConnection, resultSetCache); } public boolean acceptsURL(String url) throws SQLException { - return url != null && url.startsWith(URL_PREFIX); + return url != null && (url.startsWith(URL_FILE_PREFIX) || url.startsWith(URL_MEM_PREFIX)); } public DriverPropertyInfo[] getPropertyInfo(final String url, final Properties info) throws SQLException { diff --git a/src/main/java/com/qwazr/jdbc/cache/ResultSetCache.java b/src/main/java/com/qwazr/jdbc/cache/ResultSetCache.java index 0f6ec25..2363083 100644 --- a/src/main/java/com/qwazr/jdbc/cache/ResultSetCache.java +++ b/src/main/java/com/qwazr/jdbc/cache/ResultSetCache.java @@ -15,6 +15,8 @@ */ package com.qwazr.jdbc.cache; +import java.io.IOException; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -58,4 +60,12 @@ public interface ResultSetCache { * @return true if a cache entry is currently build for the given statement */ boolean active(Statement stmt) throws SQLException; + + ResultSet get(CachedStatement statement, String key, Provider s) throws SQLException; + + boolean checkIfExists(String key); + + interface Provider { + ResultSet provide() throws SQLException, IOException; + } } diff --git a/src/main/java/com/qwazr/jdbc/cache/ResultSetCacheImpl.java b/src/main/java/com/qwazr/jdbc/cache/ResultSetCacheImpl.java index 4bca1eb..f2ab25e 100644 --- a/src/main/java/com/qwazr/jdbc/cache/ResultSetCacheImpl.java +++ b/src/main/java/com/qwazr/jdbc/cache/ResultSetCacheImpl.java @@ -15,90 +15,23 @@ */ package com.qwazr.jdbc.cache; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.io.ByteArrayOutputStream; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; -class ResultSetCacheImpl implements ResultSetCache { +abstract class ResultSetCacheImpl implements ResultSetCache { - private final Path cacheDirectory; private final ConcurrentHashMap activeKeys; + private final ConcurrentHashMap cache; - ResultSetCacheImpl(final Path cacheDirectory) { - if (!Files.exists(cacheDirectory)) { - try { - Files.createDirectories(cacheDirectory); - } catch (IOException e) { - throw CacheException.of("Cannot create the cache directory: " + cacheDirectory, e); - } - } - if (!Files.isDirectory(cacheDirectory)) - throw CacheException - .of("The path is not a directory, or the directory cannot be created: " + cacheDirectory); - this.cacheDirectory = cacheDirectory; + ResultSetCacheImpl() { this.activeKeys = new ConcurrentHashMap<>(); - } - - /** - * Return the cached ResultSet for the given key. - * If the entry does not exist the ResultSet is extracted by calling the given resultSetProvider. - * If the entry does not exist and no resultSetProvider is given (null) an SQLException is thrown. - * - * @param statement the cached statement - * @param key the generated key for this statement - * @param resultSetProvider the optional result provider - * @return the cached ResultSet - * @throws SQLException if the statement cannot be executed - */ - final CachedResultSet get(final CachedStatement statement, final String key, final Provider resultSetProvider) - throws SQLException { - final Path resultSetPath = cacheDirectory.resolve(key); - if (!Files.exists(resultSetPath)) { - if (resultSetProvider == null) - throw new SQLException("No cache available"); - buildCache(key, resultSetPath, resultSetProvider); - } - return new CachedResultSet(statement, resultSetPath); - } - - private void buildCache(final String key, final Path resultSetPath, final Provider resultSetProvider) - throws SQLException { - final Lock keyLock = activeKeys.computeIfAbsent(key, s -> new ReentrantLock(true)); - try { - keyLock.lock(); - try { - final Path tempPath = cacheDirectory.resolve(key + ".tmp"); - try { - final ResultSet providedResultSet = resultSetProvider.provide(); - ResultSetWriter.write(tempPath, providedResultSet); - Files.move(tempPath, resultSetPath, StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - throw new SQLException("Failed in renaming the file " + tempPath, e); - - } finally { - try { - Files.deleteIfExists(tempPath); - } catch (IOException e) { - // Quiet - } - } - } finally { - keyLock.unlock(); - } - } finally { - activeKeys.remove(key); - } + this.cache = new ConcurrentHashMap<>(); } /** @@ -108,39 +41,13 @@ private void buildCache(final String key, final Path resultSetPath, final Provid * @return always true if the cache entry exists */ - final boolean checkIfExists(final String key) { - final Path resultSetPath = cacheDirectory.resolve(key); - return Files.exists(resultSetPath); - } - - private void parse(final Consumer consumer) throws SQLException { - try { - synchronized (cacheDirectory) { - Files.list(cacheDirectory).forEach(path -> { - if (!path.endsWith(".tmp")) - consumer.accept(path); - }); - } - } catch (CacheException e) { - throw e.getSQLException(); - } catch (IOException e) { - throw CacheException.of(e).getSQLException(); - } + public boolean checkIfExists(final String key) { + return cache.containsKey(key); } @Override public void flush() throws SQLException { - parse(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - throw CacheException.of(e); - } - }); - } - - private Path checkCacheDirectory() { - return Objects.requireNonNull(cacheDirectory, "No cache directory"); + cache.clear(); } private ConcurrentHashMap checkCacheMap() { @@ -154,34 +61,10 @@ private CachedStatement checkCachedStatement(final Statement stmt) throws SQLExc throw new SQLException("The statement is not cached"); } - private String checkKey(final Statement stmt) throws SQLException { + String checkKey(final Statement stmt) throws SQLException { return Objects.requireNonNull(checkCachedStatement(stmt).getOrGenerateKey(), "No key found"); } - @Override - public void flush(final Statement stmt) throws SQLException { - try { - Files.deleteIfExists(cacheDirectory.resolve(checkKey(stmt))); - } catch (IOException e) { - throw CacheException.of(e); - } - } - - @Override - public int size() throws SQLException { - final AtomicInteger counter = new AtomicInteger(); - parse(path -> { - if (!path.endsWith(".tmp")) - counter.incrementAndGet(); - }); - return counter.get(); - } - - @Override - public boolean exists(Statement stmt) throws SQLException { - return Files.exists(checkCacheDirectory().resolve(checkKey(stmt))); - } - @Override public int active() { return checkCacheMap().size(); @@ -191,10 +74,4 @@ public int active() { public boolean active(Statement stmt) throws SQLException { return checkCacheMap().containsKey(checkKey(stmt)); } - - interface Provider { - - ResultSet provide() throws SQLException; - } - } diff --git a/src/main/java/com/qwazr/jdbc/cache/ResultSetInMemoryCacheImpl.java b/src/main/java/com/qwazr/jdbc/cache/ResultSetInMemoryCacheImpl.java new file mode 100644 index 0000000..b1e80f5 --- /dev/null +++ b/src/main/java/com/qwazr/jdbc/cache/ResultSetInMemoryCacheImpl.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016-2017 Emmanuel Keller / QWAZR + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qwazr.jdbc.cache; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +class ResultSetInMemoryCacheImpl extends ResultSetCacheImpl { + + private final ConcurrentHashMap activeKeys; + private final ConcurrentHashMap cache; + + ResultSetInMemoryCacheImpl() { + this.activeKeys = new ConcurrentHashMap<>(); + this.cache = new ConcurrentHashMap<>(); + } + + /** + * Return the cached ResultSet for the given key. + * If the entry does not exist the ResultSet is extracted by calling the given resultSetProvider. + * If the entry does not exist and no resultSetProvider is given (null) an SQLException is thrown. + * + * @param statement the cached statement + * @param key the generated key for this statement + * @param resultSetProvider the optional result provider + * @return the cached ResultSet + * @throws SQLException if the statement cannot be executed + */ + public ResultSet get(final CachedStatement statement, final String key, final ResultSetCache.Provider resultSetProvider) + throws SQLException { + if (!cache.containsKey(key)) { + if (resultSetProvider == null) + throw new SQLException("No cache available"); + try { + buildCache(key, resultSetProvider); + } catch (IOException e) { + throw new SQLException("Can not read cache", e); + } + } + return new CachedInMemoryResultSet(statement, cache.get(key)); + } + + private void buildCache(final String key, final Provider resultSetProvider) + throws SQLException, IOException { + final Lock keyLock = activeKeys.computeIfAbsent(key, s -> new ReentrantLock(true)); + try { + keyLock.lock(); + try { + final ResultSet providedResultSet = resultSetProvider.provide(); + ByteArrayOutputStream outputStream = ResultSetWriter.write(providedResultSet); + cache.put(key, outputStream); + } finally { + keyLock.unlock(); + } + } finally { + activeKeys.remove(key); + } + } + + /** + * Check if an entry is available for this key. + * + * @param key the computed key + * @return always true if the cache entry exists + */ + + public boolean checkIfExists(final String key) { + return cache.containsKey(key); + } + + @Override + public void flush() throws SQLException { + cache.clear(); + } + + @Override + public void flush(final Statement stmt) throws SQLException { + cache.remove(checkKey(stmt)); + } + + @Override + public int size() throws SQLException { + return cache.size(); + } + + @Override + public boolean exists(Statement stmt) throws SQLException { + return cache.containsKey(checkKey(stmt)); + } +} diff --git a/src/main/java/com/qwazr/jdbc/cache/ResultSetOnDiskCacheImpl.java b/src/main/java/com/qwazr/jdbc/cache/ResultSetOnDiskCacheImpl.java new file mode 100644 index 0000000..eb24c77 --- /dev/null +++ b/src/main/java/com/qwazr/jdbc/cache/ResultSetOnDiskCacheImpl.java @@ -0,0 +1,173 @@ +/* + * Copyright 2016-2017 Emmanuel Keller / QWAZR + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qwazr.jdbc.cache; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +class ResultSetOnDiskCacheImpl extends ResultSetCacheImpl { + + private final Path cacheDirectory; + private final ConcurrentHashMap activeKeys; + + ResultSetOnDiskCacheImpl(final Path cacheDirectory) { + if (!Files.exists(cacheDirectory)) { + try { + Files.createDirectories(cacheDirectory); + } catch (IOException e) { + throw CacheException.of("Cannot create the cache directory: " + cacheDirectory, e); + } + } + if (!Files.isDirectory(cacheDirectory)) + throw CacheException + .of("The path is not a directory, or the directory cannot be created: " + cacheDirectory); + this.cacheDirectory = cacheDirectory; + this.activeKeys = new ConcurrentHashMap<>(); + } + + /** + * Return the cached ResultSet for the given key. + * If the entry does not exist the ResultSet is extracted by calling the given resultSetProvider. + * If the entry does not exist and no resultSetProvider is given (null) an SQLException is thrown. + * + * @param statement the cached statement + * @param key the generated key for this statement + * @param resultSetProvider the optional result provider + * @return the cached ResultSet + * @throws SQLException if the statement cannot be executed + */ + public CachedOnDiskResultSet get(final CachedStatement statement, final String key, final Provider resultSetProvider) + throws SQLException { + final Path resultSetPath = cacheDirectory.resolve(key); + if (!Files.exists(resultSetPath)) { + if (resultSetProvider == null) + throw new SQLException("No cache available"); + buildCache(key, resultSetPath, resultSetProvider); + } + try { + return new CachedOnDiskResultSet(statement, resultSetPath); + } catch (IOException e) { + throw new SQLException("Can not read cache", e); + } + } + + private void buildCache(final String key, final Path resultSetPath, final Provider resultSetProvider) + throws SQLException { + final Lock keyLock = activeKeys.computeIfAbsent(key, s -> new ReentrantLock(true)); + try { + keyLock.lock(); + try { + final Path tempPath = cacheDirectory.resolve(key + ".tmp"); + try { + final ResultSet providedResultSet = resultSetProvider.provide(); + ResultSetWriter.write(tempPath, providedResultSet); + Files.move(tempPath, resultSetPath, StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + throw new SQLException("Failed in renaming the file " + tempPath, e); + + } finally { + try { + Files.deleteIfExists(tempPath); + } catch (IOException e) { + // Quiet + } + } + } finally { + keyLock.unlock(); + } + } finally { + activeKeys.remove(key); + } + } + + /** + * Check if an entry is available for this key. + * + * @param key the computed key + * @return always true if the cache entry exists + */ + + public boolean checkIfExists(final String key) { + final Path resultSetPath = cacheDirectory.resolve(key); + return Files.exists(resultSetPath); + } + + private void parse(final Consumer consumer) throws SQLException { + try { + synchronized (cacheDirectory) { + Files.list(cacheDirectory).forEach(path -> { + if (!path.endsWith(".tmp")) + consumer.accept(path); + }); + } + } catch (CacheException e) { + throw e.getSQLException(); + } catch (IOException e) { + throw CacheException.of(e).getSQLException(); + } + } + + @Override + public void flush() throws SQLException { + parse(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw CacheException.of(e); + } + }); + } + + private Path checkCacheDirectory() { + return Objects.requireNonNull(cacheDirectory, "No cache directory"); + } + + @Override + public void flush(final Statement stmt) throws SQLException { + try { + Files.deleteIfExists(cacheDirectory.resolve(checkKey(stmt))); + } catch (IOException e) { + throw CacheException.of(e); + } + } + + @Override + public int size() throws SQLException { + final AtomicInteger counter = new AtomicInteger(); + parse(path -> { + if (!path.endsWith(".tmp")) + counter.incrementAndGet(); + }); + return counter.get(); + } + + @Override + public boolean exists(Statement stmt) throws SQLException { + return Files.exists(checkCacheDirectory().resolve(checkKey(stmt))); + } +} diff --git a/src/main/java/com/qwazr/jdbc/cache/ResultSetWriter.java b/src/main/java/com/qwazr/jdbc/cache/ResultSetWriter.java index 0415109..fe8750a 100644 --- a/src/main/java/com/qwazr/jdbc/cache/ResultSetWriter.java +++ b/src/main/java/com/qwazr/jdbc/cache/ResultSetWriter.java @@ -36,6 +36,18 @@ static void write(final Path resultSetPath, final ResultSet resultSet) throws SQ } } + static ByteArrayOutputStream write(final ResultSet resultSet) throws SQLException { + try (final ByteArrayOutputStream fos = new ByteArrayOutputStream()) { + try (final DataOutputStream output = new DataOutputStream(fos)) { + writeMetadata(output, resultSet.getMetaData()); + writeResultSet(output, resultSet); + return fos; + } + } catch (IOException e) { + throw new SQLException("Error while writing the ResultSet cache", e); + } + } + private static void writeMetadata(final DataOutputStream output, final ResultSetMetaData metadata) throws IOException, SQLException { final int columnCount = metadata.getColumnCount(); diff --git a/src/test/java/com/qwazr/jdbc/cache/test/DerbyTest.java b/src/test/java/com/qwazr/jdbc/cache/DerbyTest.java similarity index 95% rename from src/test/java/com/qwazr/jdbc/cache/test/DerbyTest.java rename to src/test/java/com/qwazr/jdbc/cache/DerbyTest.java index 2eade13..1420974 100644 --- a/src/test/java/com/qwazr/jdbc/cache/test/DerbyTest.java +++ b/src/test/java/com/qwazr/jdbc/cache/DerbyTest.java @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.qwazr.jdbc.cache.test; +package com.qwazr.jdbc.cache; -import com.qwazr.jdbc.cache.ResultSetCache; import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.BeforeClass; @@ -52,28 +51,26 @@ import java.util.logging.Logger; @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class DerbyTest { +abstract public class DerbyTest { - final static Logger LOGGER = Logger.getLogger(DerbyTest.class.getName()); + private final static Logger LOGGER = Logger.getLogger(DerbyTest.class.getName()); private static Connection cnxCacheDisable; private static Connection cnxCacheEnable; private static Connection cnxCacheNoBackend; - private static String jdbcCacheUrl; + abstract Class expectedResultSetClass(); + abstract String getOrSetJdbcCacheUrl(); + abstract String getDerbyDbName(); @BeforeClass public static void init() throws ClassNotFoundException, IOException { Class.forName("com.qwazr.jdbc.cache.Driver"); - String tempDirPath = Files.createTempDirectory("jdbc-cache-test").toUri().getPath(); - if (tempDirPath.contains(":") && tempDirPath.startsWith("/")) - tempDirPath = tempDirPath.substring(1); - jdbcCacheUrl = "jdbc:cache:file:" + tempDirPath + File.separatorChar + "cache"; } @Test public void test000testDriver() throws SQLException { - Driver driver = DriverManager.getDriver(jdbcCacheUrl); + Driver driver = DriverManager.getDriver(getOrSetJdbcCacheUrl()); Assert.assertNotNull(driver); Assert.assertEquals(1, driver.getMajorVersion()); Assert.assertEquals(3, driver.getMinorVersion()); @@ -90,24 +87,24 @@ public void test000testDriver() throws SQLException { @Test public void test001initConnectionWithoutCache() throws SQLException, IOException, ClassNotFoundException { final Properties info = new Properties(); - info.setProperty("cache.driver.url", "jdbc:derby:memory:myDB;create=true"); + info.setProperty("cache.driver.url", "jdbc:derby:memory:" + getDerbyDbName() + ";create=true"); info.setProperty("cache.driver.class", "org.apache.derby.jdbc.EmbeddedDriver"); info.setProperty("cache.driver.active", "false"); - cnxCacheDisable = DriverManager.getConnection(jdbcCacheUrl, info); + cnxCacheDisable = DriverManager.getConnection(getOrSetJdbcCacheUrl(), info); Assert.assertNotNull(cnxCacheDisable); } @Test public void test002initConnectionWithCache() throws SQLException, IOException, ClassNotFoundException { final Properties info = new Properties(); - info.setProperty("cache.driver.url", "jdbc:derby:memory:myDB;create=true"); - cnxCacheEnable = DriverManager.getConnection(jdbcCacheUrl, info); + info.setProperty("cache.driver.url", "jdbc:derby:memory:" + getDerbyDbName() + ";create=true"); + cnxCacheEnable = DriverManager.getConnection(getOrSetJdbcCacheUrl(), info); Assert.assertNotNull(cnxCacheEnable); } @Test public void test003initConnectionNoBackend() throws SQLException, IOException, ClassNotFoundException { - cnxCacheNoBackend = DriverManager.getConnection(jdbcCacheUrl); + cnxCacheNoBackend = DriverManager.getConnection(getOrSetJdbcCacheUrl()); Assert.assertNotNull(cnxCacheNoBackend); } @@ -171,13 +168,13 @@ private void checkWasNull(final Object object, final int i, final ResultSet resu Assert.assertFalse(resultSet.wasNull()); } - private ResultSet checkCache(ResultSet resultSet) { - Assert.assertEquals("com.qwazr.jdbc.cache.CachedResultSet", resultSet.getClass().getName()); + protected ResultSet checkCache(ResultSet resultSet) { + Assert.assertEquals(expectedResultSetClass().getName(), resultSet.getClass().getName()); return resultSet; } - private ResultSet checkNoCache(ResultSet resultSet) { - Assert.assertNotEquals("com.qwazr.jdbc.cache.CachedResultSet", resultSet.getClass().getName()); + protected ResultSet checkNoCache(ResultSet resultSet) { + Assert.assertNotEquals(expectedResultSetClass().getName(), resultSet.getClass().getName()); return resultSet; } diff --git a/src/test/java/com/qwazr/jdbc/cache/InMemoryCacheDerbyTest.java b/src/test/java/com/qwazr/jdbc/cache/InMemoryCacheDerbyTest.java new file mode 100644 index 0000000..8452858 --- /dev/null +++ b/src/test/java/com/qwazr/jdbc/cache/InMemoryCacheDerbyTest.java @@ -0,0 +1,40 @@ +/** + * Copyright 2016 Emmanuel Keller / QWAZR + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qwazr.jdbc.cache; + +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +import java.io.File; +import java.sql.ResultSet; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class InMemoryCacheDerbyTest extends DerbyTest { + @Override + Class expectedResultSetClass() { + return CachedInMemoryResultSet.class; + } + + @Override + String getOrSetJdbcCacheUrl() { + return "jdbc:cache:mem:foo"; + } + + @Override + String getDerbyDbName() { + return "myDB2"; + } +} diff --git a/src/test/java/com/qwazr/jdbc/cache/OnDiskCacheDerbyTest.java b/src/test/java/com/qwazr/jdbc/cache/OnDiskCacheDerbyTest.java new file mode 100644 index 0000000..c74b91f --- /dev/null +++ b/src/test/java/com/qwazr/jdbc/cache/OnDiskCacheDerbyTest.java @@ -0,0 +1,58 @@ +/** + * Copyright 2016 Emmanuel Keller / QWAZR + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qwazr.jdbc.cache; + +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.ResultSet; + +import static org.junit.Assert.fail; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class OnDiskCacheDerbyTest extends DerbyTest { + private static String jdbcCacheUrl = null; + + @Override + Class expectedResultSetClass() { + return CachedOnDiskResultSet.class; + } + + @Override + String getOrSetJdbcCacheUrl() { + if (jdbcCacheUrl == null) { + String tempDirPath; + try { + tempDirPath = Files.createTempDirectory("jdbc-cache-test").toUri().getPath(); + if (tempDirPath.contains(":") && tempDirPath.startsWith("/")) + tempDirPath = tempDirPath.substring(1); + jdbcCacheUrl = "jdbc:cache:file:" + tempDirPath + File.separatorChar + "cache"; + } catch (IOException e) { + fail("Can not create the cache dir: " + e.getMessage()); + return null; + } + } + return jdbcCacheUrl; + } + + @Override + String getDerbyDbName() { + return "myDB1"; + } +}