at.spardat.xma.boot.cache.FileCacheStore Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH .
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* s IT Solutions AT Spardat GmbH - initial API and implementation
*******************************************************************************/
/*
* @(#) $Id: FileCacheStore.java 2084 2007-11-27 14:53:31Z s3460 $
*/
package at.spardat.xma.boot.cache;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import at.spardat.xma.boot.BootRuntime;
import at.spardat.xma.boot.Statics;
import at.spardat.xma.boot.logger.LogLevel;
import at.spardat.xma.boot.logger.Logger;
import at.spardat.xma.boot.transport.Result;
import at.spardat.xma.boot.util.PropertyFile;
import at.spardat.xma.boot.util.Util;
/**
* file storage of the file cache
* @author s2877
* @since 1.3.0
*/
public class FileCacheStore {
/** logger */
private Logger log_;
/**
* debug file output
*/
private Boolean debug;
/** file lock timeout (milliseconds) */
private static int lockTimeout = 30000;
private static int sleepOnLock = 150;
/**
* The root directory for this file cache.
* it is defined as //
*/
private File baseDir_;
/** used to format if-modified-since correctly */
private DateFormat formater;
/**
* @throws IOException containing the filename
*/
FileCacheStore(BootRuntime brt) throws IOException {
log_ = Logger.getLogger( "boot.cache" ); //$NON-NLS-1$
setBaseDir( brt.getDataDirectory());
debug = brt.getDebug();
formater = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US ); //$NON-NLS-1$
formater.setTimeZone(new SimpleTimeZone(0, "GMT")); //$NON-NLS-1$
}
/** @return the directory for this cache. */
File getBaseDir() {
return baseDir_;
}
/**
* sets the root directory
*
* @param fbaseDir the base directory for the boot runtime.
* @throws IOException containing the filename
*/
private void setBaseDir(File fbaseDir) throws IOException {
if( !fbaseDir.exists())
throw new IllegalArgumentException( "base directory' "+fbaseDir.getAbsolutePath()+" 'for file-cache does not exist"); //$NON-NLS-1$
File fCacheDir = new File( fbaseDir, Statics.CACHE_DIRNAME);
if( !fCacheDir.exists()) {
boolean success = fCacheDir.mkdirs();
if(!success) throw new IOException("error creating "+fCacheDir.getAbsolutePath());
}
this.baseDir_ = fCacheDir;
}
/**
* Retrieve a resource from the cache.
* @param resourceFile the File to retrieve
* @param urlRemote the ULR of the resource on the server
* @return the found resource or null if it is not allready cached.
*/
public FCResource getResource(File resourceFile,URL urlRemote) {
try {
return new FCResource(urlRemote, resourceFile, false, debug.booleanValue());
} catch (FileNotFoundException ex) {
log_.log(LogLevel.FINE, "file not cached: \"{0}\"", resourceFile.toString()); //$NON-NLS-1$
} catch (IOException ioe) { // if we can't read it, we don't have it
log_.log(LogLevel.WARNING, "error reading "+resourceFile.getAbsolutePath(),ioe);
}
return null;
}
/**
* Put a resource into the cache.
* @param file to write the content into
* @param result content of the resource as received from the server
* @param urlRemote of the content on the server
* @param temp old expired version of the resource if exists in the cache
* @param bmode if true the content of the resource file is read into memory imeditately
* @param bforce indicates if an update of the resource was forced
* @throws IOException containing the filename
*/
public FCResource storeResource(File file,Result result,URL urlRemote,IFileCacheResource temp,boolean bmode, boolean bforce) throws IOException {
FCResource fcr;
File flock = lock(file);
try {
if(result.isModified() && temp!=null && ( temp.isExpired() || bforce == true) ) {
if( file.delete() == false ) {
log_.log(LogLevel.WARNING, "file could not be deleted: \"{0}\"", file.toString() ); //$NON-NLS-1$
}
}
/* the resource was updated and a modified resource was loaded */
if( result!=null && result.isModified()) {
saveContent(file,result.getBuffer());
createPropertyFile(file,result,urlRemote);
}
fcr = new FCResource(urlRemote,file, bmode,debug.booleanValue());
/* the resource was checked for an updated, but was not modified */
if(!result.isModified()) {
fcr.setLastUpdated( System.currentTimeMillis() );
fcr.setExpiration(result.getExpirationDate());
fcr.store();
}
} finally {
unlock(flock);
}
return fcr;
}
/**
* Put a resource into the cache.
* @param file to write the content into
* @param result the resource as received from the server
* @param content to store into the file
* @param urlRemote of the content on the server
* @param bmode if true the content of the resource file is read into memory imeditately
* @throws IOException containing the filename
*/
public FCResource storeResource(File file, Result result, byte[] content, URL urlRemote, boolean bmode) throws IOException {
FCResource fcr;
File flock = lock(file);
try {
saveContent(file,content);
createPropertyFile(file,result,urlRemote);
fcr = new FCResource(urlRemote,file, bmode,debug.booleanValue());
} finally {
unlock(flock);
}
return fcr;
}
/**
* lock a resource by creating a lock file for the resource fres
.
*
* this is done by creating a new file with createNewFile. If an existing
* lockfile is already found, it checks its validity. that means, the file must not be
* older than lockTimeout . Older lockfiles are removed.
*
* As other processes will not notify this process about lockfile changes, this process
* will poll for changes. It waits sleepOnLock between each retry.
*
* After a lockfile is remove by one process, another process may create a new lockfile
* in between the delete and our retry for creation. Therefore it only tries and waits for
* unlocking 3 times.
*
*
* @param fres the resource to lock. this is the base resource, without lock extension
* @return File the lock-file
* @throws IOException containing the filename
*/
File lock( File fres ) throws IOException {
int tryLocking = 0;
File lock = new File( fres.getPath() + Statics.LOCKFILE_EXT ); // lockfile
if( !lock.exists()) lock.getParentFile().mkdirs();
try {
while( tryLocking < 3 && lock.createNewFile() == false ) {
// already existing lock-file. wait for unlock
long fTime = 0L;
long cTime;
tryLocking++;
do {
// check if the lockfile is already too old
long ltime = lock.lastModified();
if (ltime == 0L ) break; // 0 if it does not exist anymore
if( fTime == 0L ) fTime = ltime; // save the current file-time
if( ltime != fTime ) break; // if this new time differs, a new file has been created.
cTime = System.currentTimeMillis();
if( (cTime-fTime) >= this.getLockTimeout() ) { // too old; invalid; cleanup
lock.delete(); // this may also fail, if meanwhile another process has deleted this file.
break;
}
try { Thread.sleep( getSleepOnLock() );
} catch (InterruptedException e) { }
} while( ((cTime-fTime) < this.getLockTimeout()) && lock.exists() );
} // while createNewFile
} catch( IOException e ) {
IOException ne = new IOException("error creating lock file "+lock.getAbsolutePath());
ne.initCause(e);
throw ne;
}
return lock;
}
/**
* unlock a resource
*
* @param file the lockfile
* @return void
*/
void unlock( File file ) {
if( !file.exists()) {
// throw new IllegalArgumentException("lockfile does not exist");
}
log_.log(LogLevel.ALL, "locked for: {0} ms; file: {1}", new Object[] { new Long( System.currentTimeMillis()-file.lastModified()), file.getName() }); //$NON-NLS-1$
if( file.delete() == false )
log_.log(LogLevel.INFO, "lockfile delete failed: \"{0}\"", file.getName() ); //$NON-NLS-1$
}// unlock
/** @return long lock timeout */
private long getLockTimeout() {
return lockTimeout;
}
/**
* returns the sleep time in between file-lock polling
*
* @return int sleep time
*/
private static int getSleepOnLock() {
return sleepOnLock;
}
/**
* saves transport result to disk.
*
* @param file file to save information to
* @param result transport result
* @throws IOException containing the filename
*/
void saveToDisk(File file, Result result, URL url) throws IOException {
saveContent(file,result.getBuffer());
createPropertyFile(file,result,url);
}
/**
* Stores the given data into the given file. If the file
* does not exist, it is created together with needed parent directories.
* If it exists it is overwritten.
* @param file to write
* @param data to write into the file
* @throws IOException containing the filename if an I/O Error occures
*/
void saveContent(File file, byte[] data) throws IOException {
OutputStream os = null;
try {
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
os = new FileOutputStream(file);
os.write(data);
Util.close(os,file.getAbsolutePath());
} catch (IOException exc) {
Util.close(os,file.getAbsolutePath());
if (file.exists()) file.delete();
IOException ne = new IOException("saving of '"+file.getAbsolutePath()+"' failed.");
ne.initCause(exc);
throw ne;
}
}
/**
* Creates the .ifo file corresponding to the given file and writes the properties of the
* FCResource into it. These are the http-headers contained in 'result', the url and for
* .jar files the Hashvalue.
* @param file data file
* @param result data received from the server
* @param url of the corresponding resource on the server
* @throws IOException containing the filename
*/
void createPropertyFile(File file,Result result, URL url) throws IOException {
PropertyFile pf = null;
try {
// make shure, no old file lies around.
File ifoFile = new File( file.getPath() + Statics.FILEINFO_EXT);
if(ifoFile.exists()) ifoFile.delete();
pf = new PropertyFile( new File( file.getPath() + Statics.FILEINFO_EXT) ,debug.booleanValue());
pf.setPropertyNoflush( Statics.HTTP_EXPIRES, Long.toString(result.getExpirationDate() ));
if(debug.booleanValue()) {
pf.setPropertyNoflush(Statics.HTTP_EXPIRES + ".STR", formater.format(new Date(result.getExpirationDate())) );
}
pf.setPropertyNoflush( Statics.HTTP_LAST_MODIFIED, Long.toString(result.getLastModified() ));
if(debug.booleanValue()) {
pf.setPropertyNoflush(Statics.HTTP_LAST_MODIFIED + ".STR", formater.format(new Date(result.getLastModified())) );
}
pf.setPropertyNoflush(Statics.strContentLength, Long.toString(result.getContentLength() ));
long lLastUpdated = System.currentTimeMillis();
pf.setPropertyNoflush(Statics.strLastUpdated, Long.toString(lLastUpdated));
if(debug.booleanValue()) {
pf.setPropertyNoflush(Statics.strLastUpdated + ".STR", formater.format(new Date(lLastUpdated)) );
}
pf.setPropertyNoflush(Statics.URL_NAME, url.toExternalForm() );
if(result.getEtag() != null ) pf.setPropertyNoflush( Statics.strEtag, result.getEtag());
log_.log( LogLevel.INFO, "cached new resource: \"{0}\"", url.toExternalForm()); //$NON-NLS-1$
if( url.getQuery() == null && file.toString().endsWith( ".jar") ) {
JarFile jarfile = new JarFile(file);
try {
Manifest manifest = jarfile.getManifest();
Attributes attributes = manifest.getMainAttributes();
if(attributes!=null) {
String strApplicationDigest = attributes.getValue(Statics.XMA_DIGEST);
if( strApplicationDigest!=null) {
pf.setPropertyNoflush( Statics.XMA_DIGEST, strApplicationDigest);
}else {
log_.log(LogLevel.WARNING, "Missing XMA-Digest for jarfile: " + url.toExternalForm() );
}
} else {
log_.log(LogLevel.WARNING, "Attributes missing for jarfile:" + url.toExternalForm() );
}
} finally {
try {
jarfile.close();
} catch (IOException arg) {
log_.log(LogLevel.WARNING,"error closing "+file.getAbsolutePath(),arg);
}
}
}
if( url.getQuery()==null && ( file.toString().endsWith( ".jar") || file.toString().endsWith( ".xml")) ) {
pf.setPropertyNoflush(Statics.HTTP_MAX_AGE, "0" );
}
if(result.getTransformations()!=null) {
pf.setPropertyNoflush(Statics.TRANSFORM_HEADER,result.getTransformations());
}
pf.store();
/* cleanup all file resources on any error */
} catch( Exception exc ) {
if( file.exists()) file.delete();
if( pf!=null) {
File f = pf.getStoreFile();
if( f!=null && f.exists()) {
f.delete();
}
}
IOException ne = new IOException("saving of '"+file.getAbsolutePath()+"' failed.");
ne.initCause(exc);
throw ne;
}
}
/**
* deletes both files of the given FCResouce.
* @param res to delete
* @throws IOException containing the filename if the file locking failed
*/
void removeResource(FCResource res) throws IOException {
remove(res.getResourceFile(),res.getProperties());
}
/**
* deltes the given file and its corresponding .ifo file.
* @param file to delete
* @throws IOException containing the filename if the file locking failed
* @since 1.3.1
*/
void removeResource(File file) throws IOException {
remove(file,new File( file.getPath() + Statics.FILEINFO_EXT));
}
/**
* Removes a file together with its .ifo file.
* @param fRes resource file to delete
* @param fProp corresponding .ifo file to delete
* @throws IOException containing the filename
*/
private void remove(File fRes,File fProp) throws IOException {
File lock = lock(fRes);
try {
if( fRes.delete() == false) {
log_.log(LogLevel.WARNING, "delete of resource failed: {0}", fRes.getAbsolutePath() ); //$NON-NLS-1$
}
if( fProp.delete() == false ) {
log_.log(LogLevel.WARNING, "delete of property failed: {0}", fProp.getAbsolutePath() ); //$NON-NLS-1$
}
} finally {
unlock(lock);
}
}
/**
* Find the latest previous version of the given file.
* This is the file with the same name containing the greatest version number
* less than the the version of the given file. If the name of the file does not
* contain a valid version number, nothing will be found.
* If there are more than one file with the same version number but different hash
* values, they will be ignored.
* @param file for which to search the previous version
* @return the found resource or null.
*/
public FCResource findPreviousVersion(final File file) {
File dir = file.getParentFile();
VersionNumber currentVersion = VersionNumber.parse(file.getName());
if(currentVersion==null) return null;
FilenameFilter filter = new FilenameFilter() {
final Pattern pattern = VersionNumber.searchPattern(file.getName());
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
};
String[] oldNames = dir.list(filter);
// find newest version older than current version
// if there are more than on file for a version this particual version must be ignored!
Set duplicates = new HashSet();
VersionNumber oldVersion = null;
int index = -1 ;
if(oldNames!=null) {
boolean found=false;
label: do {
oldVersion = null;
index = -1;
for(int i=oldNames.length-1;i>=0;i--) {
VersionNumber version = VersionNumber.parse(oldNames[i]);
if(currentVersion.compareTo(version)<=0) continue; // newer or equal current version
if(duplicates.contains(version)) continue; // known duplicate -> ignore
if(version.equals(oldVersion)) { // new duplicate found
duplicates.add(version); // -> add to known dupplicates
continue label; // and restart search
}
if((oldVersion==null || oldVersion.compareTo(version)<0)) {
oldVersion=version;
index=i;
}
}
found=true;
} while(!found);
}
if(oldVersion!=null) { // previous version found load and return it
try {
FCResource res = new FCResource(null,new File(dir,oldNames[index]),false,debug.booleanValue());
res.versionNumber=oldVersion;
return res;
} catch (IOException e) {
log_.log(LogLevel.WARNING,"error reading "+dir.getAbsolutePath()+File.separator+oldNames[index]+": ",e);
return null;
}
} else {
return null;
}
}
}