001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.ejb.plugins;
023:
024: import java.io.BufferedInputStream;
025: import java.io.BufferedOutputStream;
026: import java.io.File;
027: import java.io.FileInputStream;
028: import java.io.FileNotFoundException;
029: import java.io.FileOutputStream;
030: import java.io.IOException;
031: import java.rmi.RemoteException;
032: import java.security.AccessController;
033: import java.security.PrivilegedAction;
034: import java.security.PrivilegedActionException;
035: import java.security.PrivilegedExceptionAction;
036:
037: import javax.ejb.EJBException;
038: import javax.ejb.RemoveException;
039: import javax.ejb.SessionBean;
040:
041: import org.jboss.ejb.AllowedOperationsAssociation;
042: import org.jboss.ejb.Container;
043: import org.jboss.ejb.StatefulSessionContainer;
044: import org.jboss.ejb.StatefulSessionEnterpriseContext;
045: import org.jboss.ejb.StatefulSessionPersistenceManager;
046: import org.jboss.system.ServiceMBeanSupport;
047: import org.jboss.system.server.ServerConfigLocator;
048: import org.jboss.util.id.UID;
049:
050: /**
051: * A file-based stateful session bean persistence manager.
052: * <p>
053: * Reads and writes session bean objects to files by using the
054: * standard Java serialization mechanism.
055: * <p>
056: * Passivated state files are stored under:
057: * <tt><em>jboss-server-data-dir</em>/<em>storeDirectoryName</em>/<em>ejb-name</em>-<em>unique-id</em></tt>.
058: * <p>
059: * Since ejb-name is not unique across deployments we generate a <em>unique-id</em> to make
060: * sure that beans with the same EJB name do not collide.
061: *
062: * @author <a href="mailto:rickard.oberg@telkel.com">Rickard �berg</a>
063: * @author <a href="mailto:marc.fleury@telkel.com">Marc Fleury</a>
064: * @author <a href="mailto:sebastien.alborini@m4x.org">Sebastien Alborini</a>
065: * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
066: * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
067: * @version <tt>$Revision: 57209 $</tt>
068: */
069: public class StatefulSessionFilePersistenceManager extends
070: ServiceMBeanSupport implements
071: StatefulSessionPersistenceManager,
072: StatefulSessionFilePersistenceManagerMBean {
073: /** The default store directory name ("<tt>sessions</tt>"). */
074: public static final String DEFAULT_STORE_DIRECTORY_NAME = "sessions";
075:
076: /** Our container. */
077: private StatefulSessionContainer con;
078:
079: /**
080: * The sub-directory name under the server data directory where
081: * session data is stored.
082: *
083: * @see #DEFAULT_STORE_DIRECTORY_NAME
084: * @see #setStoreDirectoryName
085: */
086: private String storeDirName = DEFAULT_STORE_DIRECTORY_NAME;
087:
088: /** The base directory where sessions state files are stored for our container. */
089: private File storeDir;
090:
091: /**
092: * Enable purging leftover state files at create and destroy
093: * time (default is true).
094: */
095: private boolean purgeEnabled = true;
096:
097: /**
098: * Saves a reference to the {@link StatefulSessionContainer} for
099: * its bean type.
100: *
101: * @throws ClassCastException Container is not a StatefulSessionContainer.
102: */
103: public void setContainer(final Container con) {
104: this .con = (StatefulSessionContainer) con;
105: }
106:
107: //
108: // jason: these properties are intended to be used when plugins/interceptors
109: // can take configuration values (need to update xml schema and processors).
110: //
111:
112: /**
113: * Set the sub-directory name under the server data directory
114: * where session data will be stored.
115: *
116: * <p>
117: * This value will be appened to the value of
118: * <tt><em>jboss-server-data-dir</em></tt>.
119: *
120: * <p>
121: * This value is only used during creation and will not dynamically
122: * change the store directory when set after the create step has finished.
123: *
124: * @jmx:managed-attribute
125: *
126: * @param dirName A sub-directory name.
127: */
128: public void setStoreDirectoryName(final String dirName) {
129: this .storeDirName = dirName;
130: }
131:
132: /**
133: * Get the sub-directory name under the server data directory
134: * where session data is stored.
135: *
136: * jmx:managed-attribute
137: *
138: * @see #setStoreDirectoryName
139: *
140: * @return A sub-directory name.
141: */
142: public String getStoreDirectoryName() {
143: return storeDirName;
144: }
145:
146: /**
147: * Set the stale session state purge enabled flag.
148: *
149: * jmx:managed-attribute
150: *
151: * @param flag The toggle flag to enable or disable purging.
152: */
153: public void setPurgeEnabled(final boolean flag) {
154: this .purgeEnabled = flag;
155: }
156:
157: /**
158: * Get the stale session state purge enabled flag.
159: *
160: * jmx:managed-attribute
161: *
162: * @return True if purge is enabled.
163: */
164: public boolean getPurgeEnabled() {
165: return purgeEnabled;
166: }
167:
168: /**
169: * Returns the directory used to store session passivation state files.
170: *
171: * jmx:managed-attribute
172: *
173: * @return The directory used to store session passivation state files.
174: */
175: public File getStoreDirectory() {
176: return storeDir;
177: }
178:
179: /**
180: * Setup the session data storage directory.
181: *
182: * <p>Purges any existing session data found.
183: */
184: protected void createService() throws Exception {
185: // Initialize the dataStore
186: String ejbName = con.getBeanMetaData().getEjbName();
187:
188: // Get the system data directory
189: File dir = ServerConfigLocator.locate().getServerTempDir();
190:
191: // Setup the reference to the session data store directory
192: dir = new File(dir, storeDirName);
193: // ejbName is not unique across all deployments, so use a unique token
194: dir = new File(dir, ejbName + "-" + new UID().toString());
195: storeDir = dir;
196:
197: log.debug("Storing sessions for '" + ejbName + "' in: "
198: + storeDir);
199:
200: // if the directory does not exist then try to create it
201: if (!storeDir.exists()) {
202: if (MkdirsFileAction.mkdirs(storeDir) == false) {
203: throw new IOException("Failed to create directory: "
204: + storeDir);
205: }
206: }
207:
208: // make sure we have a directory
209: if (!storeDir.isDirectory()) {
210: throw new IOException(
211: "File exists where directory expected: " + storeDir);
212: }
213:
214: // make sure we can read and write to it
215: if (!storeDir.canWrite() || !storeDir.canRead()) {
216: throw new IOException(
217: "Directory must be readable and writable: "
218: + storeDir);
219: }
220:
221: // Purge state session state files, should be none, due to unique directory
222: purgeAllSessionData();
223: }
224:
225: /**
226: * Removes any state files left in the storgage directory.
227: */
228: private void purgeAllSessionData() {
229: if (!purgeEnabled)
230: return;
231:
232: log.debug("Purging all session data in: " + storeDir);
233:
234: File[] sessions = storeDir.listFiles();
235: for (int i = 0; i < sessions.length; i++) {
236: if (!sessions[i].delete()) {
237: log.warn("Failed to delete session state file: "
238: + sessions[i]);
239: } else {
240: log
241: .debug("Removed stale session state: "
242: + sessions[i]);
243: }
244: }
245: }
246:
247: /**
248: * Purge any data in the store, and then the store directory too.
249: */
250: protected void destroyService() throws Exception {
251: // Purge data and attempt to delete directory
252: purgeAllSessionData();
253:
254: // Nuke the directory too if purge is enabled
255: if (purgeEnabled && !storeDir.delete()) {
256: log
257: .warn("Failed to delete session state storage directory: "
258: + storeDir);
259: }
260: }
261:
262: /**
263: * Make a session state file for the given instance id.
264: */
265: private File getFile(final Object id) {
266: //
267: // jason: may have to translate id into a os-safe string, though
268: // the format of UID is safe on Unix and win32 already...
269: //
270:
271: return new File(storeDir, String.valueOf(id) + ".ser");
272: }
273:
274: /**
275: * @return A {@link UID}.
276: */
277: public Object createId(StatefulSessionEnterpriseContext ctx)
278: throws Exception {
279: return new UID();
280: }
281:
282: /**
283: * Non-operation.
284: */
285: public void createdSession(StatefulSessionEnterpriseContext ctx)
286: throws Exception {
287: // nothing
288: }
289:
290: /**
291: * Restores session state from the serialized file & invokes
292: * {@link SessionBean#ejbActivate} on the target bean.
293: */
294: public void activateSession(
295: final StatefulSessionEnterpriseContext ctx)
296: throws RemoteException {
297: boolean trace = log.isTraceEnabled();
298: if (trace) {
299: log.trace("Attempting to activate; ctx=" + ctx);
300: }
301:
302: Object id = ctx.getId();
303:
304: // Load state
305: File file = getFile(id);
306: if (trace) {
307: log.trace("Reading session state from: " + file);
308: }
309:
310: try {
311: FileInputStream fis = FISAction.open(file);
312: SessionObjectInputStream in = new SessionObjectInputStream(
313: ctx, new BufferedInputStream(fis));
314:
315: try {
316: Object obj = in.readObject();
317: if (trace) {
318: log.trace("Session state: " + obj);
319: }
320: ctx.setInstance(obj);
321: } finally {
322: in.close();
323: }
324: } catch (Exception e) {
325: throw new EJBException("Could not activate; failed to "
326: + "restore state", e);
327: }
328:
329: removePassivated(id);
330:
331: try {
332: // Instruct the bean to perform activation logic
333: AllowedOperationsAssociation
334: .pushInMethodFlag(IN_EJB_ACTIVATE);
335: SessionBean bean = (SessionBean) ctx.getInstance();
336: bean.ejbActivate();
337: } finally {
338: AllowedOperationsAssociation.popInMethodFlag();
339: }
340:
341: if (trace) {
342: log.trace("Activation complete; ctx=" + ctx);
343: }
344: }
345:
346: /**
347: * Invokes {@link SessionBean#ejbPassivate} on the target bean and saves the
348: * state of the session to a file.
349: */
350: public void passivateSession(
351: final StatefulSessionEnterpriseContext ctx)
352: throws RemoteException {
353: boolean trace = log.isTraceEnabled();
354: if (trace) {
355: log.trace("Attempting to passivate; ctx=" + ctx);
356: }
357:
358: try {
359: // Instruct the bean to perform passivation logic
360: AllowedOperationsAssociation
361: .pushInMethodFlag(IN_EJB_PASSIVATE);
362: SessionBean bean = (SessionBean) ctx.getInstance();
363: bean.ejbPassivate();
364: } finally {
365: AllowedOperationsAssociation.popInMethodFlag();
366: }
367:
368: // Store state
369:
370: File file = getFile(ctx.getId());
371: if (trace) {
372: log.trace("Saving session state to: " + file);
373: }
374:
375: try {
376: FileOutputStream fos = FOSAction.open(file);
377: SessionObjectOutputStream out = new SessionObjectOutputStream(
378: new BufferedOutputStream(fos));
379:
380: Object obj = ctx.getInstance();
381: if (trace) {
382: log.trace("Writing session state: " + obj);
383: }
384:
385: try {
386: out.writeObject(obj);
387: } finally {
388: out.close();
389: }
390: } catch (Exception e) {
391: throw new EJBException(
392: "Could not passivate; failed to save state", e);
393: }
394:
395: if (trace) {
396: log.trace("Passivation complete; ctx=" + ctx);
397: }
398: }
399:
400: /**
401: * Invokes {@link SessionBean#ejbRemove} on the target bean.
402: */
403: public void removeSession(final StatefulSessionEnterpriseContext ctx)
404: throws RemoteException, RemoveException {
405: boolean trace = log.isTraceEnabled();
406: if (trace) {
407: log.trace("Attempting to remove; ctx=" + ctx);
408: }
409:
410: // Instruct the bean to perform removal logic
411: SessionBean bean = (SessionBean) ctx.getInstance();
412: bean.ejbRemove();
413:
414: if (trace) {
415: log.trace("Removal complete; ctx=" + ctx);
416: }
417: }
418:
419: /**
420: * Removes the saved state file (if any) for the given session id.
421: */
422: public void removePassivated(final Object id) {
423: boolean trace = log.isTraceEnabled();
424:
425: File file = getFile(id);
426:
427: // only attempt to delete if the file exists
428: if (file.exists()) {
429: if (trace) {
430: log.trace("Removing passivated state file: " + file);
431: }
432:
433: if (DeleteFileAction.delete(file) == false) {
434: log.warn("Failed to delete passivated state file: "
435: + file);
436: }
437: }
438: }
439:
440: static class DeleteFileAction implements PrivilegedAction {
441: File file;
442:
443: DeleteFileAction(File file) {
444: this .file = file;
445: }
446:
447: public Object run() {
448: boolean deleted = file.delete();
449: return new Boolean(deleted);
450: }
451:
452: static boolean delete(File file) {
453: DeleteFileAction action = new DeleteFileAction(file);
454: Boolean deleted = (Boolean) AccessController
455: .doPrivileged(action);
456: return deleted.booleanValue();
457: }
458: }
459:
460: static class MkdirsFileAction implements PrivilegedAction {
461: File file;
462:
463: MkdirsFileAction(File file) {
464: this .file = file;
465: }
466:
467: public Object run() {
468: boolean ok = file.mkdirs();
469: return new Boolean(ok);
470: }
471:
472: static boolean mkdirs(File file) {
473: MkdirsFileAction action = new MkdirsFileAction(file);
474: Boolean ok = (Boolean) AccessController
475: .doPrivileged(action);
476: return ok.booleanValue();
477: }
478: }
479:
480: static class FISAction implements PrivilegedExceptionAction {
481: File file;
482:
483: FISAction(File file) {
484: this .file = file;
485: }
486:
487: public Object run() throws Exception {
488: FileInputStream fis = new FileInputStream(file);
489: return fis;
490: }
491:
492: static FileInputStream open(File file)
493: throws FileNotFoundException {
494: FISAction action = new FISAction(file);
495: FileInputStream fis = null;
496: try {
497: fis = (FileInputStream) AccessController
498: .doPrivileged(action);
499: } catch (PrivilegedActionException e) {
500: throw (FileNotFoundException) e.getException();
501: }
502:
503: return fis;
504: }
505: }
506:
507: static class FOSAction implements PrivilegedExceptionAction {
508: File file;
509:
510: FOSAction(File file) {
511: this .file = file;
512: }
513:
514: public Object run() throws Exception {
515: FileOutputStream fis = new FileOutputStream(file);
516: return fis;
517: }
518:
519: static FileOutputStream open(File file)
520: throws FileNotFoundException {
521: FOSAction action = new FOSAction(file);
522: FileOutputStream fos = null;
523: try {
524: fos = (FileOutputStream) AccessController
525: .doPrivileged(action);
526: } catch (PrivilegedActionException e) {
527: throw (FileNotFoundException) e.getException();
528: }
529:
530: return fos;
531: }
532: }
533: }
|