/******************************************************************************* * Copyright 2012 Geoscience Australia * * 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 au.gov.ga.earthsci.core.retrieve.cache; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.URL; import java.net.URLConnection; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.security.DigestInputStream; import java.security.MessageDigest; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import au.gov.ga.earthsci.common.util.HashReadWriteLocker; import au.gov.ga.earthsci.worldwind.common.util.Util; /** * {@link IURLCache} implementation that uses a directory in a file system for * caching data. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class FileURLCache implements IURLCache { private Logger logger = LoggerFactory.getLogger(FileURLCache.class); private final File directory; private final HashReadWriteLocker locker = new HashReadWriteLocker(); private final static String PARTIAL_SUFFIX = ".partial"; //$NON-NLS-1$ private final static String CONTENT_TYPE_SUFFIX = ".contenttype"; //$NON-NLS-1$ private final static String URLS_PROPERTIES_FILENAME = "urls.properties"; //$NON-NLS-1$ public FileURLCache(File directory) { if (directory == null) { throw new NullPointerException("Directory cannot be null"); //$NON-NLS-1$ } this.directory = directory; } @Override public boolean isPartial(URL url) { return isFileLocked(getPartialFile(url)); } @Override public long getPartialLength(URL url) { return lengthLocked(getPartialFile(url)); } @Override public long getPartialLastModified(URL url) { return lastModifiedLocked(getPartialFile(url)); } @Override public OutputStream writePartial(URL url, long offset) throws IOException { final File partialFile = getPartialFile(url); locker.lockWrite(partialFile); RandomAccessFile raf = null; try { partialFile.getParentFile().mkdirs(); raf = new RandomAccessFile(partialFile, "rw"); //$NON-NLS-1$ partialFile.setReadable(true, false); partialFile.setWritable(true, false); FileChannel channel = raf.getChannel(); offset = Math.max(0l, offset); channel.truncate(offset); channel.position(offset); return new FilterOutputStream(Channels.newOutputStream(channel)) { private boolean unlocked = false; @Override public void close() throws IOException { try { super.close(); } finally { if (!unlocked) { unlocked = true; locker.unlockWrite(partialFile); } } } }; } catch (IOException e) { if (raf != null) { raf.close(); } locker.unlockWrite(partialFile); throw e; } } @Override public boolean writeComplete(URL url, long lastModified, String contentType) { File partialFile = getPartialFile(url); File completeFile = getCompleteFile(url); locker.lockWrite(partialFile); try { locker.lockRead(completeFile); try { if (fileEquals(partialFile, completeFile)) { partialFile.delete(); return false; } } finally { locker.unlockRead(completeFile); } locker.lockWrite(completeFile); try { partialFile.renameTo(completeFile); if (lastModified > 0) { completeFile.setLastModified(lastModified); } setContentType(url, contentType, completeFile); } finally { locker.unlockWrite(completeFile); } } finally { locker.unlockWrite(partialFile); } return true; } public static boolean fileEquals(File file1, File file2) { byte[] md51 = fileMD5(file1); byte[] md52 = fileMD5(file2); if (md51 == null || md52 == null) { return false; } return byteArrayEquals(md51, md52); } public static boolean byteArrayEquals(byte[] b1, byte[] b2) { if (b1 == b2) { return true; } if (b1.length != b2.length) { return false; } for (int i = 0; i < b1.length; i++) { if (b1[i] != b2[i]) { return false; } } return true; } public static byte[] fileMD5(File file) { try { MessageDigest md = MessageDigest.getInstance("MD5"); //$NON-NLS-1$ InputStream is = null; try { is = new DigestInputStream(new BufferedInputStream(new FileInputStream(file)), md); byte[] buffer = new byte[8192]; while (is.read(buffer) >= 0) { } } finally { if (is != null) { is.close(); } } return md.digest(); } catch (Exception e) { return null; } } @Override public boolean isComplete(URL url) { return isFileLocked(getCompleteFile(url)); } @Override public long getLength(URL url) { return lengthLocked(getCompleteFile(url)); } @Override public long getLastModified(URL url) { return lastModifiedLocked(getCompleteFile(url)); } @Override public String getContentType(URL url) { File contentTypeFile = getContentTypeFile(url); try { locker.lockRead(contentTypeFile); if (contentTypeFile.isFile()) { if (contentTypeFile.length() > 0) { try { return readTextFile(contentTypeFile); } catch (IOException e) { logger.warn("Error reading content type for url: " + url, e); //$NON-NLS-1$ } } return null; } else { File completeFile = getCompleteFile(url); return URLConnection.guessContentTypeFromName(completeFile.getName()); } } finally { locker.unlockRead(contentTypeFile); } } private void setContentType(URL url, String contentType, File completeFile) { if (contentType == null) { return; } String guessedContentType = URLConnection.guessContentTypeFromName(completeFile.getName()); if ((guessedContentType == null && contentType == null) || contentType.equals(guessedContentType)) { //don't need to write a content type file if the URLConnection can guess the content type from the complete filename return; } File contentTypeFile = getContentTypeFile(url); try { writeTextFile(contentTypeFile, contentType); } catch (IOException e) { logger.warn("Error writing content type for url: " + url, e); //$NON-NLS-1$ } } protected boolean isFileLocked(File file) { try { locker.lockRead(file); return file.isFile(); } finally { locker.unlockRead(file); } } protected long lengthLocked(File file) { try { locker.lockRead(file); return file.length(); } finally { locker.unlockRead(file); } } protected long lastModifiedLocked(File file) { try { locker.lockRead(file); return file.lastModified(); } finally { locker.unlockRead(file); } } protected String readTextFile(File file) throws IOException { try { locker.lockRead(file); StringBuilder sb = new StringBuilder(); InputStream is = null; try { is = new BufferedInputStream(new FileInputStream(file)); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) >= 0) { String s = new String(buffer, 0, len, "UTF-8"); //$NON-NLS-1$ sb.append(s); } } finally { if (is != null) { is.close(); } } return sb.toString(); } finally { locker.unlockRead(file); } } protected void writeTextFile(File file, String text) throws IOException { try { locker.lockWrite(file); OutputStream os = null; try { os = new BufferedOutputStream(new FileOutputStream(file)); if (text != null) { byte[] buffer = text.getBytes("UTF-8"); //$NON-NLS-1$ os.write(buffer); } } finally { if (os != null) { os.close(); } } } finally { locker.unlockWrite(file); } } @Override public InputStream read(URL url) throws IOException { final File completeFile = getCompleteFile(url); locker.lockRead(completeFile); try { return new FilterInputStream(new FileInputStream(completeFile)) { private boolean unlocked = false; @Override public void close() throws IOException { try { super.close(); } finally { if (!unlocked) { unlocked = true; locker.unlockRead(completeFile); } } } }; } catch (IOException e) { locker.unlockRead(completeFile); throw e; } } @Override public File getFile(URL url) { return getCompleteFile(url); } private File getCompleteFile(URL url) { return fileForURL(url, ""); //$NON-NLS-1$ } private File getPartialFile(URL url) { return fileForURL(url, PARTIAL_SUFFIX); } private File getContentTypeFile(URL url) { return fileForURL(url, CONTENT_TYPE_SUFFIX); } private File fileForURL(URL url, String suffix) { String hashDirectory = !Util.isBlank(url.getHost()) ? url.getHost() + File.separator : ""; //$NON-NLS-1$ hashDirectory += getHashDirectory(url); String extension = au.gov.ga.earthsci.common.util.Util.getExtension(url.getPath()); if (extension == null || extension.length() > 30) { //probably not an extension extension = ""; //$NON-NLS-1$ } File propertiesFile = new File(directory, hashDirectory + File.separator + URLS_PROPERTIES_FILENAME); locker.lockWrite(propertiesFile); try { Properties properties = new Properties(); if (propertiesFile.exists()) { try { loadProperties(properties, propertiesFile); } catch (IOException e) { logger.error("Error reading url properties file", e); //$NON-NLS-1$ } } String filename = properties.getProperty(url.toString()); if (filename == null) { filename = properties.size() + extension; } properties.setProperty(url.toString(), filename); propertiesFile.getParentFile().mkdirs(); try { saveProperties(properties, propertiesFile); } catch (IOException e) { logger.error("Error writing url properties file", e); //$NON-NLS-1$ } return new File(directory, hashDirectory + File.separator + filename + suffix); } finally { locker.unlockWrite(propertiesFile); } } private static String getHashDirectory(URL url) { String hashCode = String.valueOf(url.toString().hashCode()); StringBuilder directory = new StringBuilder(); if (hashCode.charAt(0) == '-') { directory.append('-'); hashCode = hashCode.substring(1); } while (hashCode.length() < 10) { hashCode = "0" + hashCode; //$NON-NLS-1$ } directory.append(hashCode.substring(0, 3)); directory.append(File.separator); directory.append(hashCode.substring(3, 6)); directory.append(File.separator); directory.append(hashCode.substring(6)); return directory.toString(); } private static void loadProperties(Properties properties, File file) throws IOException { FileInputStream fis = null; try { fis = new FileInputStream(file); properties.load(fis); } finally { if (fis != null) { fis.close(); } } } private static void saveProperties(Properties properties, File file) throws IOException { FileOutputStream fos = null; try { fos = new FileOutputStream(file); properties.store(fos, null); } finally { if (fos != null) { fos.close(); } } } }