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;
22 * Does all the IO operations for a SimpleClient instance accessing
23 * a SimpleClient vamsas session.
25 * Basically, it defines the various standard names for the files
26 * in the session directory (that maps to the sessionUrn),
27 * provides constructors for the file handlers and watchers of
28 * 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.
32 * much may not be valid now :
33 * Vamsas client is intialised with a path to create live session directories.
34 * This path may contain a vamsas.properties file
35 * that sets additional parameters (otherwise client
36 * just uses the one on the classpath).
38 * A vamsas session consists of :
39 * SessionDir - translates to urn of a live session.
40 * Contains: Vamsas Document (as a jar), Session client list file,
41 * both of which may be locked, and additional
42 * temporary versions of these files when write
43 * operations are taking place.
46 * - vamsasdocument.xml : core info
48 * - <applicationname>.version.sessionnumber.raw (string given in vamsasdocument.xml applicationData entry)
51 * - filename given in the vamsasdocument.xml. Should be checked for validity by any client and rewritten if necessary.
52 * The lockfile can point to the jar itself.
54 * Initially - documentHandler either:
55 * - creates a zip for a new session for the client
56 * - connect to an existing session zip
57 * 1. reads session urn file
59 * 3. examines session - decide whether to create new application data slice or connect to one stored in session.
60 * 4. writes info into session file
61 * 5. releases lock and generates local client events.
62 * 6. Creates Watcher thread to generate events.
65 * - Update watcher checks for file change -
67 * Procedures for file based session message exchange
68 * - session document modification flag
72 public class VamsasSession {
74 * indicator file for informing other processes that
75 * they should finalise their vamsas datasets for
76 * storing into a vamsas archive.
78 public static final String CLOSEANDSAVE_FILE="stored.log";
80 * session file storing the last_stored_stat data
82 public static final String MODIFIEDDOC_FILE="modified";
85 private SimpleSessionManager sessionManager = null;
88 * Count of cycles before considering the current client as the last one of the session (if no other client registered as active )
90 private final int watchCycleCountBeforeLastClient = 1220 ;
93 * time between checking
95 public int WATCH_SLEEP=30;
97 protected String clientFileDirectory = "clients";
100 * called to clear update flag after a successful offline storage event
102 protected void clearUnsavedFlag() {
103 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
104 if (!laststored.clearFlag())
105 log.warn("Unsaved flag was not cleared for "+sessionDir);
108 * called to indicate session document has been modified.
111 protected void setUnsavedFlag() {
112 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
113 if (!laststored.setFlag())
114 log.warn("Couldn't set the Unsaved flag for "+sessionDir);
118 * @return true if session document has been modified since last offline storage event
120 protected boolean getUnsavedFlag() {
121 SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
122 return laststored.checkFlag();
127 public static final String SESSION_LOG="Log.txt";
128 private static Log log = LogFactory.getLog(VamsasSession.class);
129 protected Logger slog = Logger.getLogger("uk.ac.vamsas.client.SessionLog");
131 * setup the sessionLog using Log4j.
132 * @throws IOException
134 private void initLog() throws IOException {
135 // TODO: fix session event logging
136 // LATER: make dedicated appender format for session log.
137 /*Appender app = slog.getAppender("log4j.appender.SESSIONLOG");
138 // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
139 // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
140 for (Enumeration e = slog.getAllAppenders() ; e.hasMoreElements() ;) {
141 System.out.println(e.nextElement());
146 File sessionLogFile = new File(this.sessionDir, SESSION_LOG);
147 slog.addAppender(new FileAppender(new PatternLayout("%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(), true));
149 log.info("No appender for SessionLog");
154 * the sessionDir is given as the session location for new clients.
156 protected File sessionDir;
158 * holds the list of attached clients
161 public static final String CLIENT_LIST="Clients.obj";
165 VamsasFile vamArchive;
166 public static final String VAMSAS_OBJ="VamDoc.jar";
169 * sets up the vamsas session files and watchers in sessionDir
172 protected VamsasSession(File sessionDir1) throws IOException {
173 if (sessionDir1==null)
174 throw new Error("Null directory for VamsasSession.");
175 if (sessionDir1.exists()) {
176 if (!sessionDir1.isDirectory() || !sessionDir1.canWrite() || !sessionDir1.canRead())
177 throw new IOException("Cannot access '"+sessionDir1+"' as a read/writable Directory.");
178 if (!checkSessionFiles(sessionDir1))
179 log.warn("checkSessionFiles() returned false. Possible client implementation error");
180 this.sessionDir = sessionDir1;
181 initSessionObjects();
182 slog.debug("Initialising additional VamsasSession instance");
183 log.debug("Attached to VamsasSession in "+sessionDir1);
186 // start from scratch
187 if (!sessionDir1.mkdir())
188 throw new IOException("Failed to make VamsasSession directory in "+sessionDir1);
189 createSessionFiles();
190 initSessionObjects();
191 slog.debug("Session directory created.");
192 log.debug("Initialised VamsasSession in "+sessionDir1);
196 * tests presence of existing sessionfiles files in dir
200 private boolean checkSessionFiles(File dir) throws IOException {
201 File c_file = new File(dir,CLIENT_LIST);
202 File v_doc = new File(dir,VAMSAS_OBJ);
203 if (c_file.exists() && v_doc.exists())
208 * create new empty files in dir
211 private void createSessionFiles() throws IOException {
212 if (sessionDir==null)
213 throw new IOException("Invalid call to createSessionFiles() with null sessionDir");
214 File c_file = new File(sessionDir,CLIENT_LIST);
215 File v_doc = new File(sessionDir,VAMSAS_OBJ);
216 if (!c_file.exists() && c_file.createNewFile())
217 log.debug("Created new ClientFile "+c_file); // don't care if this works or not
218 if (!v_doc.exists() && v_doc.createNewFile())
219 log.debug("Created new Vamsas Session Document File "+v_doc);
222 * construct SessionFile objects and watchers for each
224 private void initSessionObjects() throws IOException {
225 createSessionFiles();
226 if (clist!=null || vamArchive!=null)
227 throw new IOException("initSessionObjects called for initialised VamsasSession object.");
228 clist = new ClientsFile(new File(sessionDir,CLIENT_LIST));
229 vamArchive = new VamsasFile(new File(sessionDir,VAMSAS_OBJ));
230 storedocfile=new ClientsFile(new File(sessionDir, CLOSEANDSAVE_FILE));
234 * make a new watcher object for the clientFile
235 * @return new ClientFile watcher instance
237 public FileWatcher getClientWatcher() {
238 return new FileWatcher(clist.sessionFile);
241 * make a new watcher object for the vamsas Document
242 * @return new ClientFile watcher instance
244 public FileWatcher getDocWatcher() {
245 return new FileWatcher(vamArchive.sessionFile);
247 FileWatcher store_doc_file=null;
248 public ClientsFile storedocfile=null;
250 * make a new watcher object for the messages file
251 * @return new watcher instance
253 public FileWatcher getStoreWatcher() {
254 return new FileWatcher(new File(sessionDir,CLOSEANDSAVE_FILE));
258 * write to the StoreWatcher file to indicate that a storeDocumentRequest has been made.
259 * The local client's storeWatcher FileWatcher object is updated so the initial change is not registered.
264 public void addStoreDocumentRequest(ClientHandle client, UserHandle user) throws IOException {
265 // TODO: replace this with clientsFile mechanism
266 SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE));
267 while (!sfw.lockFile())
268 log.debug("Trying to get lock for "+CLOSEANDSAVE_FILE);
269 RandomAccessFile sfwfile=sfw.fileLock.getRaFile();
270 sfwfile.setLength(0); // wipe out any old info.
271 // TODO: rationalise what gets written to this file (ie do we want other clients to read the id of the requestor?)
272 sfwfile.writeUTF(client.getClientUrn()+":"+user.getFullName()+"@"+user.getOrganization());
274 if (store_doc_file!=null)
275 store_doc_file.setState();
276 slog.info("FinalizeAppData request from "+user.getFullName()+" using "+client.getClientUrn()+"");
279 * create a new session with an existing vamsas Document - by copying it into the session.
282 public void setVamsasDocument(File archive) throws IOException {
283 log.debug("Transferring vamsas data from "+archive+" to session:"+vamArchive.sessionFile);
284 SessionFile xtantdoc = new SessionFile(archive);
285 vamArchive.updateFrom(null, xtantdoc);
286 // LATER: decide if session archive provenance should be updated to reflect access.
287 // TODO: soon! do a proper import objects from external file
288 log.debug("Transfer complete.");
291 * write session as a new vamsas Document (this will overwrite any existing file without warning)
293 * TODO: verify that lock should be released for vamsas document.
296 protected void writeVamsasDocument(File destarchive, Lock extlock) throws IOException {
297 log.debug("Transferring vamsas data from "+vamArchive.sessionFile+" to session:"+destarchive);
298 SessionFile newdoc = new SessionFile(destarchive);
299 if (extlock==null && !vamArchive.lockFile())
300 while (!vamArchive.lockFile())
301 log.info("Trying to get lock for "+vamArchive.sessionFile);
302 // TODO: LATER: decide if a provenance entry should be written in the exported document recording the export from the session
303 newdoc.updateFrom(extlock, vamArchive);
304 // 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).
307 log.debug("Transfer complete.");
310 * extant archive IO handler
312 VamsasArchive _va=null;
314 * Creates a VamsasArchive Vobject for accessing and updating document
315 * Note: this will lock the Vamsas Document for exclusive access to the client.
316 * @return session vamsas document
317 * @throws IOException if locks fail or vamsas document read fails.
319 protected VamsasArchive getVamsasDocument() throws IOException {
320 // check we haven't already done this once - probably should be done by caller
323 // patiently wait for a lock on the document. (from ArchiveClient.getUpdateable())
325 while (vamArchive.getLock()==null && --tries>0) {
327 log.debug("Trying to get a document lock for the "+tries+"'th time.");
330 throw new IOException("Failed to get lock for vamsas archive.");
332 VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true, vamArchive);
337 * Unlocks the vamsas archive session document after it has been closed.
338 * @throws IOException
340 protected void unlockVamsasDocument() throws IOException {
344 if (vamArchive!=null)
349 * create a uniquely named uk.ac.vamsas.client.simpleclient.ClientsFile.addClient(ClientHandle)ile in the session Directory
350 * @see java.io.File.createTempFile
351 * @param pref Prefix for name
352 * @param suff Suffix for name
353 * @return SessionFile object configured for the new file (of length zero)
354 * @throws IOException
356 protected SessionFile getTempSessionFile(String pref, String suff) throws IOException {
357 File tfile = File.createTempFile(pref,suff,sessionDir);
358 SessionFile tempFile = new SessionFile(tfile);
363 * add a IClient to the session
365 * add the client to the client list file
366 * @param client client to add to the session
368 protected void addClient(SimpleClient client)
371 slog.error("Try to add a null client to the session ");
373 log.debug("Adding client "+client.getClientHandle().getClientUrn());
374 getClientWatcherElement().haltWatch();
375 clist.addClient(client.getClientHandle());
378 log.debug("Register Client as Active.");
380 client.createActiveClientFile();
381 } catch (IOException e) {
382 log.debug("Error during active client file creation.");
384 //tracks modification to the client list and readds client to the list
385 getClientWatcherElement().setHandler(new AddClientWatchCallBack(client));
386 getClientWatcherElement().enableWatch();
392 * Handler for the client watcher.
394 * If (the current client is not in the client list, it is added again;)
396 private class AddClientWatchCallBack implements WatcherCallBack
399 private SimpleClient client ;
402 *Inits the handler with the client to check in the list
403 * @param client client to monitor in the client list
405 protected AddClientWatchCallBack (SimpleClient client)
407 this.client = client;
411 * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
412 * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
414 public boolean handleWatchEvent(WatcherElement watcher, Lock lock)
416 boolean isWatchEnable = watcher.isWatchEnabled();
417 if (lock== null)//no update on the list
418 return isWatchEnable;
419 log.debug("change on the client list ");
424 //checks if the client is not already in the lists
425 ClientHandle[] cl = clist.retrieveClientList(lock);//clist.retrieveClientList();
426 boolean found = false;
429 for (int chi = cl.length-1; !found && chi > -1; chi--) {
430 found = cl[chi].equals(this.client.getClientHandle());
435 {log.debug("client not in the list ");
436 if( log.isDebugEnabled())
437 log.debug("the client has not been found in the list. Adding it again :"+cl);
441 log.debug("client is in the list");
444 log.debug("isWatchEnable "+isWatchEnable);
445 return isWatchEnable;
451 * removes a client from the current session
452 * removes the client from the session client list
453 * if the client is the last one from the session (ClientList), the current session is removed
454 * from active session list.
456 * 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,
457 * 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
459 * @param client client to remove
461 protected void removeClient(SimpleClient client)//IClient client)
465 log.error("Null client passed to removeClient");
468 ClientSessionFileWatcherElement cwe=getClientWatcherElement();
469 if (cwe!=null && cwe.isWatchEnabled()) {
472 //set handler to check is the the last active client of the session
473 //Wait for several watchers cycle to see if the current client was the last client active in the session.
474 //if yes, close the session
476 // getClientWatcherElement().setHandler(new RemoveClientWatchCallBack (client));
477 getClientWatcherElement().setTimeoutBeforeLastCycle(this.watchCycleCountBeforeLastClient);
478 log.info("remove client from list");
480 log.info("list cleared");
485 log.debug("Stopping EventGenerator..");
486 client.evgen.stopWatching();
487 cwe.setHandler(null);
489 // ask to the client to copy application data into the document
490 client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
492 if ( this.isLastActiveClient(client))
494 log.debug("Last active client: closing session");
495 log.info("Closing session");
496 getSessionManager().removeSession(client.getSessionHandle());
501 log.debug("Releasing active client file");
502 client.releaseActiveClientFile();
504 catch (IOException e)
506 log.error("error during active file client release");
512 /*clist.removeClient(client.getClientHandle(),null);
513 if (this.clist.retrieveClientList() == null|| this.clist.retrieveClientList().length<1)
514 {//assume it is the last client has been removed shutting down session
515 slog.info("last client removed: removing session");
516 log.debug("last client removed: removing session");
517 this.getSessionManager().removeSession(client.getSessionHandle());
521 int active=clist.retrieveClientList().length;
522 log.debug("Still "+active+" active clients");
523 slog.info("Still "+active+" active clients");
529 private boolean isLastActiveClient(SimpleClient client)
531 log.debug("Testing if current client is the last one.");
532 boolean noOtherActiveClient = true;
533 //create, if need, subdirectory to contain client files
534 File clientlockFileDir = new File (this.sessionDir, clientFileDirectory);
535 if( !clientlockFileDir.exists())
537 log.error("Something wrong the active client file does not exits... should not happen");
543 //no check every file in the directory and try to get lock on it.
544 File [] clientFiles = clientlockFileDir.listFiles();
545 if(clientFiles == null || clientFiles.length==0)
546 {//there is not file on the directory. the current client should be the last one.
550 for (int i = clientFiles.length - 1; i>-1&& noOtherActiveClient ;i--)
552 File clientFile = clientFiles[i];
553 log.debug("testing file for lock: "+clientFile.getAbsolutePath());
554 if(client.getClientlockFile().equals(clientFile))
556 log.debug("current client file found");
559 if (clientFile != null && clientFile.exists() )
563 log.debug("Try to acquire a lock on the file");
564 // Get a file channel for the file
565 FileChannel channel = new RandomAccessFile(clientFile, "rw").getChannel();
567 // Use the file channel to create a lock on the file.
568 // This method blocks until it can retrieve the lock.
569 // java.nio.channels.FileLock activeClientFilelock = channel.lock();
571 // Try acquiring the lock without blocking. This method returns
572 // null or throws an exception if the file is already locked.
575 java.nio.channels.FileLock activeClientFilelock = channel.tryLock();
577 //the lock has been acquired.
578 //the file was not lock and so the corresponding application seems to have die
579 if(activeClientFilelock != null)
581 log.debug("lock obtained : file must be from a crashed application");
584 activeClientFilelock.release();
585 log.debug("lock released");
588 log.debug("channel closed");
592 log.debug("crashed application file deleted");
597 noOtherActiveClient = false;
598 log.debug("lock not obtained : another application is active");
601 catch (OverlappingFileLockException e)
603 // File is already locked in this thread or virtual machine
604 //that the expected behaviour
605 log.debug("lock not accessible ",e);
610 log.debug("error during lock testing ",e);
615 } catch (Exception e) {
616 log.error("error during counting active clients");
618 return noOtherActiveClient;
621 * Handler for the client watcher. after a client have been removed
623 * Checks if the client is not the last active one.
625 * If (the current client is not in the client list readd it;)
627 private class RemoveClientWatchCallBack implements WatcherCallBack
630 private SimpleClient client ;
631 private boolean manualCheckOfClientCount = false;
633 *Inits the handler with the client to check in the list
634 * @param client client to monitor in the client list
636 protected RemoveClientWatchCallBack (SimpleClient client)
638 this.client = client;
642 * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
643 * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
645 public boolean handleWatchEvent(WatcherElement watcher, Lock lock)
647 // if lock is null, no client has been added since last, clear.
648 //the client is then the last client
655 //checks if the client is not already in the lists
656 // ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
658 boolean islastClient = true;
659 if (manualCheckOfClientCount)
661 log.debug("manual checking of count of client");
662 //checks if the client is not already in the lists
663 ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
664 if(cl == null || cl.length<1 )
665 // {//no client has registered as active
668 log.debug("list is empty");
671 islastClient = false;
672 log.debug("list is not empty");
674 // if(cl == null || cl.length<1 )
675 // {//no client has registered as active
678 //the client is the last one, so close current session
679 log.info("last client removed: closing session");
680 closeSession(client);
685 log.debug("not the last client found ");
686 // ask to the client to cpoy application data into the document
687 // client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
692 log.debug("Stopping EventGenerator..");
693 client.evgen.stopWatching();
695 watcher.setHandler(null);//Do not check if the client is the last client. watcher will shutdown anyway
696 // watcher.haltWatch();
703 * closes the current session,
704 * and send an event to the last client to close the document
705 * @param client the last client of the client
707 private void closeSession(SimpleClient client)
710 client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client,null);
711 log.debug("close document request done");
712 this.closeSession(client.getSessionHandle());
716 * CLoses the current session
717 * @param sessionHandle sessionHandle of the session to remove
719 private void closeSession(SessionHandle sessionHandle)
721 getSessionManager().removeSession(sessionHandle);
722 log.debug("Session removed");
725 * @return the sessionManager
727 protected SimpleSessionManager getSessionManager() {
728 return sessionManager;
731 * @param sessionManager the sessionManager to set
733 protected void setSessionManager(SimpleSessionManager sessionManager) {
734 this.sessionManager = sessionManager;
736 public ClientsFile getStoreDocFile() {
737 if (storedocfile==null) {
743 ClientSessionFileWatcherElement clistWatchElement=null;
744 public ClientSessionFileWatcherElement getClientWatcherElement() {
745 if (clistWatchElement==null) {
746 clistWatchElement=new ClientSessionFileWatcherElement(clist,null);
748 return clistWatchElement;
751 * writes a vector of vorba Ids to the session.
753 public void setModObjectList(Vector modObjects) {
754 log.debug("Writing "+modObjects.size()+" ids to ModObjectList");
755 // TODO Auto-generated method stub
758 * get current list of modified objects.
759 * @return null or Vector of objects
761 public Vector getModObjectList() {
762 log.debug("Reading modObjectList");