Skip to content

Commit

Permalink
[jmaps-core] add embedded derby based secondary cache
Browse files Browse the repository at this point in the history
  • Loading branch information
mikey75 committed Oct 2, 2024
1 parent 24b59b6 commit a2dcc0f
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 32 deletions.
3 changes: 3 additions & 0 deletions ReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
1.2
- Added Derby based database secondary cache

1.1
- Example app migrated to xmlbeans instead of jaxb
- Formalize map files format (use xsd schema), add validation of mapfiles against the schema
Expand Down
6 changes: 6 additions & 0 deletions jmaps-viewer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
<version>${okhttp.version}</version>
</dependency>

<!-- Derby based db cache -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.14.2.0</version>
</dependency>

<!--LRU cache-->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ public class Defaults {
// default cache dir for storing WMTS service descriptors (capabilities.xml etc)
public static final Path DEFAULT_WMTS_DESCRIPTOR_CACHE = Paths.get(DEFAULT_CACHE_DIR.toString(), "wmts-cache");

// default cache dir $HOME/.jmaps-cache/tile-cache
// default cache dir for directory based cache -> $HOME/.jmaps-cache/tile-cache
public static final Path DEFAULT_TILECACHE_DIR = Paths.get(DEFAULT_CACHE_DIR.toString(), "tile-cache");
// default cache dir for derby DB based cache -> $HOME/.jmaps-cache/tile-cache-db
public static final Path DEFAULT_TILE_CACHE_DB = Paths.get(DEFAULT_CACHE_DIR.toString(), "tile-cache-db");

// default user-agent
public static final String DEFAULT_USER_AGENT = "JMaps Tiler v.1.0";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected BaseCache(Path baseDir, Duration cacheTimeout) {
this.cacheTimeout = cacheTimeout;
log.info("Secondary Tile Cache: {}, location: {}", getClass().getSimpleName(), getBaseDir());

if (!cacheTimeout.isZero()) {
if (isCacheTimeoutEnabled()) {
log.info("Cache expiration checking enabled! Tiles will be re-downloaded every: {}", cacheTimeout);
}
}
Expand All @@ -33,7 +33,11 @@ public void setCacheTimeout(Duration duration) {
log.info("Disabling cache expiration checking");
} else {
log.info("Setting new cache timeout duration to: {}", duration);
cacheTimeout = duration;
}
cacheTimeout = duration;
}

protected boolean isCacheTimeoutEnabled() {
return !cacheTimeout.isZero();
}
}
150 changes: 150 additions & 0 deletions jmaps-viewer/src/main/java/net/wirelabs/jmaps/map/cache/DBCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package net.wirelabs.jmaps.map.cache;

import lombok.extern.slf4j.Slf4j;
import net.wirelabs.jmaps.map.Defaults;
import net.wirelabs.jmaps.map.utils.ImageUtils;

import javax.sql.rowset.serial.SerialBlob;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.sql.*;
import java.time.Duration;

