1 package uk.ac.vamsas.client.simpleclient;
4 import java.io.FileNotFoundException;
5 import java.io.IOException;
6 import java.io.RandomAccessFile;
7 import java.nio.channels.FileChannel;
8 import java.nio.channels.OverlappingFileLockException;
10 import org.apache.commons.logging.Log;
11 import org.apache.commons.logging.LogFactory;
12 import org.apache.log4j.FileAppender;
13 import org.apache.log4j.Logger;
14 import org.apache.log4j.PatternLayout;
16 import uk.ac.vamsas.client.ClientHandle;
17 import uk.ac.vamsas.client.Events;
18 import uk.ac.vamsas.client.IClient;
19 import uk.ac.vamsas.client.SessionHandle;
20 import uk.ac.vamsas.client.UserHandle;
23 * Does all the IO operations for a SimpleClient instance accessing
24 * a SimpleClient vamsas session.
26 * Basically, it defines the various standard names for the files
27 * in the session directory (that maps to the sessionUrn),
28 * provides constructors for the file handlers and watchers of
29 * those file entities, and some higher level methods
30 * to check and change the state flags for the session.
32 * TODO: move the stuff below to the SimpleClientFactory documentation.
33 * much may not be valid now :
34 * Vamsas client is intialised with a path to create live session directories.
35 * This path may contain a vamsas.properties file
36 * that sets additional parameters (otherwise client
37 * just uses the one on the classpath).
39 * A vamsas session consists of :
40 * SessionDir - translates to urn of a live session.
41 * Contains: Vamsas Document (as a jar), Session client list file,
42 * both of which may be locked, and additional
43 * temporary versions of these files when write
44 * operations are taking place.
47 * - vamsasdocument.xml : core info
49 * - <applicationname>.version.sessionnumber.raw (string given in vamsasdocument.xml applicationData entry)
52 * - filename given in the vamsasdocument.xml. Should be checked for validity by any client and rewritten if necessary.
53 * The lockfile can point to the jar itself.
55 * Initially - documentHandler either:
56 * - creates a zip for a new session for the client
57 * - connect to an existing session zip
58 * 1. reads session urn file
60 * 3. examines session - decide whether to create new application data slice or connect to one stored in session.
61 * 4. writes info into session file
62 * 5. releases lock and generates local client events.
63 * 6. Creates Watcher thread to generate events.
66 * - Update watcher checks for file change -
68 * Procedures for file based session message exchange
69 * - session document modification flag
73 public class VamsasSession {
75 * indicator file for informing other processes that
76 * they should finalise their vamsas datasets for
77 * storing into a vamsas archive.
79 public static final String CLOSEANDSAVE_FILE = "stored.log";
82 * session file storing the last_stored_stat data
84 public static final String MODIFIEDDOC_FILE = "modified";
86 private SimpleSessionManager sessionManager = null;
89 * Count of cycles before considering the current client as the last one of the session (if no other client registered as active )
91 private final int watchCycleCountBeforeLastClient = 1220;
94 * time between checking
96 public int WATCH_SLEEP = 30;
98 protected String clientFileDirectory = "clients";
101 * called to clear update flag after a successful offline storage event
103 protected void clearUnsavedFlag() {
104 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir,
106 if (!laststored.clearFlag())
107 log.warn("Unsaved flag was not cleared for " + sessionDir);
111 * called to indicate session document has been modified.
114 protected void setUnsavedFlag() {
115 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir,
117 if (!laststored.setFlag())
118 log.warn("Couldn't set the Unsaved flag for " + sessionDir);
123 * @return true if session document has been modified since last offline storage event
125 protected boolean getUnsavedFlag() {
126 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir,
128 return laststored.checkFlag();
134 public static final String SESSION_LOG = "Log.txt";
136 private static Log log = LogFactory.getLog(VamsasSession.class);
138 protected Logger slog = Logger.getLogger("uk.ac.vamsas.client.SessionLog");
140 * the appender that writes to the log file inside the session's directory.
142 private FileAppender slogAppender=null;
144 * setup the sessionLog using Log4j.
145 * @throws IOException
147 private void initLog() throws IOException {
148 // TODO: fix session event logging
149 // LATER: make dedicated appender format for session log.
150 /*Appender app = slog.getAppender("log4j.appender.SESSIONLOG");
151 // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
152 // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
153 for (Enumeration e = slog.getAllAppenders() ; e.hasMoreElements() ;) {
154 System.out.println(e.nextElement());
159 File sessionLogFile = new File(this.sessionDir, SESSION_LOG);
160 slog.addAppender(slogAppender = new FileAppender(new PatternLayout(
161 "%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(),
164 log.info("No appender for SessionLog");
167 private void closeSessionLog() {
170 if (slogAppender!=null)
172 slog.removeAppender(slogAppender);
173 slogAppender.close();
180 * the sessionDir is given as the session location for new clients.
182 protected File sessionDir;
185 * holds the list of attached clients
189 public static final String CLIENT_LIST = "Clients.obj";
194 VamsasFile vamArchive;
196 public static final String VAMSAS_OBJ = "VamDoc.jar";
199 * sets up the vamsas session files and watchers in sessionDir1
202 protected VamsasSession(File sessionDir1) throws IOException {
203 this(sessionDir1, null);
206 * sets up the vamsas session files and watchers in sessionDir1
208 * @param extVamDoc null or an existing archive to initialise the session with
209 * @throws any IOExceptions from creating session directory and files.
210 * @throws error if both extVamDoc and sessionDir1 already exist (cannot import new data into session in this way)
212 protected VamsasSession(File sessionDir1, File extVamDoc) throws IOException {
213 if (sessionDir1 == null)
214 throw new Error("Null directory for VamsasSession.");
215 if (sessionDir1.exists()) {
216 if (extVamDoc!=null && extVamDoc.exists())
217 throw new Error("Client Initialisation Error: Cannot join an existing session directory with an existing vamsas document to import.");
218 if (!sessionDir1.isDirectory() || !sessionDir1.canWrite()
219 || !sessionDir1.canRead())
220 throw new IOException("Cannot access '" + sessionDir1
221 + "' as a read/writable Directory.");
222 if (!checkSessionFiles(sessionDir1))
224 .warn("checkSessionFiles() returned false. Possible client implementation error");
225 this.sessionDir = sessionDir1;
226 initSessionObjects();
227 slog.debug("Initialising additional VamsasSession instance");
228 log.debug("Attached to VamsasSession in " + sessionDir1);
231 // start from scratch
232 if (!sessionDir1.mkdir())
233 throw new IOException("Failed to make VamsasSession directory in "
235 createSessionFiles(extVamDoc);
236 initSessionObjects();
237 slog.debug("Session directory created.");
238 log.debug("Initialised VamsasSession in " + sessionDir1);
243 * tests presence of existing sessionfiles files in dir
247 private boolean checkSessionFiles(File dir) throws IOException {
248 File c_file = new File(dir, CLIENT_LIST);
249 File v_doc = new File(dir, VAMSAS_OBJ);
250 if (c_file.exists() && v_doc.exists())
256 * create new empty files in dir
259 private void createSessionFiles() throws IOException {
260 createSessionFiles(null);
264 * @param extVamDoc null or an existing vamsas document to initialise session with
265 * @throws IOException
267 private void createSessionFiles(File extVamDoc) throws IOException {
268 if (sessionDir == null)
269 throw new IOException(
270 "Invalid call to createSessionFiles() with null sessionDir");
271 File c_file = new File(sessionDir, CLIENT_LIST);
272 File v_doc = new File(sessionDir, VAMSAS_OBJ);
273 if (!c_file.exists() && c_file.createNewFile())
274 log.debug("Created new ClientFile " + c_file); // don't care if this works or not
279 if (v_doc.createNewFile())
281 log.debug("Created new Vamsas Session Document File " + v_doc);
283 log.warn("Didn't create Vamsas Session Document file in "+v_doc);
286 log.debug("Creating new session document from "+extVamDoc);
288 SessionFile sesdoc = new SessionFile(new File(sessionDir, VAMSAS_OBJ));
289 SessionFile extdoc = new SessionFile(extVamDoc);
290 sesdoc.updateFrom(null, extdoc);
291 } catch (Exception e)
294 v_doc.createNewFile();
295 log.warn("Problem initialising new session ("+v_doc+") from existing vamsas document ("+extVamDoc+")",e);
296 throw new IOException("Couldn't initialise session from existing vamsas document");
298 log.debug("Session document initialised from "+extVamDoc);
304 * construct SessionFile objects and watchers for each
306 private void initSessionObjects() throws IOException {
307 createSessionFiles();
308 if (clist != null || vamArchive != null)
309 throw new IOException(
310 "initSessionObjects called for initialised VamsasSession object.");
311 clist = new ClientsFile(new File(sessionDir, CLIENT_LIST));
312 vamArchive = new VamsasFile(new File(sessionDir, VAMSAS_OBJ));
313 storedocfile = new ClientsFile(new File(sessionDir, CLOSEANDSAVE_FILE));
318 * make a new watcher object for the clientFile
319 * @return new ClientFile watcher instance
321 public FileWatcher getClientWatcher() {
322 return new FileWatcher(clist.sessionFile);
326 * make a new watcher object for the vamsas Document
327 * @return new ClientFile watcher instance
329 public FileWatcher getDocWatcher() {
330 return new FileWatcher(vamArchive.sessionFile);
333 FileWatcher store_doc_file = null;
335 public ClientsFile storedocfile = null;
338 * make a new watcher object for the messages file
339 * @return new watcher instance
341 public FileWatcher getStoreWatcher() {
342 return new FileWatcher(new File(sessionDir, CLOSEANDSAVE_FILE));
347 * write to the StoreWatcher file to indicate that a storeDocumentRequest has been made.
348 * The local client's storeWatcher FileWatcher object is updated so the initial change is not registered.
353 public void addStoreDocumentRequest(ClientHandle client, UserHandle user)
355 // TODO: replace this with clientsFile mechanism
356 SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE));
357 while (!sfw.lockFile())
358 log.debug("Trying to get lock for " + CLOSEANDSAVE_FILE);
359 RandomAccessFile sfwfile = sfw.fileLock.getRaFile();
360 sfwfile.setLength(0); // wipe out any old info.
361 // TODO: rationalise what gets written to this file (ie do we want other clients to read the id of the requestor?)
362 sfwfile.writeUTF(client.getClientUrn() + ":" + user.getFullName() + "@"
363 + user.getOrganization());
365 if (store_doc_file != null)
366 store_doc_file.setState();
367 slog.info("FinalizeAppData request from " + user.getFullName() + " using "
368 + client.getClientUrn() + "");
372 * create a new session with an existing vamsas Document - by copying it into the session.
375 public void setVamsasDocument(File archive) throws IOException {
376 log.debug("Transferring vamsas data from " + archive + " to session:"
377 + vamArchive.sessionFile);
378 SessionFile xtantdoc = new SessionFile(archive);
379 vamArchive.updateFrom(null, xtantdoc);
380 // LATER: decide if session archive provenance should be updated to reflect access.
381 // TODO: soon! do a proper import objects from external file
382 log.debug("Transfer complete.");
386 * write session as a new vamsas Document (this will overwrite any existing file without warning)
388 * TODO: verify that lock should be released for vamsas document.
391 protected void writeVamsasDocument(File destarchive, Lock extlock)
393 log.debug("Transferring vamsas data from " + vamArchive.sessionFile
394 + " to session:" + destarchive);
395 SessionFile newdoc = new SessionFile(destarchive);
396 if (extlock == null && !vamArchive.lockFile())
397 while (!vamArchive.lockFile())
398 log.info("Trying to get lock for " + vamArchive.sessionFile);
399 // TODO: LATER: decide if a provenance entry should be written in the exported document recording the export from the session
400 newdoc.updateFrom(null, vamArchive);
401 // LATER: LATER: fix use of updateFrom for file systems where locks cannot be made (because they don't have a lockManager, ie NFS/Unix, etc).
404 log.debug("Transfer complete.");
408 * extant archive IO handler
410 VamsasArchive _va = null;
413 * Creates a VamsasArchive Vobject for accessing and updating document
414 * Note: this will lock the Vamsas Document for exclusive access to the client.
415 * @return session vamsas document
416 * @throws IOException if locks fail or vamsas document read fails.
418 protected VamsasArchive getVamsasDocument() throws IOException {
419 // check we haven't already done this once - probably should be done by caller
422 // patiently wait for a lock on the document. (from ArchiveClient.getUpdateable())
424 while (vamArchive.getLock() == null && --tries > 0) {
426 log.debug("Trying to get a document lock for the " + tries + "'th time.");
429 throw new IOException("Failed to get lock for vamsas archive.");
431 VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true,
438 * Unlocks the vamsas archive session document after it has been closed.
439 * @throws IOException
441 protected void unlockVamsasDocument() throws IOException {
445 if (vamArchive != null)
451 * create a uniquely named uk.ac.vamsas.client.simpleclient.ClientsFile.addClient(ClientHandle)ile in the session Directory
452 * @see java.io.File.createTempFile
453 * @param pref Prefix for name
454 * @param suff Suffix for name
455 * @return SessionFile object configured for the new file (of length zero)
456 * @throws IOException
458 protected SessionFile getTempSessionFile(String pref, String suff)
460 File tfile = File.createTempFile(pref, suff, sessionDir);
461 SessionFile tempFile = new SessionFile(tfile);
466 * add a IClient to the session
468 * add the client to the client list file
469 * @param client client to add to the session
471 protected void addClient(SimpleClient client) {
473 slog.error("Try to add a null client to the session ");
475 log.debug("Adding client " + client.getClientHandle().getClientUrn());
476 getClientWatcherElement().haltWatch();
477 clist.addClient(client.getClientHandle());
480 log.debug("Register Client as Active.");
482 client.createActiveClientFile();
483 } catch (IOException e) {
484 log.debug("Error during active client file creation.");
486 //tracks modification to the client list and readds client to the list
487 getClientWatcherElement().setHandler(new AddClientWatchCallBack(client));
488 getClientWatcherElement().enableWatch();
494 * Handler for the client watcher.
496 * If (the current client is not in the client list, it is added again;)
498 private class AddClientWatchCallBack implements WatcherCallBack {
500 private SimpleClient client;
503 *Inits the handler with the client to check in the list
504 * @param client client to monitor in the client list
506 protected AddClientWatchCallBack(SimpleClient client) {
507 this.client = client;
511 * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
512 * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
514 public boolean handleWatchEvent(WatcherElement watcher, Lock lock) {
515 boolean isWatchEnable = watcher.isWatchEnabled();
516 if (lock == null)//no update on the list
517 return isWatchEnable;
518 log.debug("change on the client list ");
519 if (client != null) {
521 //checks if the client is not already in the lists
522 ClientHandle[] cl = clist.retrieveClientList(lock);//clist.retrieveClientList();
523 boolean found = false;
525 for (int chi = cl.length - 1; !found && chi > -1; chi--) {
526 found = cl[chi].equals(this.client.getClientHandle());
531 log.debug("client not in the list ");
532 if (log.isDebugEnabled())
534 .debug("the client has not been found in the list. Adding it again :"
538 log.debug("client is in the list");
541 log.debug("isWatchEnable " + isWatchEnable);
542 return isWatchEnable;
548 * removes a client from the current session
549 * removes the client from the session client list
550 * if the client is the last one from the session (ClientList), the current session is removed
551 * from active session list.
553 * The active should add them self to the client list. To insure to close the session,when the current client is the lact active client,
554 * clears the list of clients and when two cycles to insure there is no more active client, that otherwise would have readd themself to the list
556 * @param client client to remove
558 protected void removeClient(SimpleClient client)//IClient client)
560 if (client == null) {
561 log.error("Null client passed to removeClient");
564 //ClientSessionFileWatcherElement cwe=getClientWatcherElement();
565 //if (cwe!=null && cwe.isWatchEnabled()) {
568 //set handler to check is the the last active client of the session
569 //Wait for several watchers cycle to see if the current client was the last client active in the session.
570 //if yes, close the session
572 // getClientWatcherElement().setHandler(new RemoveClientWatchCallBack (client));
573 // getClientWatcherElement().setTimeoutBeforeLastCycle(this.watchCycleCountBeforeLastClient);
574 log.info("remove client from list");
575 if (clistWatchElement!=null)
577 clistWatchElement.haltWatch();
578 clistWatchElement.watched.unlockFile();
581 //clist.unlockFile();
582 log.info("list cleared");
584 // cwe.enableWatch();
586 log.debug("Stopping EventGenerator..");
587 client.evgen.stopWatching();
588 // cwe.setHandler(null);
589 // ask to the client to copy application data into the document
590 client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client, null);
591 boolean closeSession = isLastActiveClient(client);
593 log.debug("Raising request-to-save event");
594 client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client, null);
595 client.evgen._raise(Events.SESSION_SHUTDOWN, null, client
596 .getSessionHandle(), null);
599 client.evgen.stopWatching();
601 log.debug("Attempting to release active client locks");
602 client.releaseActiveClientFile();
603 } catch (IOException e) {
604 log.error("error during active file client release");
608 log.debug("Last active client: closing session");
609 log.info("Closing session");
610 getSessionManager().removeSession(client.getSessionHandle());
615 * close every file and stop.
617 private void tidyUp() {
621 storedocfile.unlockFile();
626 private boolean isLastActiveClient(SimpleClient client) {
627 log.debug("Testing if current client is the last one.");
629 .debug("current client lockfile is '" + client.getClientlockFile()
631 boolean noOtherActiveClient = true;
632 //create, if need, subdirectory to contain client files
633 File clientlockFileDir = new File(this.sessionDir, clientFileDirectory);
634 if (!clientlockFileDir.exists()) {
636 .error("Something wrong the active client file does not exits... should not happen");
642 //no check every file in the directory and try to get lock on it.
643 File[] clientFiles = clientlockFileDir.listFiles();
644 if (clientFiles == null || clientFiles.length == 0) {//there is not file on the directory. the current client should be the last one.
648 for (int i = clientFiles.length - 1; i > -1 && noOtherActiveClient; i--) {
649 File clientFile = clientFiles[i];
650 log.debug("testing file for lock: " + clientFile.getAbsolutePath());
651 if (client.getClientLock().isTargetLockFile(clientFile)) {
652 log.debug("current client file found");
655 if (clientFile != null && clientFile.exists()) {
657 log.debug("Try to acquire a lock on the file");
658 // Get a file channel for the file
659 FileChannel channel = new RandomAccessFile(clientFile, "rw")
662 // Use the file channel to create a lock on the file.
663 // This method blocks until it can retrieve the lock.
664 // java.nio.channels.FileLock activeClientFilelock = channel.lock();
666 // Try acquiring the lock without blocking. This method returns
667 // null or throws an exception if the file is already locked.
669 java.nio.channels.FileLock activeClientFilelock = channel
672 //the lock has been acquired.
673 //the file was not lock and so the corresponding application seems to have die
674 if (activeClientFilelock != null) {
676 .debug("lock obtained : file must be from a crashed application");
678 activeClientFilelock.release();
679 log.debug("lock released");
682 log.debug("channel closed");
686 log.debug("crashed application file deleted");
689 noOtherActiveClient = false;
690 log.debug("lock not obtained : another application is active");
692 } catch (OverlappingFileLockException e) {
693 // File is already locked in this thread or virtual machine
694 //that the expected behaviour
695 log.debug("lock not accessible ", e);
697 } catch (Exception e) {
698 log.debug("error during lock testing ", e);
703 } catch (Exception e) {
704 log.error("error during counting active clients");
706 return noOtherActiveClient;
710 * Handler for the client watcher. after a client have been removed
712 * Checks if the client is not the last active one.
714 * If (the current client is not in the client list readd it;)
716 private class RemoveClientWatchCallBack implements WatcherCallBack {
718 private SimpleClient client;
720 private boolean manualCheckOfClientCount = false;
723 *Inits the handler with the client to check in the list
724 * @param client client to monitor in the client list
726 protected RemoveClientWatchCallBack(SimpleClient client) {
727 this.client = client;
731 * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
732 * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
734 public boolean handleWatchEvent(WatcherElement watcher, Lock lock) {
735 // if lock is null, no client has been added since last, clear.
736 //the client is then the last client
737 if (client != null) {
741 //checks if the client is not already in the lists
742 // ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
744 boolean islastClient = true;
745 if (manualCheckOfClientCount) {
746 log.debug("manual checking of count of client");
747 //checks if the client is not already in the lists
748 ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
749 if (cl == null || cl.length < 1)
750 // {//no client has registered as active
753 log.debug("list is empty");
755 islastClient = false;
756 log.debug("list is not empty");
758 // if(cl == null || cl.length<1 )
759 // {//no client has registered as active
761 //the client is the last one, so close current session
762 log.info("last client removed: closing session");
763 closeSession(client);
766 log.debug("not the last client found ");
767 // ask to the client to cpoy application data into the document
768 // client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
773 log.debug("Stopping EventGenerator..");
774 // TODO: ensure ClientsFile lock is really released!! clist.unlockFile();
775 client.evgen.stopWatching();
777 watcher.setHandler(null);//Do not check if the client is the last client. watcher will shutdown anyway
778 // watcher.haltWatch();
785 * closes the current session,
786 * and send an event to the last client to close the document
787 * @param client the last client of the client
789 private void closeSession(SimpleClient client) {
791 client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client, null);
792 log.debug("close document request done");
793 closeSession(client.getSessionHandle());
797 * CLoses the current session
798 * @param sessionHandle sessionHandle of the session to remove
800 private void closeSession(SessionHandle sessionHandle) {
801 getSessionManager().removeSession(sessionHandle);
802 log.debug("Session removed");
806 * @return the sessionManager
808 protected SimpleSessionManager getSessionManager() {
809 return sessionManager;
813 * @param sessionManager the sessionManager to set
815 protected void setSessionManager(SimpleSessionManager sessionManager) {
816 this.sessionManager = sessionManager;
819 public ClientsFile getStoreDocFile() {
820 if (storedocfile == null) {
826 ClientSessionFileWatcherElement clistWatchElement = null;
829 * get or create a watcher on clist.
830 * @return the contents of clistWatchElement or initialise it
832 public ClientSessionFileWatcherElement getClientWatcherElement() {
833 if (clistWatchElement == null) {
834 clistWatchElement = new ClientSessionFileWatcherElement(clist, null);
836 return clistWatchElement;
839 * writes a vector of vorba Ids to the session.
841 public void setModObjectList(Vector modObjects) {
842 log.debug("Writing "+modObjects.size()+" ids to ModObjectList");
843 // TODO Auto-generated method stub
846 * get current list of modified objects.
847 * @return null or Vector of objects
849 public Vector getModObjectList() {
850 log.debug("Reading modObjectList");