/******************************************************************************* * Copyright (c) MOBAC developers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.program.tilestore.berkeleydb; import java.awt.Point; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import mobac.exceptions.TileStoreException; import mobac.program.interfaces.MapSource; import mobac.program.model.Settings; import mobac.program.tilestore.TileStore; import mobac.program.tilestore.TileStoreEntry; import mobac.program.tilestore.TileStoreInfo; import mobac.program.tilestore.berkeleydb.TileDbEntry.TileDbKey; import mobac.utilities.GUIExceptionHandler; import mobac.utilities.Utilities; import mobac.utilities.file.DeleteFileFilter; import mobac.utilities.file.DirInfoFileFilter; import mobac.utilities.file.DirectoryFileFilter; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.Environment; import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.EnvironmentLockedException; import com.sleepycat.persist.EntityCursor; import com.sleepycat.persist.EntityStore; import com.sleepycat.persist.PrimaryIndex; import com.sleepycat.persist.StoreConfig; import com.sleepycat.persist.evolve.Mutations; import com.sleepycat.persist.evolve.Renamer; /** * The new database based tile store implementation. */ public class BerkeleyDbTileStore extends TileStore { /** * Max count of tile stores opened */ private static final int MAX_CONCURRENT_ENVIRONMENTS = 5; private EnvironmentConfig envConfig; private Map<String, TileDatabase> tileDbMap; private FileLock tileStoreLock = null; private Mutations mutations; public BerkeleyDbTileStore() throws TileStoreException { super(); acquireTileStoreLock(); tileDbMap = new TreeMap<String, TileDatabase>(); envConfig = new EnvironmentConfig(); envConfig.setTransactional(false); envConfig.setLocking(true); envConfig.setExceptionListener(GUIExceptionHandler.getInstance()); envConfig.setAllowCreate(true); envConfig.setSharedCache(true); envConfig.setCachePercent(50); mutations = new Mutations(); String oldPackage1 = "tac.tilestore.berkeleydb"; String oldPackage2 = "tac.program.tilestore.berkeleydb"; String entry = ".TileDbEntry"; String key = ".TileDbEntry$TileDbKey"; mutations.addRenamer(new Renamer(oldPackage1 + entry, 0, TileDbEntry.class.getName())); mutations.addRenamer(new Renamer(oldPackage1 + key, 0, TileDbKey.class.getName())); mutations.addRenamer(new Renamer(oldPackage1 + entry, 1, TileDbEntry.class.getName())); mutations.addRenamer(new Renamer(oldPackage1 + key, 1, TileDbKey.class.getName())); mutations.addRenamer(new Renamer(oldPackage2 + entry, 2, TileDbEntry.class.getName())); mutations.addRenamer(new Renamer(oldPackage2 + key, 2, TileDbKey.class.getName())); // for (Renamer r : mutations.getRenamers()) // log.debug(r.toString()); Runtime.getRuntime().addShutdownHook(new ShutdownThread(true)); } protected void acquireTileStoreLock() throws TileStoreException { try { // Get a file channel for the file File file = new File(tileStoreDir, "lock"); if (!tileStoreDir.isDirectory()) try { Utilities.mkDirs(tileStoreDir); } catch (IOException e) { throw new TileStoreException("Unable to create tile store directory: \"" + tileStoreDir.getPath() + "\""); } FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); // Use the file channel to create a lock on the file. // This method blocks until it can retrieve the lock. // Try acquiring the lock without blocking. This method returns // null or throws an exception if the file is already locked. tileStoreLock = channel.tryLock(); if (tileStoreLock == null) throw new TileStoreException("Unable to obtain tile store lock - " + "another instance of Mobile Atlas Creator is running!"); // // Release the lock // lock.release(); // // // Close the file // channel.close(); } catch (Exception e) { log.error("", e); throw new TileStoreException(e.getMessage(), e.getCause()); } } @Override public TileStoreEntry createNewEntry(int x, int y, int zoom, byte[] data, long timeLastModified, long timeExpires, String eTag) { return new TileDbEntry(x, y, zoom, data, timeLastModified, timeExpires, eTag); } @Override public TileStoreEntry createNewEmptyEntry(int x, int y, int zoom) { long time = System.currentTimeMillis(); long timeExpires = time + Settings.getInstance().tileDefaultExpirationTime; // We set the tile data to an empty array because we can not store null return new TileDbEntry(x, y, zoom, new byte[] {}, time, timeExpires, ""); } private TileDatabase getTileDatabase(MapSource mapSource) throws DatabaseException { TileDatabase db; if (tileDbMap == null) // Tile store has been closed already return null; String storeName = mapSource.getName(); if (storeName == null) return null; synchronized (tileDbMap) { db = tileDbMap.get(storeName); } if (db != null) return db; try { synchronized (tileDbMap) { cleanupDatabases(); db = tileDbMap.get(storeName); if (db == null) { db = new TileDatabase(storeName); db.lastAccess = System.currentTimeMillis(); tileDbMap.put(mapSource.getName(), db); } return db; } } catch (Exception e) { log.error("Error creating tile store db \"" + mapSource.getName() + "\"", e); throw new TileStoreException(e); } } private TileDatabase getTileDatabase(String storeName) throws DatabaseException { TileDatabase db; if (tileDbMap == null) // Tile store has been closed already return null; if (storeName == null) return null; synchronized (tileDbMap) { db = tileDbMap.get(storeName); } if (db != null) return db; try { synchronized (tileDbMap) { cleanupDatabases(); db = tileDbMap.get(storeName); if (db == null) { db = new TileDatabase(storeName); db.lastAccess = System.currentTimeMillis(); tileDbMap.put(storeName, db); } return db; } } catch (Exception e) { log.error("Error creating tile store db \"" + storeName + "\"", e); throw new TileStoreException(e); } } @Override public TileStoreInfo getStoreInfo(String storeName) throws InterruptedException { int tileCount = getNrOfTiles(storeName); long storeSize = getStoreSize(storeName); return new TileStoreInfo(storeSize, tileCount); } @Override public void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource) throws IOException { this.putTileData(tileData, x, y, zoom, mapSource, -1, -1, null); } @Override public void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource, long timeLastModified, long timeExpires, String eTag) throws IOException { TileDbEntry tile = new TileDbEntry(x, y, zoom, tileData, timeLastModified, timeExpires, eTag); TileDatabase db = null; try { if (log.isTraceEnabled()) log.trace("Saved " + mapSource.getName() + " " + tile); db = getTileDatabase(mapSource); if (db != null) db.put(tile); } catch (Exception e) { if (db != null) db.close(); log.error("Faild to write tile to tile store \"" + mapSource.getName() + "\"", e); } } @Override public void putTile(TileStoreEntry tile, MapSource mapSource) { TileDatabase db = null; try { if (log.isTraceEnabled()) log.trace("Saved " + mapSource.getName() + " " + tile); db = getTileDatabase(mapSource); db.put((TileDbEntry) tile); } catch (Exception e) { if (db != null) db.close(); log.error("Faild to write tile to tile store \"" + mapSource.getName() + "\"", e); } } @Override public TileStoreEntry getTile(int x, int y, int zoom, MapSource mapSource) { TileDatabase db = null; try { db = getTileDatabase(mapSource); if (db == null) return null; TileStoreEntry tile = db.get(new TileDbKey(x, y, zoom)); if (log.isTraceEnabled()) { if (tile == null) log.trace("Tile store cache miss: (x,y,z)" + x + "/" + y + "/" + zoom + " " + mapSource.getName()); else log.trace("Loaded " + mapSource.getName() + " " + tile); } return tile; } catch (Exception e) { if (db != null) db.close(); log.error("failed to retrieve tile from tile store \"" + mapSource.getName() + "\"", e); return null; } } public boolean contains(int x, int y, int zoom, MapSource mapSource) { try { return getTileDatabase(mapSource).contains(new TileDbKey(x, y, zoom)); } catch (DatabaseException e) { log.error("", e); return false; } } public void prepareTileStore(MapSource mapSource) { try { getTileDatabase(mapSource); } catch (DatabaseException e) { } } public void clearStore(String storeName) { File databaseDir = getStoreDir(storeName); TileDatabase db; synchronized (tileDbMap) { db = tileDbMap.get(storeName); if (db != null) db.close(false); if (databaseDir.exists()) { DeleteFileFilter dff = new DeleteFileFilter(); databaseDir.listFiles(dff); databaseDir.delete(); log.debug("Tilestore " + storeName + " cleared: " + dff); } tileDbMap.remove(storeName); } } /** * This method returns the amount of tiles in the store of tiles which is specified by the {@link MapSource} object. * * @param mapSourceName * the store to calculate number of tiles in * @return the amount of tiles in the specified store. * @throws InterruptedException */ public int getNrOfTiles(String mapSourceName) throws InterruptedException { try { File storeDir = getStoreDir(mapSourceName); if (!storeDir.isDirectory()) return 0; TileDatabase db = getTileDatabase(mapSourceName); int tileCount = (int) db.entryCount(); db.close(); return tileCount; } catch (DatabaseException e) { log.error("", e); return -1; } } public long getStoreSize(String storeName) throws InterruptedException { File tileStore = getStoreDir(storeName); if (tileStore.exists()) { DirInfoFileFilter diff = new DirInfoFileFilter(); try { tileStore.listFiles(diff); } catch (RuntimeException e) { throw new InterruptedException(); } return diff.getDirSize(); } else { return 0; } } public BufferedImage getCacheCoverage(MapSource mapSource, int zoom, Point tileNumMin, Point tileNumMax) throws InterruptedException { TileDatabase db; try { db = getTileDatabase(mapSource); return db.getCacheCoverage(zoom, tileNumMin, tileNumMax); } catch (DatabaseException e) { log.error("", e); return null; } } protected void cleanupDatabases() { if (tileDbMap.size() < MAX_CONCURRENT_ENVIRONMENTS) return; synchronized (tileDbMap) { List<TileDatabase> list = new ArrayList<TileDatabase>(tileDbMap.values()); Collections.sort(list, new Comparator<TileDatabase>() { public int compare(TileDatabase o1, TileDatabase o2) { if (o1.lastAccess == o2.lastAccess) return 0; return (o1.lastAccess < o2.lastAccess) ? -1 : 1; } }); for (int i = 0; i < list.size() - 2; i++) list.get(i).close(); } } public void closeAll() { Thread t = new ShutdownThread(false); t.start(); try { t.join(); } catch (InterruptedException e) { log.error("", e); } } /** * Returns <code>true</code> if the tile store directory of the specified {@link MapSource} exists. * * @param mapSource * @return */ public boolean storeExists(MapSource mapSource) { File tileStore = getStoreDir(mapSource); return (tileStore.isDirectory()) && (tileStore.exists()); } /** * Returns the directory used for storing the tile database of the {@link MapSource} specified by * <code>mapSource</code> * * @param mapSource * @return */ protected File getStoreDir(MapSource mapSource) { return getStoreDir(mapSource.getName()); } /** * @param mapSourceName * @return directory used for storing the tile database belonging to <code>mapSource</code> */ protected File getStoreDir(String mapSourceName) { return new File(tileStoreDir, "db-" + mapSourceName); } public String[] getAllStoreNames() { File[] dirs = tileStoreDir.listFiles(new DirectoryFileFilter()); ArrayList<String> storeNames = new ArrayList<String>(dirs.length); for (File d : dirs) { String name = d.getName(); if (name.startsWith("db-")) { name = name.substring(3); storeNames.add(name); } } String[] result = new String[storeNames.size()]; storeNames.toArray(result); return result; } private class ShutdownThread extends DelayedInterruptThread { private final boolean shutdown; public ShutdownThread(boolean shutdown) { super("DBShutdown"); this.shutdown = shutdown; } @Override public void run() { log.debug("Closing all tile databases..."); synchronized (tileDbMap) { for (TileDatabase db : tileDbMap.values()) { db.close(false); } tileDbMap.clear(); if (shutdown) { tileDbMap = null; try { tileStoreLock.release(); } catch (IOException e) { log.error("", e); } } } log.debug("All tile databases has been closed"); } } protected class TileDatabase { final String mapSourceName; final Environment env; final EntityStore store; final PrimaryIndex<TileDbKey, TileDbEntry> tileIndex; boolean dbClosed = false; long lastAccess; public TileDatabase(String mapSourceName) throws IOException, EnvironmentLockedException, DatabaseException { this(mapSourceName, getStoreDir(mapSourceName)); } public TileDatabase(String mapSourceName, File databaseDirectory) throws IOException, EnvironmentLockedException, DatabaseException { log.debug("Opening tile store db: \"" + databaseDirectory + "\""); File storeDir = databaseDirectory; DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); try { t.pauseInterrupt(); this.mapSourceName = mapSourceName; lastAccess = System.currentTimeMillis(); Utilities.mkDirs(storeDir); env = new Environment(storeDir, envConfig); StoreConfig storeConfig = new StoreConfig(); storeConfig.setAllowCreate(true); storeConfig.setTransactional(false); storeConfig.setMutations(mutations); store = new EntityStore(env, "TilesEntityStore", storeConfig); tileIndex = store.getPrimaryIndex(TileDbKey.class, TileDbEntry.class); } finally { if (t.interruptedWhilePaused()) close(); t.resumeInterrupt(); } } public boolean isClosed() { return dbClosed; } public long entryCount() throws DatabaseException { return tileIndex.count(); } public void put(TileDbEntry tile) throws DatabaseException { DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); try { t.pauseInterrupt(); tileIndex.put(tile); } finally { if (t.interruptedWhilePaused()) close(); t.resumeInterrupt(); } } public boolean contains(TileDbKey key) throws DatabaseException { return tileIndex.contains(key); } public TileDbEntry get(TileDbKey key) throws DatabaseException { return tileIndex.get(key); } public PrimaryIndex<TileDbKey, TileDbEntry> getTileIndex() { return tileIndex; } public BufferedImage getCacheCoverage(int zoom, Point tileNumMin, Point tileNumMax) throws DatabaseException, InterruptedException { log.debug("Loading cache coverage for region " + tileNumMin + " " + tileNumMax + " of zoom level " + zoom); DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); int width = tileNumMax.x - tileNumMin.x + 1; int height = tileNumMax.y - tileNumMin.y + 1; byte ff = (byte) 0xFF; byte[] colors = new byte[] { 120, 120, 120, 120, // alpha-gray 10, ff, 0, 120 // alpha-green }; IndexColorModel colorModel = new IndexColorModel(2, 2, colors, 0, true); BufferedImage image = null; try { image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel); } catch (Throwable e) { log.error("Failed to create coverage image: " + e.toString()); image = null; System.gc(); return null; } WritableRaster raster = image.getRaster(); // We are loading the coverage of the selected area column by column which is much faster than loading the // whole region at once for (int x = tileNumMin.x; x <= tileNumMax.x; x++) { TileDbKey fromKey = new TileDbKey(x, tileNumMin.y, zoom); TileDbKey toKey = new TileDbKey(x, tileNumMax.y, zoom); EntityCursor<TileDbKey> cursor = tileIndex.keys(fromKey, true, toKey, true); try { TileDbKey key = cursor.next(); while (key != null) { int pixelx = key.x - tileNumMin.x; int pixely = key.y - tileNumMin.y; raster.setSample(pixelx, pixely, 0, 1); key = cursor.next(); if (t.isInterrupted()) { log.debug("Cache coverage loading aborted"); throw new InterruptedException(); } } } finally { cursor.close(); } } return image; } protected void purge() { try { store.sync(); env.cleanLog(); } catch (DatabaseException e) { log.error("database compression failed: ", e); } } public void close() { close(true); } public void close(boolean removeFromMap) { if (dbClosed) return; if (removeFromMap) { synchronized (tileDbMap) { TileDatabase db2 = tileDbMap.get(mapSourceName); if (db2 == this) tileDbMap.remove(mapSourceName); } } DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); try { t.pauseInterrupt(); try { log.debug("Closing tile store db \"" + mapSourceName + "\""); if (store != null) store.close(); } catch (Exception e) { log.error("", e); } try { env.close(); } catch (Exception e) { log.error("", e); } finally { dbClosed = true; } } finally { if (t.interruptedWhilePaused()) close(); t.resumeInterrupt(); } } @Override protected void finalize() throws Throwable { close(); super.finalize(); } } }