VamsasSession: increased time before considering the current client is the last one...
[vamsas.git] / src / uk / ac / vamsas / client / simpleclient / VamsasSession.java
1 package uk.ac.vamsas.client.simpleclient;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.RandomAccessFile;
6
7 import org.apache.commons.logging.Log;
8 import org.apache.commons.logging.LogFactory;
9 import org.apache.log4j.FileAppender;
10 import org.apache.log4j.Logger;
11 import org.apache.log4j.PatternLayout;
12
13 import uk.ac.vamsas.client.ClientHandle;
14 import uk.ac.vamsas.client.Events;
15 import uk.ac.vamsas.client.IClient;
16 import uk.ac.vamsas.client.UserHandle;
17 /**
18  * Does all the IO operations for a SimpleClient instance accessing 
19  * a SimpleClient vamsas session.
20  * 
21  * Basically, it defines the various standard names for the files 
22  * in the session directory (that maps to the sessionUrn), 
23  * provides constructors for the file handlers and watchers of 
24  * those file entities, and some higher level methods 
25  * to check and change the state flags for the session.
26  * 
27  * TODO: move the stuff below to the SimpleClientFactory documentation.
28  * much may not be valid now :
29  * Vamsas client is intialised with a path to create live session directories. 
30  * This path may contain a vamsas.properties file 
31  * that sets additional parameters (otherwise client 
32  * just uses the one on the classpath).
33  * 
34  * A vamsas session consists of :
35  *  SessionDir - translates to urn of a live session.
36  *  Contains: Vamsas Document (as a jar), Session client list file, 
37  *  both of which may be locked, and additional 
38  *  temporary versions of these files when write 
39  *  operations are taking place.
40  * 
41  * Zip file entries
42  *  - vamsasdocument.xml : core info
43  *  one or more:
44  *  - <applicationname>.version.sessionnumber.raw (string given in vamsasdocument.xml applicationData entry)
45  *  
46  * Lockfile
47  *  - filename given in the vamsasdocument.xml. Should be checked for validity by any client and rewritten if necessary. 
48  *    The lockfile can point to the jar itself.
49  * Mode of operation.
50  * Initially - documentHandler either:
51  *  - creates a zip for a new session for the client
52  *  - connect to an existing session zip 
53  *   1. reads session urn file
54  *   2. waits for lock
55  *   3. examines session - decide whether to create new application data slice or connect to one stored in session.
56  *   4. writes info into session file
57  *   5. releases lock and generates local client events.
58  *   6. Creates Watcher thread to generate events.
59  * 
60  * During the session
61  *  - Update watcher checks for file change - 
62  * 
63  * Procedures for file based session message exchange
64  *  - session document modification flag
65  *    
66  */
67
68 public class VamsasSession {
69   /**
70    * indicator file for informing other processes that 
71    * they should finalise their vamsas datasets for 
72    * storing into a vamsas archive.
73    */
74   public static final String CLOSEANDSAVE_FILE="stored.log";
75   /**
76    * session file storing the last_stored_stat data 
77    */
78   public static final String MODIFIEDDOC_FILE="modified";
79
80   
81   private SimpleSessionManager sessionManager = null;
82   
83   /**
84    * Count of cycles before considering the current client as the last one of the session (if no other client registered as active )
85    */
86   private final int watchCycleCountBeforeLastClient = 1100 ;
87   
88   /**
89    * time between checking 
90    */
91   public int WATCH_SLEEP=30; 
92   
93   //protected String clientFileDirectory = "clients";
94   
95   /**
96    * called to clear update flag after a successful offline storage event
97    */
98   protected void clearUnsavedFlag() {
99     SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
100     if (!laststored.clearFlag())
101       log.warn("Unsaved flag was not cleared for "+sessionDir);
102   }
103   /**
104    * called to indicate session document has been modified.
105    *
106    */
107   protected void setUnsavedFlag() {
108     SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
109     if (!laststored.setFlag())
110       log.warn("Couldn't set the Unsaved flag for "+sessionDir);
111   }
112   /**
113    * 
114    * @return true if session document has been modified since last offline storage event 
115    */
116   protected boolean getUnsavedFlag() {
117     SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
118     return laststored.checkFlag();
119   }
120   /**
121    * log file location
122    */
123   public static final String SESSION_LOG="Log.txt";
124   private static Log log = LogFactory.getLog(VamsasSession.class);
125   protected Logger slog = Logger.getLogger("uk.ac.vamsas.client.SessionLog");
126   /**
127    * setup the sessionLog using Log4j.
128    * @throws IOException
129    */
130   private void initLog() throws IOException {
131     // TODO: fix session event logging
132     // LATER: make dedicated appender format for session log.
133     /*Appender app = slog.getAppender("log4j.appender.SESSIONLOG");
134     // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
135     // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
136     for (Enumeration e = slog.getAllAppenders() ; e.hasMoreElements() ;) {
137       System.out.println(e.nextElement());
138
139     }*/
140   
141     if (slog!= null ) {
142       File sessionLogFile =  new File(this.sessionDir, SESSION_LOG);
143       slog.addAppender(new FileAppender(new PatternLayout("%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(), true));
144     } else {
145       log.info("No appender for SessionLog");
146     }
147   }
148   
149   /**
150    * the sessionDir is given as the session location for new clients.
151    */
152   protected File sessionDir;
153   /**
154    * holds the list of attached clients
155    */
156   ClientsFile clist;
157   public static final String CLIENT_LIST="Clients.obj";
158   /**
159    * holds the data
160    */
161   VamsasFile vamArchive; 
162   public static final String VAMSAS_OBJ="VamDoc.jar";
163   
164   /**
165    * sets up the vamsas session files and watchers in sessionDir
166    * @param sessionDir1
167    */
168   protected VamsasSession(File sessionDir1) throws IOException {
169     if (sessionDir1==null)
170       throw new Error("Null directory for VamsasSession.");
171     if (sessionDir1.exists()) {
172       if (!sessionDir1.isDirectory() || !sessionDir1.canWrite() || !sessionDir1.canRead())
173         throw new IOException("Cannot access '"+sessionDir1+"' as a read/writable Directory.");
174       if (!checkSessionFiles(sessionDir1))
175       log.warn("checkSessionFiles() returned false. Possible client implementation error");
176       this.sessionDir = sessionDir1; 
177       initSessionObjects();
178       slog.debug("Initialising additional VamsasSession instance");
179       log.debug("Attached to VamsasSession in "+sessionDir1);
180       //} 
181     } else {
182       // start from scratch
183       if (!sessionDir1.mkdir())
184         throw new IOException("Failed to make VamsasSession directory in "+sessionDir1);
185       createSessionFiles();
186       initSessionObjects();
187       slog.debug("Session directory created.");
188       log.debug("Initialised VamsasSession in "+sessionDir1);
189     }
190   }
191   /**
192    * tests presence of existing sessionfiles files in dir
193    * @param dir
194    * @return
195    */
196   private boolean checkSessionFiles(File dir) throws IOException {
197     File c_file = new File(dir,CLIENT_LIST);
198     File v_doc = new File(dir,VAMSAS_OBJ);
199     if (c_file.exists() && v_doc.exists())
200       return true;
201     return false;
202   }
203   /**
204    * create new empty files in dir
205    *
206    */
207   private void createSessionFiles() throws IOException {
208     if (sessionDir==null)
209       throw new IOException("Invalid call to createSessionFiles() with null sessionDir");
210     File c_file = new File(sessionDir,CLIENT_LIST);
211     File v_doc = new File(sessionDir,VAMSAS_OBJ);
212     if (!c_file.exists() && c_file.createNewFile())
213       log.debug("Created new ClientFile "+c_file); // don't care if this works or not
214     if (!v_doc.exists() && v_doc.createNewFile())
215       log.debug("Created new Vamsas Session Document File "+v_doc); 
216   }
217   /**
218    * construct SessionFile objects and watchers for each
219    */
220   private void initSessionObjects() throws IOException {
221     createSessionFiles();
222     if (clist!=null || vamArchive!=null)
223       throw new IOException("initSessionObjects called for initialised VamsasSession object.");
224     clist = new ClientsFile(new File(sessionDir,CLIENT_LIST));
225     vamArchive = new VamsasFile(new File(sessionDir,VAMSAS_OBJ));
226     storedocfile=new ClientsFile(new File(sessionDir, CLOSEANDSAVE_FILE));
227     initLog();
228   }
229   /**
230    * make a new watcher object for the clientFile
231    * @return new ClientFile watcher instance
232    */
233  public FileWatcher getClientWatcher() {
234     return new FileWatcher(clist.sessionFile);
235   }
236   /**
237    * make a new watcher object for the vamsas Document
238    * @return new ClientFile watcher instance
239    */
240   public FileWatcher getDocWatcher() {
241     return new FileWatcher(vamArchive.sessionFile);
242   }
243   FileWatcher store_doc_file=null;
244   public ClientsFile storedocfile=null;
245   /**
246    * make a new watcher object for the messages file
247    * @return new watcher instance
248    */
249   public FileWatcher getStoreWatcher() {
250     return new FileWatcher(new File(sessionDir,CLOSEANDSAVE_FILE));
251
252   }
253   /**
254    * write to the StoreWatcher file to indicate that a storeDocumentRequest has been made.
255    * The local client's storeWatcher FileWatcher object is updated so the initial change is not registered.
256    * @param client
257    * @param user
258    * @return
259    */
260   public void addStoreDocumentRequest(ClientHandle client, UserHandle user) throws IOException {
261     // TODO: replace this with clientsFile mechanism
262     SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE));
263     while (!sfw.lockFile())
264       log.debug("Trying to get lock for "+CLOSEANDSAVE_FILE);
265     RandomAccessFile sfwfile=sfw.fileLock.getRaFile();
266     sfwfile.setLength(0); // wipe out any old info.
267     // TODO: rationalise what gets written to this file (ie do we want other clients to read the id of the requestor?)
268     sfwfile.writeUTF(client.getClientUrn()+":"+user.getFullName()+"@"+user.getOrganization());
269     sfw.unlockFile();
270     if (store_doc_file!=null)
271       store_doc_file.setState();
272     slog.info("FinalizeAppData request from "+user.getFullName()+" using "+client.getClientUrn()+"");
273   }
274   /**
275    * create a new session with an existing vamsas Document - by copying it into the session.
276    * @param archive
277    */
278   public void setVamsasDocument(File archive) throws IOException {
279     log.debug("Transferring vamsas data from "+archive+" to session:"+vamArchive.sessionFile);
280     SessionFile xtantdoc = new SessionFile(archive);
281     vamArchive.updateFrom(null, xtantdoc);
282     // LATER: decide if session archive provenance should be updated to reflect access.
283     // TODO: soon! do a proper import objects from external file 
284     log.debug("Transfer complete.");
285   }
286   /**
287    * write session as a new vamsas Document (this will overwrite any existing file without warning)
288    * TODO: test
289    * TODO: verify that lock should be released for vamsas document.
290    * @param destarchive
291    */
292   protected void writeVamsasDocument(File destarchive, Lock extlock) throws IOException {
293     log.debug("Transferring vamsas data from "+vamArchive.sessionFile+" to session:"+destarchive);
294     SessionFile newdoc = new SessionFile(destarchive);
295     if (extlock==null && !vamArchive.lockFile())
296       while (!vamArchive.lockFile())
297         log.info("Trying to get lock for "+vamArchive.sessionFile);
298     // TODO: LATER: decide if a provenance entry should be written in the exported document recording the export from the session
299     newdoc.updateFrom(extlock, vamArchive);
300     // 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).
301     vamArchive.unLock();
302     newdoc.unlockFile();
303     log.debug("Transfer complete.");
304   }
305   /**
306          * extant archive IO handler
307          */
308   VamsasArchive _va=null;
309   /**
310    * Creates a VamsasArchive Vobject for accessing and updating document
311    * Note: this will lock the Vamsas Document for exclusive access to the client.
312    * @return session vamsas document
313    * @throws IOException if locks fail or vamsas document read fails.
314    */
315   protected VamsasArchive getVamsasDocument() throws IOException {
316     // check we haven't already done this once - probably should be done by caller
317     if (_va!=null)
318       return _va;
319     // patiently wait for a lock on the document. (from ArchiveClient.getUpdateable())
320     long tries=5000;
321     while (vamArchive.getLock()==null && --tries>0) {
322 //       Thread.sleep(1);
323         log.debug("Trying to get a document lock for the "+tries+"'th time.");
324       }
325     if (tries==0) 
326       throw new IOException("Failed to get lock for vamsas archive.");
327       
328     VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true, vamArchive);
329
330     return va;
331   }
332   /**
333    * Unlocks the vamsas archive session document after it has been closed.
334    * @throws IOException
335    */
336   protected void unlockVamsasDocument() throws IOException {
337     if (_va!=null)
338       _va.closeArchive();
339     _va=null;
340     if (vamArchive!=null)
341       vamArchive.unLock();
342     
343   }
344   /**
345    * create a uniquely named uk.ac.vamsas.client.simpleclient.ClientsFile.addClient(ClientHandle)ile in the session Directory
346    * @see java.io.File.createTempFile
347    * @param pref Prefix for name
348    * @param suff Suffix for name
349    * @return SessionFile object configured for the new file (of length zero)
350    * @throws IOException
351    */
352   protected SessionFile getTempSessionFile(String pref, String suff) throws IOException {
353     File tfile = File.createTempFile(pref,suff,sessionDir);
354     SessionFile tempFile = new SessionFile(tfile);
355     return tempFile;
356   }
357   
358   /**
359    * add a IClient to the session
360    * 
361    * add the client to the client list file
362    * @param client client to add to the session
363    */
364   protected void addClient(IClient client)
365   {
366     if (client == null)
367       slog.error("Try to add a null client to the session ");
368     else {
369       log.debug("Adding client "+client.getClientHandle().getClientUrn());
370       getClientWatcherElement().haltWatch();
371       clist.addClient(client.getClientHandle());
372      
373       log.debug("Added.");
374       log.debug("Register Client as Active.");
375      /* try {
376         client.createActiveClientFile();
377       } catch (IOException e) {
378         log.debug("Error during  active client file creation.");
379       }*/
380       //tracks modification to the client list and readds client to the list
381       getClientWatcherElement().setHandler(new AddClientWatchCallBack(client));
382       getClientWatcherElement().enableWatch();
383     }
384   }
385   
386   /**
387    * Handler for the client watcher.
388    * 
389    * If (the current client is not in the client list, it is added again;)
390    */  
391   private class AddClientWatchCallBack  implements WatcherCallBack
392   {
393    
394     private IClient client ;
395     
396     /**
397     *Inits the handler with the client to check in the list
398      * @param client client to monitor in the client list
399      */
400     protected  AddClientWatchCallBack (IClient client)
401       {
402         this.client = client;
403       }
404     
405       /**
406        * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
407        * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
408        */
409       public boolean handleWatchEvent(WatcherElement watcher, Lock lock)
410         {
411           boolean isWatchEnable = watcher.isWatchEnabled();
412           if (lock== null)//no update on the list
413             return isWatchEnable;
414         //  log.debug("change on the client list ");
415           if (client != null)
416             {
417         
418             
419             //checks if the client is not already in the lists
420               ClientHandle[] cl = clist.retrieveClientList(lock);//clist.retrieveClientList();
421               boolean found = false;
422               if (cl != null)
423                 {
424                   for (int chi = cl.length-1; !found && chi > -1; chi--) {
425                     found =  cl[chi].equals(this.client.getClientHandle());
426                   }
427                  
428                 } 
429               if (! found) 
430                 {log.debug("client not in the list ");
431                   if( log.isDebugEnabled())
432                       log.debug("the client has not been found in the list. Adding it again :"+cl);
433                     addClient(client);
434               }
435               else
436                 log.debug("client is in the list");
437
438             }
439           //log.debug("isWatchEnable "+isWatchEnable);
440           return isWatchEnable;
441         }
442       }
443   
444 /**
445  *  
446  * removes a client from the current session
447  *  removes the client from the session client list
448  *  if the client is the last one from the session  (ClientList), the current session is removed 
449  *  from active session list.
450  *  
451  *  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,
452  *  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
453  *  
454  * @param client client to remove
455  */
456   protected void removeClient(SimpleClient client)//IClient client)
457   {
458     if (client == null)
459       {
460         log.error("Null client passed to removeClient");
461         return;
462       }
463     ClientSessionFileWatcherElement cwe=getClientWatcherElement();
464     if (cwe!=null && cwe.isWatchEnabled()) {
465       cwe.haltWatch();
466     };
467     //set handler to check is the the last active client of the session
468     //Wait for several watchers cycle to see if the current client was the last client active in the session.
469     //if yes, close the session
470     
471     getClientWatcherElement().setHandler(new RemoveClientWatchCallBack (client));
472     getClientWatcherElement().setTimeoutBeforeLastCycle(this.watchCycleCountBeforeLastClient);
473     log.info("remove client from list");
474     clist.clearList();
475     log.info("client list cleared");
476     if (cwe!=null) {
477       cwe.enableWatch();
478       
479       
480      /* try {
481         log.debug("Releasing  active client file");
482         client.releaseActiveClientFile();
483       } catch (IOException e) {
484         log.error("error during active file client release");
485       }*/
486     }
487    
488   
489    
490     /*clist.removeClient(client.getClientHandle(),null);
491     if (this.clist.retrieveClientList() == null|| this.clist.retrieveClientList().length<1)
492       {//assume it is the last client has been removed shutting down session
493         slog.info("last client removed: removing session");
494         log.debug("last client removed: removing session");
495         this.getSessionManager().removeSession(client.getSessionHandle());
496       }
497     else
498       {
499         int active=clist.retrieveClientList().length;
500         log.debug("Still "+active+" active clients");
501         slog.info("Still "+active+" active clients");
502       }*/
503    
504   }
505   
506   /**
507    * Handler for the client watcher. after a client have been removed
508    * 
509    * Checks if the client is not the last active one.
510    * 
511    * If (the current client is not in the client list readd it;)
512    */  
513   private class RemoveClientWatchCallBack  implements WatcherCallBack
514   {
515    
516     private SimpleClient client ;
517     private boolean manualCheckOfClientCount = false;
518     /**
519     *Inits the handler with the client to check in the list
520      * @param client client to monitor in the client list
521      */
522     protected  RemoveClientWatchCallBack (SimpleClient client)
523       {
524         this.client = client;
525       }
526     
527       /**
528        * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
529        * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
530        */
531       public boolean handleWatchEvent(WatcherElement watcher, Lock lock)
532         { 
533         // if lock is null, no client has been added since last, clear.
534         //the client is then the last client
535           if (client != null)
536             {
537            
538               if (lock == null )
539               {
540             
541             //checks if the client is not already in the lists
542            //   ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
543 //            ask to the client to cpoy application data into the document
544               client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
545               boolean islastClient =  true;
546               if (manualCheckOfClientCount)
547                 {
548                 log.debug("manual checking of count of client");
549   //checks if the client is not already in the lists
550                   ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
551                   if(cl == null || cl.length<1 )
552        //  {//no client has registered as active 
553                   {
554                     islastClient = true;
555                     log.debug("list is empty");
556                   }
557                   else
558                     islastClient = false;
559                   log.debug("list is not empty");
560                 }
561             //  if(cl == null || cl.length<1 )
562               //  {//no client has registered as active
563               if (islastClient)
564               {
565                 //the client is the last one, so close current session
566                   log.info("last client removed: closing session");
567
568 //                close document 
569                   client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client,null);
570                   log.debug("close document request done");
571                   
572                   getSessionManager().removeSession(client.getSessionHandle());
573                   log.debug("Session removed");
574               }
575             }
576               else
577               {
578                 log.debug("not the last client found ");
579 //              ask to the client to cpoy application data into the document
580        //         client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
581
582            //   /  }
583          
584             }
585               log.debug("Stopping EventGenerator..");
586           client.evgen.stopWatching();
587         }
588           watcher.setHandler(null);//Do not check if the client is the last client. watcher will shutdown anyway
589         //  watcher.haltWatch();
590          // watcher.
591           return false;
592         }
593       }
594
595 /**
596  * @return the sessionManager
597  */
598 protected SimpleSessionManager getSessionManager() {
599   return sessionManager;
600 }
601 /**
602  * @param sessionManager the sessionManager to set
603  */
604 protected void setSessionManager(SimpleSessionManager sessionManager) {
605   this.sessionManager = sessionManager;
606 }
607 public ClientsFile getStoreDocFile() {
608   if (storedocfile==null) {
609     
610   }
611   return storedocfile;
612 }
613
614 ClientSessionFileWatcherElement clistWatchElement=null;
615 public ClientSessionFileWatcherElement getClientWatcherElement() {
616   if (clistWatchElement==null) {
617     clistWatchElement=new ClientSessionFileWatcherElement(clist,null);
618   }
619   return clistWatchElement;
620 }
621 /**
622  * writes a vector of vorba Ids to the session.
623  * @param modObjects 
624 public void setModObjectList(Vector modObjects) {
625   log.debug("Writing "+modObjects.size()+" ids to ModObjectList");
626   // TODO Auto-generated method stub
627 }
628 **
629  * get current list of modified objects.
630  * @return null or Vector of objects
631  *
632 public Vector getModObjectList() {
633   log.debug("Reading modObjectList");
634   return null;
635 }
636 */
637 }
638
639