package org.openstreetmap.gui.jmapviewer; //License: GPL. Copyright 2008 by Jan Peter Stotz import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.util.logging.Level; import java.util.logging.Logger; import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; import org.openstreetmap.gui.jmapviewer.interfaces.TileSource.TileUpdate; /** * A {@link TileLoader} implementation that loads tiles from OSM via HTTP and * saves all loaded files in a directory located in the the temporary directory. * If a tile is present in this file cache it will not be loaded from OSM again. * * @author Jan Peter Stotz * @author Stefan Zeller */ public class OsmFileCacheTileLoader extends OsmTileLoader { private static final Logger log = Logger.getLogger(OsmFileCacheTileLoader.class.getName()); private static final String ETAG_FILE_EXT = ".etag"; private static final Charset ETAG_CHARSET = Charset.forName("UTF-8"); public static final long FILE_AGE_ONE_DAY = 1000 * 60 * 60 * 24; public static final long FILE_AGE_ONE_WEEK = FILE_AGE_ONE_DAY * 7; protected String cacheDirBase; protected long maxCacheFileAge = FILE_AGE_ONE_WEEK; protected long recheckAfter = FILE_AGE_ONE_DAY; /** * Create a OSMFileCacheTileLoader with given cache directory. * If cacheDir is <code>null</code> the system property temp dir * is used. If not set an IOException will be thrown. * @param map * @param cacheDir */ public OsmFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws SecurityException { super(map); String tempDir = null; String userName = System.getProperty("user.name"); try { tempDir = System.getProperty("java.io.tmpdir"); } catch (SecurityException e) { log.log(Level.WARNING, "Failed to access system property ''java.io.tmpdir'' for security reasons. Exception was: " + e.toString()); throw e; // rethrow } try { if (cacheDir == null) { if (tempDir == null) throw new IOException("No temp directory set"); String subDirName = "JMapViewerTiles"; // On Linux/Unix systems we do not have a per user tmp directory. // Therefore we add the user name for getting a unique dir name. if (userName != null && userName.length() > 0) subDirName += "_" + userName; cacheDir = new File(tempDir, subDirName); } log.finest("Tile cache directory: " + cacheDir); if (!cacheDir.exists() && !cacheDir.mkdirs()) throw new IOException(); cacheDirBase = cacheDir.getAbsolutePath(); } catch (Exception e) { cacheDirBase = "tiles"; } } /** * Create a OSMFileCacheTileLoader with system property temp dir. * If not set an IOException will be thrown. * @param map */ public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException { this(map, null); } @Override public Runnable createTileLoaderJob(final TileSource source, final int tilex, final int tiley, final int zoom) { return new FileLoadJob(source, tilex, tiley, zoom); } protected class FileLoadJob implements Runnable { InputStream input = null; int tilex, tiley, zoom; Tile tile; TileSource source; File tileCacheDir; File tileFile = null; long fileAge = 0; boolean fileTilePainted = false; public FileLoadJob(TileSource source, int tilex, int tiley, int zoom) { super(); this.source = source; this.tilex = tilex; this.tiley = tiley; this.zoom = zoom; } public void run() { TileCache cache = listener.getTileCache(); synchronized (cache) { tile = cache.getTile(source, tilex, tiley, zoom); if (tile == null || tile.isLoaded() || tile.loading) return; tile.loading = true; } tileCacheDir = new File(cacheDirBase, source.getName()); if (!tileCacheDir.exists()) { tileCacheDir.mkdirs(); } if (loadTileFromFile()) return; if (fileTilePainted) { Runnable job = new Runnable() { public void run() { loadOrUpdateTile(); } }; JobDispatcher.getInstance().addJob(job); } else { loadOrUpdateTile(); } } protected void loadOrUpdateTile() { try { // log.finest("Loading tile from OSM: " + tile); HttpURLConnection urlConn = loadTileFromOsm(tile); if (tileFile != null) { switch (source.getTileUpdate()) { case IfModifiedSince: urlConn.setIfModifiedSince(fileAge); break; case LastModified: if (!isOsmTileNewer(fileAge)) { log.finest("LastModified test: local version is up to date: " + tile); tile.setLoaded(true); tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter); return; } break; } } if (source.getTileUpdate() == TileUpdate.ETag || source.getTileUpdate() == TileUpdate.IfNoneMatch) { if (tileFile != null) { String fileETag = loadETagfromFile(); if (fileETag != null) { switch (source.getTileUpdate()) { case IfNoneMatch: urlConn.addRequestProperty("If-None-Match", fileETag); break; case ETag: if (hasOsmTileETag(fileETag)) { tile.setLoaded(true); tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter); return; } } } } String eTag = urlConn.getHeaderField("ETag"); saveETagToFile(eTag); } if (urlConn.getResponseCode() == 304) { // If we are isModifiedSince or If-None-Match has been set // and the server answers with a HTTP 304 = "Not Modified" log.finest("ETag test: local version is up to date: " + tile); tile.setLoaded(true); tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter); return; } byte[] buffer = loadTileInBuffer(urlConn); if (buffer != null) { tile.loadImage(new ByteArrayInputStream(buffer)); tile.setLoaded(true); listener.tileLoadingFinished(tile, true); saveTileToFile(buffer); } else { tile.setLoaded(true); } } catch (Exception e) { tile.setImage(Tile.ERROR_IMAGE); listener.tileLoadingFinished(tile, false); if (input == null) { System.err.println("failed loading " + zoom + "/" + tilex + "/" + tiley + " " + e.getMessage()); } } finally { tile.loading = false; tile.setLoaded(true); } } protected boolean loadTileFromFile() { FileInputStream fin = null; try { tileFile = getTileFile(); fin = new FileInputStream(tileFile); if (fin.available() == 0) throw new IOException("File empty"); tile.loadImage(fin); fin.close(); fileAge = tileFile.lastModified(); boolean oldTile = System.currentTimeMillis() - fileAge > maxCacheFileAge; // System.out.println("Loaded from file: " + tile); if (!oldTile) { tile.setLoaded(true); listener.tileLoadingFinished(tile, true); fileTilePainted = true; return true; } listener.tileLoadingFinished(tile, true); fileTilePainted = true; } catch (Exception e) { try { if (fin != null) { fin.close(); tileFile.delete(); } } catch (Exception e1) { } tileFile = null; fileAge = 0; } return false; } protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException { input = urlConn.getInputStream(); ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available()); byte[] buffer = new byte[2048]; boolean finished = false; do { int read = input.read(buffer); if (read >= 0) { bout.write(buffer, 0, read); } else { finished = true; } } while (!finished); if (bout.size() == 0) return null; return bout.toByteArray(); } /** * Performs a <code>HEAD</code> request for retrieving the * <code>LastModified</code> header value. * * Note: This does only work with servers providing the * <code>LastModified</code> header: * <ul> * <li>{@link OsmTileLoader#MAP_OSMA} - supported</li> * <li>{@link OsmTileLoader#MAP_MAPNIK} - not supported</li> * </ul> * * @param fileAge * @return <code>true</code> if the tile on the server is newer than the * file * @throws IOException */ protected boolean isOsmTileNewer(long fileAge) throws IOException { URL url; url = new URL(tile.getUrl()); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); prepareHttpUrlConnection(urlConn); urlConn.setRequestMethod("HEAD"); urlConn.setReadTimeout(30000); // 30 seconds read timeout // System.out.println("Tile age: " + new // Date(urlConn.getLastModified()) + " / " // + new Date(fileAge)); long lastModified = urlConn.getLastModified(); if (lastModified == 0) return true; // no LastModified time returned return (lastModified > fileAge); } protected boolean hasOsmTileETag(String eTag) throws IOException { URL url; url = new URL(tile.getUrl()); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); prepareHttpUrlConnection(urlConn); urlConn.setRequestMethod("HEAD"); urlConn.setReadTimeout(30000); // 30 seconds read timeout // System.out.println("Tile age: " + new // Date(urlConn.getLastModified()) + " / " // + new Date(fileAge)); String osmETag = urlConn.getHeaderField("ETag"); if (osmETag == null) return true; return (osmETag.equals(eTag)); } protected File getTileFile() { return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "." + source.getTileType()); } protected void saveTileToFile(byte[] rawData) { try { FileOutputStream f = new FileOutputStream(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "." + source.getTileType()); f.write(rawData); f.close(); // System.out.println("Saved tile to file: " + tile); } catch (Exception e) { System.err.println("Failed to save tile content: " + e.getLocalizedMessage()); } } protected void saveETagToFile(String eTag) { try { FileOutputStream f = new FileOutputStream(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + ETAG_FILE_EXT); f.write(eTag.getBytes(ETAG_CHARSET.name())); f.close(); } catch (Exception e) { System.err.println("Failed to save ETag: " + e.getLocalizedMessage()); } } protected String loadETagfromFile() { try { FileInputStream f = new FileInputStream(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + ETAG_FILE_EXT); byte[] buf = new byte[f.available()]; f.read(buf); f.close(); return new String(buf, ETAG_CHARSET.name()); } catch (Exception e) { return null; } } } public long getMaxFileAge() { return maxCacheFileAge; } /** * Sets the maximum age of the local cached tile in the file system. If a * local tile is older than the specified file age * {@link OsmFileCacheTileLoader} will connect to the tile server and check * if a newer tile is available using the mechanism specified for the * selected tile source/server. * * @param maxFileAge * maximum age in milliseconds * @see #FILE_AGE_ONE_DAY * @see #FILE_AGE_ONE_WEEK * @see TileSource#getTileUpdate() */ public void setCacheMaxFileAge(long maxFileAge) { this.maxCacheFileAge = maxFileAge; } public String getCacheDirBase() { return cacheDirBase; } public void setTileCacheDir(String tileCacheDir) { File dir = new File(tileCacheDir); dir.mkdirs(); this.cacheDirBase = dir.getAbsolutePath(); } }