/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ejb.plugins;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.rmi.RemoteException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import javax.ejb.EJBException;
import javax.ejb.RemoveException;
import javax.ejb.SessionBean;
import org.jboss.ejb.AllowedOperationsAssociation;
import org.jboss.ejb.Container;
import org.jboss.ejb.StatefulSessionContainer;
import org.jboss.ejb.StatefulSessionEnterpriseContext;
import org.jboss.ejb.StatefulSessionPersistenceManager;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.system.server.ServerConfigLocator;
import org.jboss.util.id.UID;
/**
* A file-based stateful session bean persistence manager.
* <p>
* Reads and writes session bean objects to files by using the
* standard Java serialization mechanism.
* <p>
* Passivated state files are stored under:
* <tt><em>jboss-server-data-dir</em>/<em>storeDirectoryName</em>/<em>ejb-name</em>-<em>unique-id</em></tt>.
* <p>
* Since ejb-name is not unique across deployments we generate a <em>unique-id</em> to make
* sure that beans with the same EJB name do not collide.
*
* @author <a href="mailto:rickard.oberg@telkel.com">Rickard Oberg</a>
* @author <a href="mailto:marc.fleury@telkel.com">Marc Fleury</a>
* @author <a href="mailto:sebastien.alborini@m4x.org">Sebastien Alborini</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
* @version <tt>$Revision: 91254 $</tt>
*/
public class StatefulSessionFilePersistenceManager extends ServiceMBeanSupport
implements StatefulSessionPersistenceManager, StatefulSessionFilePersistenceManagerMBean
{
/** The default store directory name ("<tt>sessions</tt>"). */
public static final String DEFAULT_STORE_DIRECTORY_NAME = "sessions";
/** Our container. */
private StatefulSessionContainer con;
/**
* The sub-directory name under the server data directory where
* session data is stored.
*
* @see #DEFAULT_STORE_DIRECTORY_NAME
* @see #setStoreDirectoryName
*/
private String storeDirName = DEFAULT_STORE_DIRECTORY_NAME;
/** The base directory where sessions state files are stored for our container. */
private File storeDir;
/**
* Enable purging leftover state files at create and destroy
* time (default is true).
*/
private boolean purgeEnabled = true;
/**
* Saves a reference to the {@link StatefulSessionContainer} for
* its bean type.
*
* @throws ClassCastException Container is not a StatefulSessionContainer.
*/
public void setContainer(final Container con)
{
this.con = (StatefulSessionContainer) con;
}
//
// jason: these properties are intended to be used when plugins/interceptors
// can take configuration values (need to update xml schema and processors).
//
/**
* Set the sub-directory name under the server data directory
* where session data will be stored.
*
* <p>
* This value will be appened to the value of
* <tt><em>jboss-server-data-dir</em></tt>.
*
* <p>
* This value is only used during creation and will not dynamically
* change the store directory when set after the create step has finished.
*
* @jmx:managed-attribute
*
* @param dirName A sub-directory name.
*/
public void setStoreDirectoryName(final String dirName)
{
this.storeDirName = dirName;
}
/**
* Get the sub-directory name under the server data directory
* where session data is stored.
*
* jmx:managed-attribute
*
* @see #setStoreDirectoryName
*
* @return A sub-directory name.
*/
public String getStoreDirectoryName()
{
return storeDirName;
}
/**
* Set the stale session state purge enabled flag.
*
* jmx:managed-attribute
*
* @param flag The toggle flag to enable or disable purging.
*/
public void setPurgeEnabled(final boolean flag)
{
this.purgeEnabled = flag;
}
/**
* Get the stale session state purge enabled flag.
*
* jmx:managed-attribute
*
* @return True if purge is enabled.
*/
public boolean getPurgeEnabled()
{
return purgeEnabled;
}
/**
* Returns the directory used to store session passivation state files.
*
* jmx:managed-attribute
*
* @return The directory used to store session passivation state files.
*/
public File getStoreDirectory()
{
return storeDir;
}
/**
* Setup the session data storage directory.
*
* <p>Purges any existing session data found.
*/
protected void createService() throws Exception
{
// Initialize the dataStore
String ejbName = con.getBeanMetaData().getEjbName();
// Get the system data directory
File dir = new File(ServerConfigLocator.locate().getServerTempLocation().toURI());
// Setup the reference to the session data store directory
dir = new File(dir, storeDirName);
// ejbName is not unique across all deployments, so use a unique token
dir = new File(dir, ejbName + "-" + new UID().toString());
storeDir = dir;
log.debug("Storing sessions for '" + ejbName + "' in: " + storeDir);
// if the directory does not exist then try to create it
if( !storeDir.exists() )
{
if( MkdirsFileAction.mkdirs(storeDir) == false )
{
throw new IOException("Failed to create directory: " + storeDir);
}
}
// make sure we have a directory
if( !storeDir.isDirectory() )
{
throw new IOException("File exists where directory expected: " + storeDir);
}
// make sure we can read and write to it
if( !storeDir.canWrite() || !storeDir.canRead() )
{
throw new IOException("Directory must be readable and writable: " + storeDir);
}
// Purge state session state files, should be none, due to unique directory
purgeAllSessionData();
}
/**
* Removes any state files left in the storgage directory.
*/
private void purgeAllSessionData()
{
if( !purgeEnabled ) return;
log.debug("Purging all session data in: " + storeDir);
File[] sessions = storeDir.listFiles();
for(int i = 0; i < sessions.length; i++)
{
if( !sessions[i].delete() )
{
log.warn("Failed to delete session state file: " + sessions[i]);
}
else
{
log.debug("Removed stale session state: " + sessions[i]);
}
}
}
/**
* Purge any data in the store, and then the store directory too.
*/
protected void destroyService() throws Exception
{
// Purge data and attempt to delete directory
purgeAllSessionData();
// Nuke the directory too if purge is enabled
if( purgeEnabled && !storeDir.delete() )
{
log.warn("Failed to delete session state storage directory: " + storeDir);
}
}
/**
* Make a session state file for the given instance id.
*/
private File getFile(final Object id)
{
//
// jason: may have to translate id into a os-safe string, though
// the format of UID is safe on Unix and win32 already...
//
return new File(storeDir, String.valueOf(id) + ".ser");
}
/**
* @return A {@link UID}.
*/
public Object createId(StatefulSessionEnterpriseContext ctx)
throws Exception
{
return new UID();
}
/**
* Non-operation.
*/
public void createdSession(StatefulSessionEnterpriseContext ctx)
throws Exception
{
// nothing
}
/**
* Restores session state from the serialized file & invokes
* {@link SessionBean#ejbActivate} on the target bean.
*/
public void activateSession(final StatefulSessionEnterpriseContext ctx)
throws RemoteException
{
boolean trace = log.isTraceEnabled();
if (trace)
{
log.trace("Attempting to activate; ctx=" + ctx);
}
Object id = ctx.getId();
// Load state
File file = getFile(id);
if (trace)
{
log.trace("Reading session state from: " + file);
}
try
{
FileInputStream fis = FISAction.open(file);
SessionObjectInputStream in = new SessionObjectInputStream(ctx,
new BufferedInputStream(fis));
try
{
Object obj = in.readObject();
if (trace)
{
log.trace("Session state: " + obj);
}
ctx.setInstance(obj);
}
finally
{
in.close();
}
}
catch(Exception e)
{
throw new EJBException("Could not activate; failed to " +
"restore state", e);
}
removePassivated(id);
try
{
// Instruct the bean to perform activation logic
AllowedOperationsAssociation.pushInMethodFlag(IN_EJB_ACTIVATE);
SessionBean bean = (SessionBean) ctx.getInstance();
bean.ejbActivate();
}
finally
{
AllowedOperationsAssociation.popInMethodFlag();
}
if (trace)
{
log.trace("Activation complete; ctx=" + ctx);
}
}
/**
* Invokes {@link SessionBean#ejbPassivate} on the target bean and saves the
* state of the session to a file.
*/
public void passivateSession(final StatefulSessionEnterpriseContext ctx)
throws RemoteException
{
boolean trace = log.isTraceEnabled();
if (trace)
{
log.trace("Attempting to passivate; ctx=" + ctx);
}
try
{
// Instruct the bean to perform passivation logic
AllowedOperationsAssociation.pushInMethodFlag(IN_EJB_PASSIVATE);
SessionBean bean = (SessionBean) ctx.getInstance();
bean.ejbPassivate();
}
finally
{
AllowedOperationsAssociation.popInMethodFlag();
}
// Store state
File file = getFile(ctx.getId());
if (trace)
{
log.trace("Saving session state to: " + file);
}
try
{
FileOutputStream fos = FOSAction.open(file);
SessionObjectOutputStream out = new SessionObjectOutputStream(
new BufferedOutputStream(fos));
Object obj = ctx.getInstance();
if (trace)
{
log.trace("Writing session state: " + obj);
}
try
{
out.writeObject(obj);
}
finally
{
out.close();
}
}
catch(Exception e)
{
throw new EJBException("Could not passivate; failed to save state", e);
}
if (trace)
{
log.trace("Passivation complete; ctx=" + ctx);
}
}
/**
* Invokes {@link SessionBean#ejbRemove} on the target bean.
*/
public void removeSession(final StatefulSessionEnterpriseContext ctx)
throws RemoteException, RemoveException
{
boolean trace = log.isTraceEnabled();
if (trace)
{
log.trace("Attempting to remove; ctx=" + ctx);
}
// Instruct the bean to perform removal logic
SessionBean bean = (SessionBean) ctx.getInstance();
bean.ejbRemove();
if (trace)
{
log.trace("Removal complete; ctx=" + ctx);
}
}
/**
* Removes the saved state file (if any) for the given session id.
*/
public void removePassivated(final Object id)
{
boolean trace = log.isTraceEnabled();
File file = getFile(id);
// only attempt to delete if the file exists
if( file.exists() )
{
if (trace)
{
log.trace("Removing passivated state file: " + file);
}
if( DeleteFileAction.delete(file) == false )
{
log.warn("Failed to delete passivated state file: " + file);
}
}
}
static class DeleteFileAction implements PrivilegedAction
{
File file;
DeleteFileAction(File file)
{
this.file = file;
}
public Object run()
{
boolean deleted = file.delete();
return new Boolean(deleted);
}
static boolean delete(File file)
{
DeleteFileAction action = new DeleteFileAction(file);
Boolean deleted = (Boolean) AccessController.doPrivileged(action);
return deleted.booleanValue();
}
}
static class MkdirsFileAction implements PrivilegedAction
{
File file;
MkdirsFileAction(File file)
{
this.file = file;
}
public Object run()
{
boolean ok = file.mkdirs();
return new Boolean(ok);
}
static boolean mkdirs(File file)
{
MkdirsFileAction action = new MkdirsFileAction(file);
Boolean ok = (Boolean) AccessController.doPrivileged(action);
return ok.booleanValue();
}
}
static class FISAction implements PrivilegedExceptionAction
{
File file;
FISAction(File file)
{
this.file = file;
}
public Object run() throws Exception
{
FileInputStream fis = new FileInputStream(file);
return fis;
}
static FileInputStream open(File file) throws FileNotFoundException
{
FISAction action = new FISAction(file);
FileInputStream fis = null;
try
{
fis = (FileInputStream) AccessController.doPrivileged(action);
}
catch(PrivilegedActionException e)
{
throw (FileNotFoundException) e.getException();
}
return fis;
}
}
static class FOSAction implements PrivilegedExceptionAction
{
File file;
FOSAction(File file)
{
this.file = file;
}
public Object run() throws Exception
{
FileOutputStream fis = new FileOutputStream(file);
return fis;
}
static FileOutputStream open(File file) throws FileNotFoundException
{
FOSAction action = new FOSAction(file);
FileOutputStream fos = null;
try
{
fos = (FileOutputStream) AccessController.doPrivileged(action);
}
catch(PrivilegedActionException e)
{
throw (FileNotFoundException) e.getException();
}
return fos;
}
}
}