package com.opendoorlogistics.codefromweb.jxmapviewer2; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CacheRequest; import java.net.CacheResponse; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ResponseCache; import java.net.URI; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; import org.apache.commons.io.IOUtils; /** * Fork of jxmapviewer2's LocalResponseCache which removes tiles which are * older than a set number of days so we can get the local cache updated * regularly without the overhead of checking if a tile is updated (which * can be slow on a slow internet connection). * * We also validate the tiles; checking they are a non-corrupt image. * This response cache can only therefore be used for tiles (but it will * only be used for downloading data from the base URL, so if its called * for another website downloading data that isn't a tile, * the tile validation code won't be called). * * Original author joshy */ public class ExpiringOSMLocalResponseCache extends ResponseCache { public static final long EXPIRY_HOUR_COUNT = 24*7; private final File cacheDir; private boolean checkForUpdates; private HashSet<String> acceptedBaseURLs = new HashSet<String>(); /** * Private constructor to prevent instantiation. * * @param baseURL * the URI that should be cached or <code>null</code> (for all URLs) * @param cacheDir * the cache directory * @param checkForUpdates * true if the URL is queried for newer versions of a file first */ public ExpiringOSMLocalResponseCache( File cacheDir, boolean checkForUpdates) { this.cacheDir = cacheDir; this.checkForUpdates = checkForUpdates; if (!cacheDir.exists()) { cacheDir.mkdirs(); } } public void addAcceptedBasedURL(String s){ acceptedBaseURLs.add(s.trim().toLowerCase()); } protected boolean cacheURI(URI remoteUri){ if (acceptedBaseURLs.size()>0) { String remote = remoteUri.toString(); remote = remote.trim().toLowerCase(); for(String accepted : acceptedBaseURLs){ if(remote.startsWith(accepted)){ return true; } } return false; } return true; } /** * Returns the local File corresponding to the given remote URI. * * @param remoteUri * the remote URI * @return the corresponding local file */ private File getLocalFile(URI remoteUri) { if(!cacheURI(remoteUri)){ return null; } StringBuilder sb = new StringBuilder(); String host = remoteUri.getHost(); String query = remoteUri.getQuery(); String path = remoteUri.getPath(); String fragment = remoteUri.getFragment(); if (host != null) { sb.append(host); } if (path != null) { sb.append(path); } if (query != null) { sb.append('?'); sb.append(query); } if (fragment != null) { sb.append('#'); sb.append(fragment); } String name; final int maxLen = 250; if (sb.length() < maxLen) { name = sb.toString(); } else { name = sb.substring(0, maxLen); } name = name.replace('?', '$'); name = name.replace('*', '$'); name = name.replace(':', '$'); name = name.replace('<', '$'); name = name.replace('>', '$'); name = name.replace('"', '$'); File f = new File(cacheDir, name); return f; } /** * @param remoteUri * the remote URI * @param localFile * the corresponding local file * @return true if the resource at the given remote URI is newer than the resource cached locally. */ private static boolean isUpdateAvailable(URI remoteUri, File localFile) { URLConnection conn; try { conn = remoteUri.toURL().openConnection(); } catch (MalformedURLException ex) { return false; } catch (IOException ex) { return false; } if (!(conn instanceof HttpURLConnection)) { // don't bother with non-http connections return false; } long localLastMod = localFile.lastModified(); long remoteLastMod = 0L; HttpURLConnection httpconn = (HttpURLConnection) conn; // disable caching so we don't get in feedback loop with ResponseCache httpconn.setUseCaches(false); try { httpconn.connect(); remoteLastMod = httpconn.getLastModified(); } catch (IOException ex) { // log.error("An exception occurred", ex);(); return false; } finally { httpconn.disconnect(); } return (remoteLastMod > localLastMod); } @Override public CacheResponse get(URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException { File localFile = getLocalFile(uri); if (localFile == null) { // we don't want to cache this URL return null; } if (!localFile.exists()) { // the file isn't already in our cache, return null return null; } boolean valid = true; BasicFileAttributes attributes = Files.readAttributes(localFile.toPath(), BasicFileAttributes.class); if(attributes !=null){ FileTime time = attributes.lastModifiedTime(); if(time==null){ time = attributes.creationTime(); } if(time!=null){ // This comparison may or may not be a bit off depending on // timezones, but the max error should be ~1 day and we // set the expiry longer than this anyway... // Also we should ensure the calculation is done in long, not int... long age = new Date().getTime()- time.toMillis(); long limit = EXPIRY_HOUR_COUNT * 60L * 60L * 1000L; valid = age < limit; } } if(valid){ try { byte [] bytes = IOUtils.toByteArray(localFile.toURI()); if(ImageIO.read(new ByteArrayInputStream(bytes))==null){ valid = false; } } catch (Throwable e) { valid = false; } } if(!valid){ try { localFile.delete(); } catch (Throwable e) { // TODO: handle exception } return null; } if (checkForUpdates) { if (isUpdateAvailable(uri, localFile)) { // there is an update available, so don't return cached version return null; } } return new LocalCacheResponse(localFile, rqstHeaders); } @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { // only cache http(s) GET requests if (!(conn instanceof HttpURLConnection) || !(((HttpURLConnection) conn).getRequestMethod().equals("GET"))) { return null; } File localFile = getLocalFile(uri); if (localFile == null) { // we don't want to cache this URL return null; } new File(localFile.getParent()).mkdirs(); return new LocalCacheRequest(localFile); } private class LocalCacheResponse extends CacheResponse { private FileInputStream fis; private final Map<String, List<String>> headers; private LocalCacheResponse(File localFile, Map<String, List<String>> rqstHeaders) { try { this.fis = new FileInputStream(localFile); } catch (FileNotFoundException ex) { } this.headers = rqstHeaders; } @Override public Map<String, List<String>> getHeaders() throws IOException { return headers; } @Override public InputStream getBody() throws IOException { return fis; } } private class LocalCacheRequest extends CacheRequest { private final File localFile; private FileOutputStream fos; private LocalCacheRequest(File localFile) { this.localFile = localFile; try { this.fos = new FileOutputStream(localFile); } catch (FileNotFoundException ex) { } } @Override public OutputStream getBody() throws IOException { return fos; } @Override public void abort() { // abandon the cache attempt by closing the stream and deleting // the local file try { fos.close(); localFile.delete(); } catch (IOException e) { } } } }