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 a
24 * SimpleClient vamsas session.
26 * Basically, it defines the various standard names for the files in the session
27 * directory (that maps to the sessionUrn), provides constructors for the file
28 * handlers and watchers of those file entities, and some higher level methods
29 * to check and change the state flags for the session.
31 * TODO: move the stuff below to the SimpleClientFactory documentation. much may
32 * not be valid now : Vamsas client is intialised with a path to create live
33 * session directories. This path may contain a vamsas.properties file that sets
34 * additional parameters (otherwise client just uses the one on the classpath).
36 * A vamsas session consists of : SessionDir - translates to urn of a live
37 * session. Contains: Vamsas Document (as a jar), Session client list file, both
38 * of which may be locked, and additional temporary versions of these files when
39 * write operations are taking place.
41 * Zip file entries - vamsasdocument.xml : core info one or more: -
42 * <applicationname>.version.sessionnumber.raw (string given in
43 * vamsasdocument.xml applicationData entry)
45 * Lockfile - filename given in the vamsasdocument.xml. Should be checked for
46 * validity by any client and rewritten if necessary. The lockfile can point to
47 * the jar itself. Mode of operation. Initially - documentHandler either: -
48 * creates a zip for a new session for the client - connect to an existing
49 * session zip 1. reads session urn file 2. waits for lock 3. examines session -
50 * decide whether to create new application data slice or connect to one stored
51 * in session. 4. writes info into session file 5. releases lock and generates
52 * local client events. 6. Creates Watcher thread to generate events.
54 * During the session - Update watcher checks for file change -
56 * Procedures for file based session message exchange - session document
61 public class VamsasSession {
63 * indicator file for informing other processes that they should finalise
64 * their vamsas datasets for storing into a vamsas archive.
66 public static final String CLOSEANDSAVE_FILE = "stored.log";
69 * session file storing the last_stored_stat data
71 public static final String MODIFIEDDOC_FILE = "modified";
73 private SimpleSessionManager sessionManager = null;
76 * Count of cycles before considering the current client as the last one of
77 * the session (if no other client registered as active )
79 private final int watchCycleCountBeforeLastClient = 1220;
82 * time between checking
84 public int WATCH_SLEEP = 30;
86 protected String clientFileDirectory = "clients";
89 * called to clear update flag after a successful offline storage event
91 protected void clearUnsavedFlag() {
92 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir,
94 if (!laststored.clearFlag())
95 log.warn("Unsaved flag was not cleared for " + sessionDir);
99 * called to indicate session document has been modified.
102 protected void setUnsavedFlag() {
103 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir,
105 if (!laststored.setFlag())
106 log.warn("Couldn't set the Unsaved flag for " + sessionDir);
111 * @return true if session document has been modified since last offline
114 protected boolean getUnsavedFlag() {
115 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir,
117 return laststored.checkFlag();
123 public static final String SESSION_LOG = "Log.txt";
125 private static Log log = LogFactory.getLog(VamsasSession.class);
127 protected Logger slog = Logger.getLogger("uk.ac.vamsas.client.SessionLog");
130 * the appender that writes to the log file inside the session's directory.
132 private FileAppender slogAppender = null;
135 * setup the sessionLog using Log4j.
137 * @throws IOException
139 private void initLog() throws IOException {
140 // TODO: fix session event logging
141 // LATER: make dedicated appender format for session log.
143 * Appender app = slog.getAppender("log4j.appender.SESSIONLOG"); //
144 * slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir,
145 * SESSION_LOG).getAbsolutePath())); // slog.addAppender(new
146 * FileAppender(app.getLayout(), new File(sessionDir,
147 * SESSION_LOG).getAbsolutePath())); for (Enumeration e =
148 * slog.getAllAppenders() ; e.hasMoreElements() ;) {
149 * System.out.println(e.nextElement());
154 File sessionLogFile = new File(this.sessionDir, SESSION_LOG);
155 slog.addAppender(slogAppender = new FileAppender(new PatternLayout(
156 "%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(),
159 log.info("No appender for SessionLog");
163 private void closeSessionLog() {
165 if (slogAppender != null) {
166 slog.removeAppender(slogAppender);
167 slogAppender.close();
174 * the sessionDir is given as the session location for new clients.
176 protected File sessionDir;
179 * holds the list of attached clients
183 public static final String CLIENT_LIST = "Clients.obj";
188 VamsasFile vamArchive;
190 public static final String VAMSAS_OBJ = "VamDoc.jar";
193 * sets up the vamsas session files and watchers in sessionDir1
197 protected VamsasSession(File sessionDir1) throws IOException {
198 this(sessionDir1, null);
202 * sets up the vamsas session files and watchers in sessionDir1
206 * null or an existing archive to initialise the session with
208 * IOExceptions from creating session directory and files.
210 * if both extVamDoc and sessionDir1 already exist (cannot import
211 * new data into session in this way)
213 protected VamsasSession(File sessionDir1, File extVamDoc) throws IOException {
214 if (sessionDir1 == null)
215 throw new Error("Null directory for VamsasSession.");
216 if (!sessionDir1.exists() && !sessionDir1.mkdir()) {
217 throw new IOException("Failed to make VamsasSession directory in "
220 if (!sessionDir1.isDirectory() || !sessionDir1.canWrite()
221 || !sessionDir1.canRead()) {
222 throw new IOException("Cannot access '" + sessionDir1
223 + "' as a read/writable Directory.");
225 boolean existingSession=checkSessionFiles(sessionDir1);
228 if (extVamDoc!=null) {
230 "Client Initialisation Error: Cannot join an existing session directory with an existing vamsas document to import.");
233 .debug("Joining an existing session.");
236 this.sessionDir = sessionDir1;
237 initSessionObjects();
239 { slog.debug("Initialising additional VamsasSession instance");
242 slog.debug("Founding client has joined VamsasSession instance");
245 log.debug("Attached to VamsasSession in " + sessionDir1);
248 setVamsasDocument(extVamDoc);
250 slog.debug("Session directory created.");
251 log.debug("Initialised VamsasSession in " + sessionDir1);
255 * tests presence of existing sessionfiles files in dir
260 private boolean checkSessionFiles(File dir) throws IOException {
261 File c_file = new File(dir, CLIENT_LIST);
262 File v_doc = new File(dir, VAMSAS_OBJ);
263 if (c_file.exists() && v_doc.exists())
269 * create new empty files in dir
272 private void createSessionFiles() throws IOException {
273 if (sessionDir == null)
274 throw new IOException(
275 "Invalid call to createSessionFiles() with null sessionDir");
276 File c_file = new File(sessionDir, CLIENT_LIST);
277 File v_doc = new File(sessionDir, VAMSAS_OBJ);
278 if (!c_file.exists() && c_file.createNewFile())
279 log.debug("Created new ClientFile " + c_file); // don't care if this
281 if (!v_doc.exists()) {
282 if (v_doc.createNewFile()) {
283 log.debug("Created new Vamsas Session Document File " + v_doc);
285 log.warn("Didn't create Vamsas Session Document file in " + v_doc);
291 * construct SessionFile objects and watchers for each
293 private void initSessionObjects() throws IOException {
294 createSessionFiles();
295 if (clist != null || vamArchive != null)
296 throw new IOException(
297 "initSessionObjects called for initialised VamsasSession object.");
298 clist = new ClientsFile(new File(sessionDir, CLIENT_LIST));
299 vamArchive = new VamsasFile(new File(sessionDir, VAMSAS_OBJ));
300 storedocfile = new ClientsFile(new File(sessionDir, CLOSEANDSAVE_FILE));
305 * make a new watcher object for the clientFile
307 * @return new ClientFile watcher instance
309 public FileWatcher getClientWatcher() {
310 return new FileWatcher(clist.sessionFile);
314 * make a new watcher object for the vamsas Document
316 * @return new ClientFile watcher instance
318 public FileWatcher getDocWatcher() {
319 return new FileWatcher(vamArchive.sessionFile);
322 FileWatcher store_doc_file = null;
324 public ClientsFile storedocfile = null;
327 * make a new watcher object for the messages file
329 * @return new watcher instance
331 public FileWatcher getStoreWatcher() {
332 return new FileWatcher(new File(sessionDir, CLOSEANDSAVE_FILE));
337 * write to the StoreWatcher file to indicate that a storeDocumentRequest has
338 * been made. The local client's storeWatcher FileWatcher object is updated so
339 * the initial change is not registered.
345 public void addStoreDocumentRequest(ClientHandle client, UserHandle user)
347 // TODO: replace this with clientsFile mechanism
348 SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE));
349 while (!sfw.lockFile())
350 log.debug("Trying to get lock for " + CLOSEANDSAVE_FILE);
351 RandomAccessFile sfwfile = sfw.fileLock.getRaFile();
352 sfwfile.setLength(0); // wipe out any old info.
353 // TODO: rationalise what gets written to this file (ie do we want other
354 // clients to read the id of the requestor?)
355 sfwfile.writeUTF(client.getClientUrn() + ":" + user.getFullName() + "@"
356 + user.getOrganization());
358 if (store_doc_file != null)
359 store_doc_file.setState();
360 slog.info("FinalizeAppData request from " + user.getFullName() + " using "
361 + client.getClientUrn() + "");
365 * create a new session with an existing vamsas Document - by copying it into
370 public void setVamsasDocument(File archive) throws IOException {
371 log.debug("Transferring vamsas data from " + archive + " to session:"
372 + vamArchive.sessionFile);
373 SessionFile xtantdoc = new SessionFile(archive);
374 while (!vamArchive.lockFile())
375 log.info("Trying to get lock for " + vamArchive.sessionFile);
376 vamArchive.updateFrom(null, xtantdoc);
377 xtantdoc.unlockFile();
378 unlockVamsasDocument();
379 // TODO: session archive provenance should be updated to reflect import from external source
380 log.debug("Transfer complete.");
384 * write session as a new vamsas Document (this will overwrite any existing
385 * file without warning) TODO: test TODO: verify that lock should be released
386 * for vamsas document.
390 protected void writeVamsasDocument(File destarchive, Lock extlock)
392 log.debug("Transferring vamsas data from " + vamArchive.sessionFile
393 + " to session:" + destarchive);
394 SessionFile newdoc = new SessionFile(destarchive);
395 if (extlock == null && !vamArchive.lockFile())
396 while (!vamArchive.lockFile())
397 log.info("Trying to get lock for " + vamArchive.sessionFile);
398 // TODO: LATER: decide if a provenance entry should be written in the
399 // 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
402 // be made (because they don't have a lockManager, ie NFS/Unix, etc).
405 log.debug("Transfer complete.");
409 * extant archive IO handler
411 VamsasArchive _va = null;
414 * Creates a VamsasArchive Vobject for accessing and updating document Note:
415 * this will lock the Vamsas Document for exclusive access to the client.
417 * @return session vamsas document
418 * @throws IOException
419 * if locks fail or vamsas document read fails.
421 protected VamsasArchive getVamsasDocument() throws IOException {
422 // check we haven't already done this once - probably should be done by
426 // patiently wait for a lock on the document. (from
427 // ArchiveClient.getUpdateable())
429 while (vamArchive.getLock() == null && --tries > 0) {
431 log.debug("Trying to get a document lock for the " + tries + "'th time.");
434 throw new IOException("Failed to get lock for vamsas archive.");
436 VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true,
443 * Unlocks the vamsas archive session document after it has been closed.
445 * @throws IOException
447 protected void unlockVamsasDocument() throws IOException {
451 if (vamArchive != null)
457 * create a uniquely named
458 * uk.ac.vamsas.client.simpleclient.ClientsFile.addClient(ClientHandle)ile in
459 * the session Directory
461 * @see java.io.File.createTempFile
466 * @return SessionFile object configured for the new file (of length zero)
467 * @throws IOException
469 protected SessionFile getTempSessionFile(String pref, String suff)
471 File tfile = File.createTempFile(pref, suff, sessionDir);
472 SessionFile tempFile = new SessionFile(tfile);
477 * add a IClient to the session
479 * add the client to the client list file
482 * client to add to the session
484 protected void addClient(SimpleClient client) {
486 slog.error("Try to add a null client to the session ");
488 log.debug("Adding client " + client.getClientHandle().getClientUrn());
489 getClientWatcherElement().haltWatch();
490 clist.addClient(client.getClientHandle());
493 log.debug("Register Client as Active.");
495 client.createActiveClientFile();
496 } catch (IOException e) {
497 log.debug("Error during active client file creation.");
499 // tracks modification to the client list and readds client to the list
500 getClientWatcherElement().setHandler(new AddClientWatchCallBack(client));
501 getClientWatcherElement().enableWatch();
507 * Handler for the client watcher.
509 * If (the current client is not in the client list, it is added again;)
511 private class AddClientWatchCallBack implements WatcherCallBack {
513 private SimpleClient client;
516 * Inits the handler with the client to check in the list
519 * client to monitor in the client list
521 protected AddClientWatchCallBack(SimpleClient client) {
522 this.client = client;
526 * If the client list is modified, checks if the current is still in the
527 * list. otherwise, readds ti.
529 * @return true to enable watcher, or false to disable it in future
530 * WatcherThread cycles.
532 public boolean handleWatchEvent(WatcherElement watcher, Lock lock) {
533 boolean isWatchEnable = watcher.isWatchEnabled();
534 if (lock == null)// no update on the list
535 return isWatchEnable;
536 log.debug("change on the client list ");
537 if (client != null) {
539 // checks if the client is not already in the lists
540 ClientHandle[] cl = clist.retrieveClientList(lock);// clist.retrieveClientList();
541 boolean found = false;
543 for (int chi = cl.length - 1; !found && chi > -1; chi--) {
544 found = cl[chi].equals(this.client.getClientHandle());
549 log.debug("client not in the list ");
550 if (log.isDebugEnabled())
552 .debug("the client has not been found in the list. Adding it again :"
556 log.debug("client is in the list");
559 log.debug("isWatchEnable " + isWatchEnable);
560 return isWatchEnable;
566 * removes a client from the current session removes the client from the
567 * session client list if the client is the last one from the session
568 * (ClientList), the current session is removed from active session list.
570 * The active should add them self to the client list. To insure to close the
571 * session,when the current client is the lact active client, clears the list
572 * of clients and when two cycles to insure there is no more active client,
573 * that otherwise would have readd themself to the list
578 protected void removeClient(SimpleClient client)// IClient client)
580 if (client == null) {
581 log.error("Null client passed to removeClient");
584 // ClientSessionFileWatcherElement cwe=getClientWatcherElement();
585 // if (cwe!=null && cwe.isWatchEnabled()) {
588 // set handler to check is the the last active client of the session
589 // Wait for several watchers cycle to see if the current client was the last
590 // client active in the session.
591 // if yes, close the session
593 // getClientWatcherElement().setHandler(new RemoveClientWatchCallBack
595 // getClientWatcherElement().setTimeoutBeforeLastCycle(this.watchCycleCountBeforeLastClient);
596 log.info("remove client from list");
597 if (clistWatchElement != null) {
598 clistWatchElement.haltWatch();
599 clistWatchElement.watched.unlockFile();
601 // clist.clearList();
602 // clist.unlockFile();
603 log.info("list cleared");
605 // cwe.enableWatch();
607 log.debug("Stopping EventGenerator..");
608 client.evgen.stopWatching();
609 // cwe.setHandler(null);
610 // ask to the client to copy application data into the document
611 client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client, null);
612 boolean closeSession = isLastActiveClient(client);
614 log.debug("Raising request-to-save event");
615 client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client, null);
616 client.evgen._raise(Events.SESSION_SHUTDOWN, null, client
617 .getSessionHandle(), null);
620 client.evgen.stopWatching();
622 log.debug("Attempting to release active client locks");
623 client.releaseActiveClientFile();
624 } catch (IOException e) {
625 log.error("error during active file client release");
629 log.debug("Last active client: closing session");
630 log.info("Closing session");
631 getSessionManager().removeSession(client.getSessionHandle());
636 * close every file and stop.
638 private void tidyUp() {
642 storedocfile.unlockFile();
647 private boolean isLastActiveClient(SimpleClient client) {
648 log.debug("Testing if current client is the last one.");
650 .debug("current client lockfile is '" + client.getClientlockFile()
652 boolean noOtherActiveClient = true;
653 // create, if need, subdirectory to contain client files
654 File clientlockFileDir = new File(this.sessionDir, clientFileDirectory);
655 if (!clientlockFileDir.exists()) {
657 .error("Something wrong the active client file does not exits... should not happen");
663 // no check every file in the directory and try to get lock on it.
664 File[] clientFiles = clientlockFileDir.listFiles();
665 if (clientFiles == null || clientFiles.length == 0) {// there is not file
673 for (int i = clientFiles.length - 1; i > -1 && noOtherActiveClient; i--) {
674 File clientFile = clientFiles[i];
675 log.debug("testing file for lock: " + clientFile.getAbsolutePath());
676 if (client.getClientLock().isTargetLockFile(clientFile)) {
677 log.debug("current client file found");
680 if (clientFile != null && clientFile.exists()) {
682 log.debug("Try to acquire a lock on the file");
683 // Get a file channel for the file
684 FileChannel channel = new RandomAccessFile(clientFile, "rw")
687 // Use the file channel to create a lock on the file.
688 // This method blocks until it can retrieve the lock.
689 // java.nio.channels.FileLock activeClientFilelock = channel.lock();
691 // Try acquiring the lock without blocking. This method returns
692 // null or throws an exception if the file is already locked.
694 java.nio.channels.FileLock activeClientFilelock = channel
697 // the lock has been acquired.
698 // the file was not lock and so the corresponding application
700 if (activeClientFilelock != null) {
702 .debug("lock obtained : file must be from a crashed application");
704 activeClientFilelock.release();
705 log.debug("lock released");
708 log.debug("channel closed");
712 log.debug("crashed application file deleted");
715 noOtherActiveClient = false;
716 log.debug("lock not obtained : another application is active");
718 } catch (OverlappingFileLockException e) {
719 // File is already locked in this thread or virtual machine
720 // that the expected behaviour
721 log.debug("lock not accessible ", e);
723 } catch (Exception e) {
724 log.debug("error during lock testing ", e);
729 } catch (Exception e) {
730 log.error("error during counting active clients");
732 return noOtherActiveClient;
736 * Handler for the client watcher. after a client have been removed
738 * Checks if the client is not the last active one.
740 * If (the current client is not in the client list readd it;)
742 private class RemoveClientWatchCallBack implements WatcherCallBack {
744 private SimpleClient client;
746 private boolean manualCheckOfClientCount = false;
749 * Inits the handler with the client to check in the list
752 * client to monitor in the client list
754 protected RemoveClientWatchCallBack(SimpleClient client) {
755 this.client = client;
759 * If the client list is modified, checks if the current is still in the
760 * list. otherwise, readds ti.
762 * @return true to enable watcher, or false to disable it in future
763 * WatcherThread cycles.
765 public boolean handleWatchEvent(WatcherElement watcher, Lock lock) {
766 // if lock is null, no client has been added since last, clear.
767 // the client is then the last client
768 if (client != null) {
772 // checks if the client is not already in the lists
773 // ClientHandle[] cl =
774 // clist.retrieveClientList();//lock);//clist.retrieveClientList();
776 boolean islastClient = true;
777 if (manualCheckOfClientCount) {
778 log.debug("manual checking of count of client");
779 // checks if the client is not already in the lists
780 ClientHandle[] cl = clist.retrieveClientList();// lock);//clist.retrieveClientList();
781 if (cl == null || cl.length < 1)
782 // {//no client has registered as active
785 log.debug("list is empty");
787 islastClient = false;
788 log.debug("list is not empty");
790 // if(cl == null || cl.length<1 )
791 // {//no client has registered as active
793 // the client is the last one, so close current session
794 log.info("last client removed: closing session");
795 closeSession(client);
798 log.debug("not the last client found ");
799 // ask to the client to cpoy application data into the document
800 // client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null,
806 log.debug("Stopping EventGenerator..");
807 // TODO: ensure ClientsFile lock is really released!!
808 // clist.unlockFile();
809 client.evgen.stopWatching();
811 watcher.setHandler(null);// Do not check if the client is the last
812 // client. watcher will shutdown anyway
813 // watcher.haltWatch();
820 * closes the current session, and send an event to the last client to close
824 * the last client of the client
826 private void closeSession(SimpleClient client) {
828 client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client, null);
829 log.debug("close document request done");
830 closeSession(client.getSessionHandle());
834 * CLoses the current session
836 * @param sessionHandle
837 * sessionHandle of the session to remove
839 private void closeSession(SessionHandle sessionHandle) {
840 getSessionManager().removeSession(sessionHandle);
841 log.debug("Session removed");
845 * @return the sessionManager
847 protected SimpleSessionManager getSessionManager() {
848 return sessionManager;
852 * @param sessionManager
853 * the sessionManager to set
855 protected void setSessionManager(SimpleSessionManager sessionManager) {
856 this.sessionManager = sessionManager;
859 public ClientsFile getStoreDocFile() {
860 if (storedocfile == null) {
866 ClientSessionFileWatcherElement clistWatchElement = null;
869 * get or create a watcher on clist.
871 * @return the contents of clistWatchElement or initialise it
873 public ClientSessionFileWatcherElement getClientWatcherElement() {
874 if (clistWatchElement == null) {
875 clistWatchElement = new ClientSessionFileWatcherElement(clist, null);
877 return clistWatchElement;