//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library.impl;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Date;
import java.io.*;
import openadk.library.*;
import openadk.library.infra.*;
/**
* A RequestCache implementation that stores SIF_Request information to a file
* in the agent work directory.
*
* @author Eric Petersen
* @version 1.0
*/
public class RequestCacheFile extends RequestCache
{
protected RandomAccessFile fFile;
protected HashMap fCache = new HashMap();
// TT 1105 Maximum storage size for each entry is 128k
private static final int MAX_ENTRY_SIZE = 131072;
private static final String CACHE_FILE = "requestcache.adk";
/* (non-Javadoc)
* @see openadk.library.impl.RequestCache#initialize(openadk.library.Agent)
*/
protected synchronized void initialize( Agent agent )
throws ADKException
{
// Look for the legacy version 1 request cache file, called "requests.adk"
String fname = agent.getHomeDir() + File.separator + "work" + File.separator + "requests.adk";
File cacheFile = new File( fname );
if( cacheFile.exists() )
{
// Read the legacy file and convert to the current format. This method will read the old file,
// store it in the current format, and then destroy the old file
convertLegacyFile( agent, cacheFile );
}
// Initialize the agent
initialize( agent, false );
}
/** Initializes the RequestCacheFile
* @param agent The agent to cache requests for
* @param isRetry False if this method is being called for the first time. This class may call this method again,
* with this parameter set to TRUE, which will signal that it is attempting to recover from a corrupt file condition.
* @throws ADKException
*/
private synchronized void initialize( Agent agent, boolean isRetry )
throws ADKException
{
// Ensure the requestcache.adk file exists in the work directory
String fname = agent.getHomeDir() + File.separator + "work" + File.separator + "requestcache.adk";
File cacheFile = new File( fname );
if( !cacheFile.exists() ) {
if( ( ADK.debug & ADK.DBG_LIFECYCLE ) != 0 )
Agent.getLog().info( "Creating SIF_Request ID cache: " + cacheFile.getAbsolutePath() );
}
try {
fFile = new RandomAccessFile( cacheFile, "rw" );
} catch( FileNotFoundException fnfe ) {
throw new ADKException( "Error opening or creating SIF_Request ID cache: " + fnfe, null );
}
// Read the file contents into memory
if( ( ADK.debug & ADK.DBG_LIFECYCLE ) != 0 )
{
Agent.getLog().debug( "Reading SIF_Request ID cache: " + cacheFile.getAbsolutePath() );
}
String tmpName = null;
RandomAccessFile tmp = null;
try
{
tmpName = agent.getHomeDir() + File.separator + "work" + File.separator + "requestcache.$dk";
tmp = new RandomAccessFile( tmpName, "rw" );
tmp.setLength( 0 );
int days = 90;
String str = System.getProperty( "adkglobal.requestCache.age" );
if( str != null && str.length() > 0 ) {
try {
days = Integer.parseInt(str);
} catch( Exception e ) {
Agent.getLog().warn( "Error parsing property 'adkglobal.requestCache.age', default of 90 days will be used: " + e.getMessage(), e );
}
}
// Clear out anything older than 90 days
Calendar now = Calendar.getInstance();
now.add( Calendar.DAY_OF_YEAR, -days );
Date maxAge = now.getTime();
RequestCacheFileEntry next = null;
while( ( next = read( fFile, false ) ) != null )
{
if( next.isActive() && next.getRequestTime().compareTo( maxAge ) > 0 )
{
try{
store( tmp, next );
}
catch( RequestSerializationException rse ){
Agent.getLog().warn( "Unable to store entry in the RequestCache: " + rse.toString(), rse );
}
}
}
tmp.close();
fFile.close();
//
// Overwrite the requests.adk file with the temporary, then
// delete the temporary.
//
File backupFile = new File( fname + ".bak" );
if( backupFile.exists() )
{
backupFile.delete();
}
cacheFile.renameTo( backupFile );
File newCacheFile = new File( tmpName );
newCacheFile.renameTo( cacheFile );
backupFile.delete();
try {
fFile = new RandomAccessFile( cacheFile, "rw" );
} catch( FileNotFoundException fnfe ) {
throw new ADKException( "Error opening or creating SIF_Request ID cache: " + fnfe, null );
}
if( ( ADK.debug & ADK.DBG_LIFECYCLE ) != 0 )
Agent.getLog().debug( "Read " + fCache.size() + " pending SIF_Request IDs from cache" );
}
catch( IOException ioe )
{
Agent.getLog().warn( "Could not read SIF_Request ID cache (will start with fresh cache): " + ioe );
// Make sure the files are closed
if( tmp != null ) {
try {
tmp.close();
} catch( Throwable ignored ) { /* Ignored exception */ }
}
if( fFile != null ) {
try {
fFile.close();
} catch( Throwable ignored ) { /* Ignored exception */ }
}
if( isRetry )
{
throw new ADKException( "Error opening or creating SIF_Request ID cache: " + ioe, null, ioe );
}
else
{
//
// Delete the files and re-initialize from scratch. We don't
// want a file error here to prevent the agent from running, so
// no exception is thrown to the caller.
//
File del = new File( fname );
del.delete();
del = new File( tmpName );
del.delete();
initialize( agent, true );
}
}
}
private void store( RandomAccessFile file, RequestCacheFileEntry entry )
throws IOException, RequestSerializationException
{
entry.setRequestTime( new Date() );
fCache.put( entry.getMessageId(), entry );
file.seek( file.length() );
entry.setLocation( file.length() );
file.writeBoolean( entry.isActive() );
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream( out );
objOut.writeObject( entry );
if( out.size() > MAX_ENTRY_SIZE ){
throw new RequestSerializationException( "RequestCacheFileEntry maximum allowable serialized size is 128k. Size is: " + out.size(), null );
}
byte[] serialized = out.toByteArray();
out.close();
file.writeInt( serialized.length );
file.write( serialized );
}
/**
* Reads the next RequestCacheFileEntry from the file stream
* @param file the filestream to read the entry from
* @param readInactiveData If TRUE, all entry properties will be deserialized, even if inactive.
* If FALSE, only active entries will be deserialized and inactive entries will be returned as a
* RequestCacheFileEntry with the isActive() property set to FALSE
* @return The next RequestCacheFileEntry or NULL if at the end of the stream
*/
private RequestCacheFileEntry read( DataInput file, boolean readInactiveData ) throws IOException
{
try
{
boolean active = file.readBoolean();
int length = file.readInt();
if( length > MAX_ENTRY_SIZE ){
// Prevent Out of Memory errors caused by corrupt file. The maximum size allowed for a serialied
// RequestCacheFileEntry is 128K
Agent.getLog().error( "Problem reading RequestCache due to unknown corruption. Entries likely are lost." );
return null;
}
if( !active && !readInactiveData )
{
file.skipBytes( length );
return new RequestCacheFileEntry( false );
}
else
{
try
{
// Read the serialized object into a buffer and deserialize
byte[] serialized = new byte[ length ];
file.readFully( serialized );
ByteArrayInputStream input = new ByteArrayInputStream( serialized );
ObjectInputStream dataInput = new ObjectInputStream( input );
RequestCacheFileEntry returnValue = ( RequestCacheFileEntry )dataInput.readObject();
dataInput.close();
input.close();
returnValue.setIsActive( active );
return returnValue;
}
catch( ClassNotFoundException cfne )
{
// Not sure why this would happen, but we'll just return
// an inactive record
return new RequestCacheFileEntry( false );
}
catch( IOException iox )
{
Agent.getLog().warn( "Error Deserializing RequestCacheFileEntry: " + iox.getMessage(), iox );
return new RequestCacheFileEntry( false );
}
}
}
catch( EOFException eofe )
{
// We're at the end of the file
return null;
}
}
/**
* Reads the original version of the RequestsCacheFile cache, which did not support
* storing user state. If the ADK finds a file called "requests.adk" in the agent's work
* directory, it will use this method to read the contents. The results are then stored in
* the ADK's current format in a file called "requests2.adk".
*
*/
private void convertLegacyFile( Agent agent, File legacyFileInfo )
{
RandomAccessFile newFile = null;
RandomAccessFile legacyFile = null;
try
{
String fname = agent.getHomeDir() + File.separator + "work" + File.separator + CACHE_FILE;
File newFileInfo = new File( fname );
if( newFileInfo.exists() ){
throw new IOException( "File " + fname + " already exists. Legacy file will not be converted." );
}
newFile = new RandomAccessFile( fname, "rw" );
legacyFile = new RandomAccessFile( legacyFileInfo.getPath(), "r" );
// Use the old method for reading the data out of the file
// Store the data read in the new format
boolean active;
long length = legacyFile.length();
long ptr = 0;
String msgId = null;
String objType = null;
long timestamp = 0;
while( ptr < length )
{
active = legacyFile.readBoolean();
msgId = legacyFile.readUTF();
objType = legacyFile.readUTF();
timestamp = legacyFile.readLong();
if( active )
{
// Add to cache
RequestCacheFileEntry newEntry = new RequestCacheFileEntry( active );
newEntry.setIsActive( true );
newEntry.setMessageId( msgId );
newEntry.setObjectType( objType );
newEntry.setRequestTime( new Date( timestamp ) );
try{
store( newFile, newEntry );
}
catch( RequestSerializationException rse ){
Agent.getLog().warn( "Unable to store entry in the RequestCache: " + rse.toString(), rse );
}
}
ptr = legacyFile.getFilePointer();
}
}
catch( Exception ex )
{
Agent.getLog().warn( "An error occurred while attempting to upgrade the ADK Request Cache format: " + ex.toString(), ex );
}
finally
{
fCache.clear();
// Against all odds, try deleting the legacy file
try
{
if( newFile != null ){
newFile.close();
}
if( legacyFile != null ){
legacyFile.close();
}
legacyFileInfo.delete();
}
catch( Exception ex )
{
Agent.getLog().warn( "Unable to delete legacy file " + legacyFileInfo.getPath(), ex );
}
}
}
/**
* Returns the number of requests that are current active
*
* @return The number of active requests
*/
public synchronized int getActiveRequestCount()
{
return fCache.size();
}
/**
* Closes the RequestCache
*/
public synchronized void close()
throws ADKException
{
try {
if( fFile != null )
fFile.close();
} catch( IOException ioe ) {
throw new ADKException( "Error closing SIF_Request ID cache: " + ioe, null );
}
sSingleton = null;
}
/**
* Store the request MsgId and associated SIF Data Object type in the cache
*/
public synchronized RequestInfo storeRequestInfo( SIF_Request request, Query q, Zone zone )
throws ADKException
{
Object userData = q.getUserData();
if( userData != null )
{
if( ! ( userData instanceof Serializable ) ){
throw new ADKException( "Query.getUserData() contains " +
userData.toString() + " which is not Serializable", null );
}
}
try
{
RequestCacheFileEntry entry = new RequestCacheFileEntry( true );
entry.setObjectType( request.getSIF_Query().getSIF_QueryObject().getObjectName() );
entry.setMessageId( request.getMsgId() );
entry.setUserData( q.getUserData() );
store( fFile, entry);
return entry;
}
catch( Throwable thr )
{
throw new ADKException( "Error writing to SIF_Request ID cache (MsgId: " + request.getMsgId() + ") " + thr, zone );
}
}
/* (non-Javadoc)
* @see openadk.library.impl.RequestCache#getRequestInfo(java.lang.String, openadk.library.Zone)
*/
public synchronized RequestInfo getRequestInfo( String msgId, Zone zone )
throws ADKException
{
return lookup( msgId, zone, true );
}
/* (non-Javadoc)
* @see openadk.library.impl.RequestCache#lookupRequestInfo(java.lang.String, openadk.library.Zone)
*/
public synchronized RequestInfo lookupRequestInfo( String msgId, Zone zone )
throws ADKException
{
return lookup( msgId, zone, false );
}
/**
* Looks up the specified entry in the cache.
* @param msgId The message id to lookup
* @param zone The zone associated with the message
* @param remove If TRUE, the entry will be removed from the cache
* @return The entry associated with the specified message ID or NULL if no entry was found
* @throws ADKException
*/
protected RequestCacheFileEntry lookup( String msgId, Zone zone, boolean remove )
throws ADKException
{
RequestCacheFileEntry e = (RequestCacheFileEntry)fCache.get(msgId);
if( e == null )
{
return null;
}
if( remove )
{
try {
fCache.remove( msgId );
fFile.seek( e.getLocation() );
fFile.writeBoolean( false );
} catch( IOException ioe ) {
throw new ADKException( "Error removing entry from SIF_Request ID cache: " + ioe, zone );
}
}
return e;
}
class RequestSerializationException extends Exception
{
public RequestSerializationException( String message ) {
super( message );
}
public RequestSerializationException( String message, Throwable thr ) {
super( message, thr );
}
}
@Override
public RequestInfo storeServiceRequestInfo(SIF_ServiceInput request,
Query q, Zone zone) throws ADKException {
Object userData = q.getUserData();
if( userData != null )
{
if( ! ( userData instanceof Serializable ) ){
throw new ADKException( "Query.getUserData() contains " +
userData.toString() + " which is not Serializable", null );
}
}
try
{
RequestCacheFileEntry entry = new RequestCacheFileEntry( true );
// entry.setObjectType( request.getSIF_Query().getSIF_QueryObject().getObjectName() );
entry.setObjectType( request.getSIF_Service());
entry.setMessageId( request.getMsgId() );
entry.setUserData( q.getUserData() );
store( fFile, entry);
return entry;
}
catch( Throwable thr )
{
throw new ADKException( "Error writing to SIF_Request ID cache (MsgId: " + request.getMsgId() + ") " + thr, zone );
}
}
}