/*
* ==========================================================================*\
* | $Id: PersistentStorageManager.java,v 1.18 2011/06/21 14:57:27 mwoodsvt Exp $
* |*-------------------------------------------------------------------------*|
* | Copyright (C) 2007-2010 Virginia Tech | | This file is part of the
* Student-Library. | | The Student-Library is free software; you can
* redistribute it and/or | modify it under the terms of the GNU Lesser General
* Public License as | published by the Free Software Foundation; either version
* 3 of the | License, or (at your option) any later version. | | The
* Student-Library 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 Lesser General Public
* License for more details. | | You should have received a copy of the GNU
* Lesser General Public License | along with the Student-Library; if not, see
* <http://www.gnu.org/licenses/>.
* \*==========================================================================
*/
package student.web.internal;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.CompositeClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import student.web.WebUtilities;
//import student.web.internal.converters.AliasConverter;
import student.web.internal.converters.ArrayConverter;
//import student.web.internal.converters.CachedClassConverter;
import student.web.internal.converters.AliasConverter;
import student.web.internal.converters.CachedClassConverter;
import student.web.internal.converters.CollectionConverter;
import student.web.internal.converters.FlexibleFieldSetConverter;
import student.web.internal.converters.MapConverter;
import student.web.internal.converters.PersistentMapConverter;
import student.web.internal.converters.UnrecognizedClassConverter;
// -------------------------------------------------------------------------
/**
* Manages a persistent collection of data stored in files, where the data is in
* the form of field/value maps.
*
* @author Stephen Edwards
* @author Last changed by $Author: mwoodsvt $
* @version $Revision: 1.18 $, $Date: 2011/06/21 14:57:27 $
*/
public class PersistentStorageManager
{
// ~ Instance/static variables .............................................
private static PersistentStorageManager PSM = new PersistentStorageManager();
private static final String EXT = ".dataxml";
private File baseDir = LocalityService.getSupportStrategy().getPersistentBase();
private MRUMap<String, String> idCache = new MRUMap<String, String>( 10000,
0 );
private MRUMap<String, String> idReverseCache = new MRUMap<String, String>( 10000,
0 );
private Map<ClassLoader, XStreamBundle> xstream = new WeakHashMap<ClassLoader, XStreamBundle>( 256 );
private Set<String> usedIds = null;
private long usedIdsTimestamp = 0L;
//This is used to allow other code to lock the persistence store without calling into it.
private Lock pLock = new ReentrantLock();
// ~ Constructor ...........................................................
// ----------------------------------------------------------
/**
* This is a singleton class.
*/
private PersistentStorageManager()
{
// Nothing to do
}
// ----------------------------------------------------------
/**
* Get the singleton instance of this class.
*
* @return The singleton instance of this class
*/
public static PersistentStorageManager getInstance()
{
return PSM;
}
public static PersistentStorageManager getInstance( String dirName )
{
PersistentStorageManager manager = new PersistentStorageManager();
manager.baseDir = LocalityService.getSupportStrategy().getPersistentFile( PSM.baseDir.getPath()+"/"+ dirName );
if(!manager.baseDir.exists())
{
manager.baseDir.mkdirs();
}
return manager;
}
// ----------------------------------------------------------
/**
* Get the singleton instance of this class.
*
* @return The singleton instance of this class
*/
public static void setStorageLocation( File dir )
{
synchronized ( PSM )
{
PSM.baseDir = LocalityService.getSupportStrategy().getPersistentFile( dir.getPath() );
}
}
// ~ Public Methods ........................................................
// ----------------------------------------------------------
public synchronized Set<String> getAllIds()
{
// if (usedIds == null)
// {
usedIds = new HashSet<String>( 256 );
if(!baseDir.exists())
baseDir.mkdir();
if (baseDir.exists())
{
for ( File file : baseDir.listFiles() )
{
String name = file.getName();
if ( name.endsWith( EXT ) )
{
// Strip the extension
name = name.substring( 0, name.length() - EXT.length() );
usedIds.add( unsanitizeId( name ) );
}
}
}
usedIdsTimestamp = System.currentTimeMillis();
// }
return new HashSet<String>( usedIds );
}
// ----------------------------------------------------------
public synchronized Set<String> getAllIdsContaining(
String fragment,
String after )
{
if ( fragment == null || fragment.length() == 0 )
{
return getAllIds();
}
else
{
fragment = fragment.toLowerCase();
Set<String> result = new HashSet<String>( getAllIds().size() );
for ( String id : getAllIds() )
{
if ( id.toLowerCase().contains( fragment ) )
{
result.add( id );
}
}
return result;
}
}
// ----------------------------------------------------------
public synchronized Set<String> getAllIdsNotContaining( String fragment )
{
if ( fragment == null || fragment.length() == 0 )
{
return getAllIds();
}
else
{
fragment = fragment.toLowerCase();
Set<String> result = new HashSet<String>( getAllIds().size() );
for ( String id : getAllIds() )
{
if ( !id.toLowerCase().contains( fragment ) )
{
result.add( id );
}
}
return result;
}
}
// ----------------------------------------------------------
public synchronized boolean idSetHasChangedSince( long time )
{
return usedIdsTimestamp == 0L || usedIdsTimestamp > time;
}
// ----------------------------------------------------------
public synchronized StoredObject getPersistentObject(
String id,
Map<String, StoredObject> cache,
ClassLoader loader )
{
StoredObject result = null;
pLock.lock();
try
{
try
{
result = getPersistentObjectHelper( id, cache, loader);
}
finally
{
Snapshot.clearLocal();
}
}
finally
{
pLock.unlock();
}
return result;
}
private StoredObject getPersistentObjectHelper(
String id,
Map<String, StoredObject> cache,
ClassLoader loader)
{
StoredObject result = null;
if ( baseDir.exists() )
{
String sanitizedId = sanitizeId( id );
File src= LocalityService.getSupportStrategy().getPersistentFile( baseDir,sanitizedId + EXT);
final InputStream in = LocalityService.getSupportStrategy().getObjectSource(src);
if ( in != null )
{
try
{
Object object = readObjectFromXML(id,cache, in, loader, new Snapshot() );
result = new StoredObject( id,
sanitizedId,
object,
Snapshot.getLocal(),
src.lastModified() );
}
finally
{
try
{
in.close();
}
catch(IOException e)
{
//Oh well
}
}
}
}
return result;
}
public Object readObjectFromXML(
String key,
Map<String, StoredObject> cache,
final InputStream in,
final ClassLoader loader, Snapshot local)
{
Snapshot.setLocal( local );
final XStreamBundle bundle = getXStreamFor( loader, key, cache );
Object object = AccessController.doPrivileged( new PrivilegedAction<Object>()
{
public Object run()
{
return bundle.xstream.fromXML( in );
}
} );
return object;
}
// ----------------------------------------------------------
public synchronized boolean persistentObjectHasChanged(
String id,
long timestamp,
ClassLoader loader )
{
id = sanitizeId( id );
File dest = LocalityService.getSupportStrategy().getPersistentFile( baseDir, id+EXT );
return timestamp < dest.lastModified();
}
// ----------------------------------------------------------
public synchronized StoredObject storePersistentObject(
String id,
Map<String, StoredObject> cache,
Object object )
{
pLock.lock();
StoredObject stored = null;
try
{
ClassLoader loader = object.getClass().getClassLoader();
stored = new StoredObject( id,
sanitizeId( id ),
object,
Snapshot.getLocal(),
0L );
storePersistentObjectChanges( id, cache, stored, loader );
}
finally
{
pLock.unlock();
}
return stored;
}
public synchronized void storePersistentObjectChanges(
String id,
Map<String, StoredObject> cache,
final StoredObject object,
ClassLoader loader )
{
pLock.lock();
try
{
storePersistentObjectChanges( id, cache, object, loader, null );
}
finally
{
pLock.unlock();
}
}
public class FakePrintWriter extends Writer
{
@Override
public void write( char[] cbuf, int off, int len ) throws IOException
{
// heh do nothing
}
@Override
public void flush() throws IOException
{
// heh do nothing
}
@Override
public void close() throws IOException
{
// heh do nothing
}
}
public synchronized void refreshPersistentObject(
String id,
Map<String, StoredObject> cache,
final StoredObject object,
ClassLoader loader )
{
storePersistentObjectChanges( id, cache, object, loader, new FakePrintWriter() );
}
// ----------------------------------------------------------
public synchronized void storePersistentObjectChanges(
String id,
Map<String, StoredObject> cache,
final StoredObject object,
ClassLoader loader,
final Writer replacementWriter )
{
pLock.lock();
StoredObject obj = null;
try
{
String sanitizedId = sanitizeId( id );
Snapshot.setLocal( new Snapshot() );
obj = getPersistentObjectHelper( id, cache, loader );
// Leave the snapshots set in the converter
File dest = LocalityService.getSupportStrategy()
.getPersistentFile( baseDir, sanitizedId + EXT );
final PrintWriter out;
if ( replacementWriter == null )
{
out = new PrintWriter( LocalityService.getSupportStrategy()
.getObjectOutput( dest ) );
}
else
{
out = new PrintWriter( replacementWriter );
}
Snapshot local;
if ( object.fieldset() == null )
{
local = new Snapshot();
}
else
{
local = object.fieldset();
}
writeObjectToXML( id,
cache,
object.value(),
out,
loader,
Snapshot.getLocal(),
local );
if ( usedIds != null && !usedIds.contains( id ) )
{
usedIdsTimestamp = System.currentTimeMillis();
usedIds.add( id );
}
object.timestamp = System.currentTimeMillis();
Snapshot.clearNewest();
Snapshot.clearLocal();
}
finally
{
pLock.unlock();
}
}
public void writeObjectToXML(
String key,
Map<String, StoredObject> cache,
final Object object,
final Writer out,
final ClassLoader loader,
Snapshot newest,
Snapshot local)
{
Snapshot.setNewest( newest );
Snapshot.setLocal( local );
final XStreamBundle bundle = getXStreamFor( loader, key, cache );
AccessController.doPrivileged( new PrivilegedAction<Object>()
{
public Object run()
{
bundle.xstream.toXML( object, out );
return null;
}
} );
try
{
out.close();
}
catch ( IOException e )
{
//Best attempt at close
}
}
// ----------------------------------------------------------
public synchronized boolean hasFieldSetFor( String id, ClassLoader loader )
{
id = sanitizeId( id );
File dest = LocalityService.getSupportStrategy().getPersistentFile( baseDir,id + EXT );
return dest.exists();
}
// ----------------------------------------------------------
public synchronized void removeFieldSet( String id )
{
pLock.lock();
try
{
idCache.remove( id );
if ( usedIds != null )
{
usedIds.remove( id );
}
id = sanitizeId( id );
File dest = LocalityService.getSupportStrategy().getPersistentFile( baseDir,id + EXT );
if ( dest.exists() )
{
dest.delete();
}
}
finally
{
pLock.unlock();
}
}
public void lock()
{
pLock.lock();
}
public void unlock()
{
pLock.unlock();
}
// ----------------------------------------------------------
public synchronized void flushCache()
{
idCache.clear();
xstream.clear();
usedIds.clear();
}
// ----------------------------------------------------------
public synchronized void flushClassCacheFor( ClassLoader loader )
{
xstream.remove( loader );
}
// ----------------------------------------------------------
public static class StoredObject
{
public StoredObject( String id, String sanitizedId, Object value,
Snapshot fieldset, long timestamp )
{
this.id = id;
this.sanitizedId = sanitizedId;
this.value = value;
this.fieldset = fieldset;
this.timestamp = timestamp;
}
public String id()
{
return id;
}
public String sanitizedId()
{
return sanitizedId;
}
public Object value()
{
return value;
}
public Snapshot fieldset()
{
return fieldset;
}
public void setFieldset( Snapshot snaps )
{
fieldset = snaps;
}
public long timestamp()
{
return timestamp;
}
public void setValue( Object newValue )
{
value = newValue;
}
private String id;
private String sanitizedId;
private Object value;
private Snapshot fieldset;
private long timestamp;
}
// ~ Private Methods .......................................................
// ----------------------------------------------------------
/**
* Transforms an id into something safe to use as a file name. First, it
* url-encodes the id. Then it adds on a hex suffix that encodes the
* capitalization of the id. This second step is to ensure that names that
* differ only by case still map to unique file names, even on platforms
* that do not support case-sensitive file names (like Windows). Once
* converted, the results are cached so that the conversion can be faster on
* subsequent calls for the same id.
*
* @param id
* The id to transform
* @return A version of the id safe for use as a file name.
*/
public String sanitizeId( String id )
{
String result = idCache.get( id );
if ( result == null )
{
result = WebUtilities.urlEncode( id ) + "-";
int length = id.length();
int marker = 0;
for ( int i = 0; i < length; i++ )
{
if ( Character.isUpperCase( id.charAt( i ) ) )
{
marker += 1 << ( i % 4 );
}
if ( i % 4 == 3 )
{
result += Integer.toHexString( marker );
marker = 0;
}
}
if ( length % 4 > 0 )
{
result += Integer.toHexString( marker );
}
idCache.put( id, result );
idReverseCache.put( result, id );
}
return result;
}
// ----------------------------------------------------------
public String unsanitizeId( String id )
{
String result = idReverseCache.get( id );
if ( result == null )
{
String encodedBase = id;
String caps = "";
int pos = id.lastIndexOf( '-' );
if ( pos > 0 )
{
encodedBase = id.substring( 0, pos );
caps = id.substring( pos + 1 );
}
String unencoded = WebUtilities.urlDecode( encodedBase );
result = "";
pos = 0;
int length = caps.length();
for ( int i = 0; i < length && pos < unencoded.length(); i++ )
{
int digit = Integer.parseInt( caps.substring( i, i + 1 ), 16 );
for ( int j = 0; j < 4 && pos < unencoded.length(); j++ )
{
if ( ( digit & ( 1 << j ) ) != 0 )
{
result += Character.toUpperCase( unencoded.charAt( pos++ ) );
}
else
{
result += Character.toLowerCase( unencoded.charAt( pos++ ) );
}
}
}
if ( pos < unencoded.length() )
{
result += unencoded.substring( pos );
}
idReverseCache.put( id, result );
idCache.put( result, id );
}
return result;
}
// ----------------------------------------------------------
private XStreamBundle getXStreamFor( final ClassLoader loader, String key, Map<String,StoredObject> cache )
{
XStreamBundle result = xstream.get( loader );
if ( result == null )
{
result = AccessController.doPrivileged( new PrivilegedAction<XStreamBundle>()
{
public XStreamBundle run()
{
return new XStreamBundle( loader );
}
} );
xstream.put( loader, result );
}
result.init(key, cache);
return result;
}
// ----------------------------------------------------------
private static class XStreamBundle
{
XStream xstream;
private FlexibleFieldSetConverter fConverter;
private CollectionConverter cConverter;
private MapConverter mConverter;
private ArrayConverter aConverter;
private AliasConverter aliasConverter;
private CachedClassConverter cachedClassConverter;
public XStreamBundle( ClassLoader loader )
{
Map<String,Object> aliasContext = new HashMap<String,Object>();
cachedClassConverter= new CachedClassConverter(aliasContext);
try
{
if ( System.getSecurityManager() != null )
System.getSecurityManager().checkCreateClassLoader();
if(loader == null)
loader = this.getClass().getClassLoader();
xstream = new FlexibleXStream(loader,cachedClassConverter);
}
catch(AccessControlException e)
{
xstream = new FlexibleXStream(this.getClass().getClassLoader(),cachedClassConverter);
}
fConverter = new FlexibleFieldSetConverter( xstream.getMapper(),
xstream.getReflectionProvider(),aliasContext );
cachedClassConverter.setFlexibleFieldSetConverter(fConverter);
//Prevent Persistent Map from being converted.
PersistentMapConverter pConverter = new PersistentMapConverter();
xstream.registerConverter( pConverter, XStream.PRIORITY_VERY_HIGH );
aliasConverter = new AliasConverter(fConverter);
xstream.registerConverter( aliasConverter,XStream.PRIORITY_VERY_HIGH );
// flex field converter
xstream.registerConverter( fConverter, XStream.PRIORITY_VERY_LOW );
// Unrecognized Class Converter
UnrecognizedClassConverter ucc = new UnrecognizedClassConverter(xstream.getMapper());
xstream.registerConverter( ucc, XStream.PRIORITY_VERY_HIGH );
// //Collection Converter
cConverter = new CollectionConverter( xstream.getMapper() );
xstream.registerConverter( cConverter, XStream.PRIORITY_VERY_HIGH );
//
// //Map Converter
mConverter = new MapConverter( xstream.getMapper() );
xstream.registerConverter( mConverter, XStream.PRIORITY_VERY_HIGH );
//
// //Array Converter
aConverter = new ArrayConverter( xstream.getMapper() );
xstream.registerConverter( aConverter, XStream.PRIORITY_VERY_HIGH );
}
public void init(String key, Map<String,StoredObject> cache){
cachedClassConverter.init();
fConverter.setContext(key,cache);
}
}
public boolean hasFieldSetChanged( String key, long timestamp )
{
String sanitized = sanitizeId( key );
File persisted = LocalityService.getSupportStrategy().getPersistentFile( baseDir,sanitized + EXT );
return persisted.exists() && persisted.lastModified() > timestamp;
}
}