@Slf4j
public class DBCache extends BaseCache implements Cache<String, BufferedImage> {

private static final String CONNECTION_TEMPLATE = "jdbc:derby:%s;create=true";

private static final String CREATE_TABLE_SQL = "CREATE TABLE TILECACHE (tileUrl VARCHAR(1024) PRIMARY KEY,tileImg BLOB(512 K),timeStamp BIGINT)";
private static final String GET_TEMPLATE_SQL = "select TILEIMG from TILECACHE where TILEURL='%s'";
private static final String PUT_TEMPLATE_SQL = "INSERT INTO TILECACHE VALUES('%s', ?, ?)";
private static final String UPDATE_TEMPLATE_SQL = "UPDATE TILECACHE set TILEIMG=?,TIMESTAMP=? WHERE TILEURL='%s'";
private static final String GET_TIMESTAMP_TEMPLATE_SQL = "select TIMESTAMP from TILECACHE where TILEURL='%s'";
private static final String COUNT_TEMPLATE_SQL = "select count (*) from TILECACHE where TILEURL='%s'";


public DBCache() {
super(Defaults.DEFAULT_TILE_CACHE_DB, Defaults.DEFAULT_CACHE_TIMEOUT);
createDatabaseIfNotExists();
}

public DBCache(Path dbBaseDir, Duration cacheTimeout) {
super(dbBaseDir, cacheTimeout);
createDatabaseIfNotExists();
}

@Override
public BufferedImage get(String key) {
return getImage(key);
}

@Override
public void put(String key, BufferedImage value) {
putImage(key, value);
}

@Override
public boolean keyExpired(String key) {

if (isCacheTimeoutEnabled()) {
long timestamp = getTimestampFromDB(key);
// if gettimestamp returns 0, the key does not exist, so no expiration set, return immediately
if (timestamp == 0) return false;
long expirationTimeStamp = System.currentTimeMillis() - getCacheTimeout().toMillis();
return (timestamp < expirationTimeStamp);
}
return false;
}

Connection getConnection() throws SQLException {
return DriverManager.getConnection(String.format(CONNECTION_TEMPLATE, getBaseDir()));
}

private void createDatabaseIfNotExists() {
try {
try (Connection dbConnection = getConnection(); Statement smt = dbConnection.createStatement()) {
// create cache table
if (!cacheTableExists(dbConnection)) {
smt.execute(CREATE_TABLE_SQL);
}
log.info("Connected and initialized: {}", dbConnection.getMetaData().getURL());
}
} catch (SQLException e) {
log.info("Could not create connection!");
}
}

private boolean cacheTableExists(Connection dbConnection) throws SQLException {
if (dbConnection != null) {
DatabaseMetaData dbmd = dbConnection.getMetaData();
try (ResultSet rs = dbmd.getTables(null, null, "TILECACHE", null)) {
return rs.next();
}
}
return false;
}

private void putImage(String key, BufferedImage value) {
try {
byte[] imgbytes = ImageUtils.imageToBytes(value);
Blob blob = new SerialBlob(imgbytes);
String sqlCmd;

// always write entry - TileProvider decides if it's a reload or new tile
if (!entryExists(key)) {
sqlCmd = String.format(PUT_TEMPLATE_SQL, key);
} else {
sqlCmd = String.format(UPDATE_TEMPLATE_SQL, key);
}

try (Connection connection = getConnection(); PreparedStatement ps = connection.prepareStatement(sqlCmd)) {
ps.setBlob(1, blob);
ps.setLong(2, System.currentTimeMillis());
ps.execute();
}
} catch (IOException | SQLException e) {
log.warn("Cache put failed! {}", e.getMessage());
}
}

private BufferedImage getImage(String key) {
String query = String.format(GET_TEMPLATE_SQL, key);
try {
try (Connection connection = getConnection(); Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(query)) {
if (rs.next()) { // only one result is always expected from the query so no need to loop
Blob b = rs.getBlob(1);
byte[] is = b.getBytes(1, 512 * 1024);
return ImageUtils.imageFromBytes(is);
}
}
return null;
} catch (IOException | SQLException e) {
return null;
}
}

private long getTimestampFromDB(String key) {

try {
String query = String.format(GET_TIMESTAMP_TEMPLATE_SQL, key);
try (Connection connection = getConnection(); Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(query)) {
if (rs.next()) { // only one result is always expected from the query so no need to loop
return rs.getLong(1);
}
}

} catch (SQLException e) {
log.warn("Getting timestampe from db entry failed! {}", e.getMessage());
}
return 0;
}

boolean entryExists(String key) throws SQLException {

String query = String.format(COUNT_TEMPLATE_SQL, key);
try (Connection connection = getConnection(); Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(query)) {
return (rs.next() && rs.getInt(1) == 1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,18 @@ private void putImage(String key, BufferedImage b) {

public boolean keyExpired(String key) {

long now = System.currentTimeMillis();
long expirationTime = now - getCacheTimeout().toMillis();
long lastWrittenOn;

// if expiration time = current time -> no expiration set (cache timeout = 0) - so key never expires
if (now == expirationTime) return false;

try {
File file = getLocalFile(key);
lastWrittenOn = Files.getLastModifiedTime(file.toPath()).toMillis();
} catch (IOException e) {
return false;
if (isCacheTimeoutEnabled()) {
long expirationTimeStamp = System.currentTimeMillis() - getCacheTimeout().toMillis();

try {
File file = getLocalFile(key);
long fileTimeStamp = Files.getLastModifiedTime(file.toPath()).toMillis();
return (fileTimeStamp < expirationTimeStamp);
} catch (IOException e) {
return false;
}
}
return (lastWrittenOn < expirationTime);
return false; // if cache timeout is not enabled - keys never expire, are always valid

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.wirelabs.jmaps.map.utils;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import javax.imageio.ImageIO;
import java.awt.image.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ImageUtils {

public static byte[] imageToBytes(BufferedImage image) throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", byteStream);
return byteStream.toByteArray();
}

public static BufferedImage imageFromBytes(byte[] imageData) throws IOException {
ByteArrayInputStream byteStream = new ByteArrayInputStream(imageData);
return ImageIO.read(byteStream);

}

public static boolean imagesEqual(BufferedImage image1, BufferedImage image2) {
if (image1.getWidth() != image2.getWidth() || image1.getHeight() != image2.getHeight()) {
return false;
}
for (int x = 1; x < image2.getWidth(); x++) {
for (int y = 1; y < image2.getHeight(); y++) {
if (image1.getRGB(x, y) != image2.getRGB(x, y)) {
return false;
}
}
}
return true;
}
}
14 changes: 0 additions & 14 deletions jmaps-viewer/src/test/java/net/wirelabs/jmaps/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.awt.image.*;
import java.util.Random;

/**
Expand Down Expand Up @@ -46,17 +45,4 @@ public static String getRandomString(int len) {
return resultString.toString();
}

public static boolean compareImages(BufferedImage img1, BufferedImage img2) {
if (img1.getWidth() == img2.getWidth() && img1.getHeight() == img2.getHeight()) {
for (int x = 0; x < img1.getWidth(); x++) {
for (int y = 0; y < img1.getHeight(); y++) {
if (img1.getRGB(x, y) != img2.getRGB(x, y))
return false;
}
}
} else {
return false;
}
return true;
}
}
Loading

0 comments on commit a2dcc0f

Please sign in to comment.