safety checks and informational Errors generated when IClientDocument is not stored...
[vamsas.git] / src / uk / ac / vamsas / client / simpleclient / VamsasSession.java
index 1c77d66..2ae181d 100644 (file)
@@ -1,21 +1,22 @@
 package uk.ac.vamsas.client.simpleclient;
 
-import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.PrintStream;
-import java.io.PrintWriter;
 import java.io.RandomAccessFile;
-import java.io.Writer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.OverlappingFileLockException;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-import org.apache.log4j.Appender;
-import org.apache.log4j.Logger;
 import org.apache.log4j.FileAppender;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
 
 import uk.ac.vamsas.client.ClientHandle;
+import uk.ac.vamsas.client.Events;
 import uk.ac.vamsas.client.IClient;
+import uk.ac.vamsas.client.SessionHandle;
 import uk.ac.vamsas.client.UserHandle;
 /**
  * Does all the IO operations for a SimpleClient instance accessing 
@@ -84,6 +85,18 @@ public class VamsasSession {
   private SimpleSessionManager sessionManager = null;
   
   /**
+   * Count of cycles before considering the current client as the last one of the session (if no other client registered as active )
+   */
+  private final int watchCycleCountBeforeLastClient = 1220 ;
+  
+  /**
+   * time between checking 
+   */
+  public int WATCH_SLEEP=30; 
+  
+  protected String clientFileDirectory = "clients";
+  
+  /**
    * called to clear update flag after a successful offline storage event
    */
   protected void clearUnsavedFlag() {
@@ -119,22 +132,22 @@ public class VamsasSession {
    * @throws IOException
    */
   private void initLog() throws IOException {
+    // TODO: fix session event logging
     // LATER: make dedicated appender format for session log.
-    Appender app = slog.getAppender("SESSION_LOG");
-   // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
-    
-    //Appender app = slog.getAppender("SESSION_LOG");
-    if (app == null) log.info("No appender for SESSION_LOG");
+    /*Appender app = slog.getAppender("log4j.appender.SESSIONLOG");
+    // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
+    // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath()));
+    for (Enumeration e = slog.getAllAppenders() ; e.hasMoreElements() ;) {
+      System.out.println(e.nextElement());
+
+    }*/
   
-    if (slog!= null && app != null)
-      {
-        if (app instanceof FileAppender)
-          {
-            File sessionLogFile =  new File(this.sessionDir, ((FileAppender)app).getFile());
-            slog.addAppender(new FileAppender(app.getLayout(), sessionLogFile.getAbsolutePath()));
-          }
-        // slog.removeAppender("SESSION_LOG");
-      }
+    if (slog!= null ) {
+      File sessionLogFile =  new File(this.sessionDir, SESSION_LOG);
+      slog.addAppender(new FileAppender(new PatternLayout("%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(), true));
+    } else {
+      log.info("No appender for SessionLog");
+    }
   }
   
   /**
@@ -162,22 +175,17 @@ public class VamsasSession {
     if (sessionDir1.exists()) {
       if (!sessionDir1.isDirectory() || !sessionDir1.canWrite() || !sessionDir1.canRead())
         throw new IOException("Cannot access '"+sessionDir1+"' as a read/writable Directory.");
-      this.sessionDir = sessionDir1;//createSessionFiles, need sessionDir attribute set
-      if (!checkSessionFiles(sessionDir1)) 
-        {
-          createSessionFiles();
-        }
-        // session files exist in the directory
-        
-        initSessionObjects();
-        slog.debug("Initialising additional VamsasSession instance");
-        log.debug("Attached to VamsasSession in "+sessionDir1);
+      if (!checkSessionFiles(sessionDir1))
+      log.warn("checkSessionFiles() returned false. Possible client implementation error");
+      this.sessionDir = sessionDir1; 
+      initSessionObjects();
+      slog.debug("Initialising additional VamsasSession instance");
+      log.debug("Attached to VamsasSession in "+sessionDir1);
       //} 
     } else {
       // start from scratch
       if (!sessionDir1.mkdir())
         throw new IOException("Failed to make VamsasSession directory in "+sessionDir1);
-      this.sessionDir = sessionDir1; 
       createSessionFiles();
       initSessionObjects();
       slog.debug("Session directory created.");
@@ -205,48 +213,45 @@ public class VamsasSession {
       throw new IOException("Invalid call to createSessionFiles() with null sessionDir");
     File c_file = new File(sessionDir,CLIENT_LIST);
     File v_doc = new File(sessionDir,VAMSAS_OBJ);
-    if (c_file.createNewFile())
+    if (!c_file.exists() && c_file.createNewFile())
       log.debug("Created new ClientFile "+c_file); // don't care if this works or not
-    if (v_doc.createNewFile())
+    if (!v_doc.exists() && v_doc.createNewFile())
       log.debug("Created new Vamsas Session Document File "+v_doc); 
   }
   /**
    * construct SessionFile objects and watchers for each
    */
   private void initSessionObjects() throws IOException {
+    createSessionFiles();
     if (clist!=null || vamArchive!=null)
       throw new IOException("initSessionObjects called for initialised VamsasSession object.");
     clist = new ClientsFile(new File(sessionDir,CLIENT_LIST));
     vamArchive = new VamsasFile(new File(sessionDir,VAMSAS_OBJ));
+    storedocfile=new ClientsFile(new File(sessionDir, CLOSEANDSAVE_FILE));
     initLog();
   }
   /**
    * make a new watcher object for the clientFile
    * @return new ClientFile watcher instance
    */
-  public FileWatcher getClientWatcher() {
+ public FileWatcher getClientWatcher() {
     return new FileWatcher(clist.sessionFile);
   }
-  FileWatcher session_doc_watcher=null;
   /**
    * make a new watcher object for the vamsas Document
    * @return new ClientFile watcher instance
    */
   public FileWatcher getDocWatcher() {
-    if (session_doc_watcher==null)
-      return session_doc_watcher = new FileWatcher(vamArchive.sessionFile);
     return new FileWatcher(vamArchive.sessionFile);
   }
   FileWatcher store_doc_file=null;
+  public ClientsFile storedocfile=null;
   /**
    * make a new watcher object for the messages file
-   * (thread safe - keeps a reference to the first watcher)
    * @return new watcher instance
    */
   public FileWatcher getStoreWatcher() {
-    if (store_doc_file==null)
-      return store_doc_file = new FileWatcher(new File(CLOSEANDSAVE_FILE));
-    return new FileWatcher(new File(CLOSEANDSAVE_FILE));
+    return new FileWatcher(new File(sessionDir,CLOSEANDSAVE_FILE));
 
   }
   /**
@@ -257,6 +262,7 @@ public class VamsasSession {
    * @return
    */
   public void addStoreDocumentRequest(ClientHandle client, UserHandle user) throws IOException {
+    // TODO: replace this with clientsFile mechanism
     SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE));
     while (!sfw.lockFile())
       log.debug("Trying to get lock for "+CLOSEANDSAVE_FILE);
@@ -293,14 +299,17 @@ public class VamsasSession {
     if (extlock==null && !vamArchive.lockFile())
       while (!vamArchive.lockFile())
         log.info("Trying to get lock for "+vamArchive.sessionFile);
-    // TODO: LATER: decide if session archive provenance should be written in vamsasDocument file for this export.
+    // TODO: LATER: decide if a provenance entry should be written in the exported document recording the export from the session
     newdoc.updateFrom(extlock, vamArchive);
     // 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).
     vamArchive.unLock();
     newdoc.unlockFile();
     log.debug("Transfer complete.");
   }
-  
+  /**
+        * extant archive IO handler
+        */
+  VamsasArchive _va=null;
   /**
    * Creates a VamsasArchive Vobject for accessing and updating document
    * Note: this will lock the Vamsas Document for exclusive access to the client.
@@ -308,15 +317,35 @@ public class VamsasSession {
    * @throws IOException if locks fail or vamsas document read fails.
    */
   protected VamsasArchive getVamsasDocument() throws IOException {
-    // TODO: check we haven't already done this once
-    if (!vamArchive.lockFile()) 
+    // check we haven't already done this once - probably should be done by caller
+    if (_va!=null)
+      return _va;
+    // patiently wait for a lock on the document. (from ArchiveClient.getUpdateable())
+    long tries=5000;
+    while (vamArchive.getLock()==null && --tries>0) {
+//       Thread.sleep(1);
+        log.debug("Trying to get a document lock for the "+tries+"'th time.");
+      }
+    if (tries==0) 
       throw new IOException("Failed to get lock for vamsas archive.");
-    
+      
     VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true, vamArchive);
-    
+
     return va;
   }
   /**
+   * Unlocks the vamsas archive session document after it has been closed.
+   * @throws IOException
+   */
+  protected void unlockVamsasDocument() throws IOException {
+    if (_va!=null)
+      _va.closeArchive();
+    _va=null;
+    if (vamArchive!=null)
+      vamArchive.unLock();
+    
+  }
+  /**
    * create a uniquely named uk.ac.vamsas.client.simpleclient.ClientsFile.addClient(ClientHandle)ile in the session Directory
    * @see java.io.File.createTempFile
    * @param pref Prefix for name
@@ -336,14 +365,87 @@ public class VamsasSession {
    * add the client to the client list file
    * @param client client to add to the session
    */
-  protected void addClient(IClient client)
+  protected void addClient(SimpleClient client)
   {
     if (client == null)
-      this.slog.error("Try to add a null client to the session ");
-    else
-      this.clist.addClient(client.getClientHandle(), getClientWatcher().getChangedState());
+      slog.error("Try to add a null client to the session ");
+    else {
+      log.debug("Adding client "+client.getClientHandle().getClientUrn());
+      getClientWatcherElement().haltWatch();
+      clist.addClient(client.getClientHandle());
+     
+      log.debug("Added.");
+      log.debug("Register Client as Active.");
+      try {
+        client.createActiveClientFile();
+      } catch (IOException e) {
+        log.debug("Error during  active client file creation.");
+      }
+      //tracks modification to the client list and readds client to the list
+      getClientWatcherElement().setHandler(new AddClientWatchCallBack(client));
+      getClientWatcherElement().enableWatch();
+     
+    }
   }
   
+  /**
+   * Handler for the client watcher.
+   * 
+   * If (the current client is not in the client list, it is added again;)
+   */  
+  private class AddClientWatchCallBack  implements WatcherCallBack
+  {
+   
+    private SimpleClient client ;
+    
+    /**
+    *Inits the handler with the client to check in the list
+     * @param client client to monitor in the client list
+     */
+    protected  AddClientWatchCallBack (SimpleClient client)
+      {
+        this.client = client;
+      }
+    
+      /**
+       * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
+       * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
+       */
+      public boolean handleWatchEvent(WatcherElement watcher, Lock lock)
+        {
+          boolean isWatchEnable = watcher.isWatchEnabled();
+          if (lock== null)//no update on the list
+            return isWatchEnable;
+          log.debug("change on the client list ");
+          if (client != null)
+            {
+        
+            
+            //checks if the client is not already in the lists
+              ClientHandle[] cl = clist.retrieveClientList(lock);//clist.retrieveClientList();
+              boolean found = false;
+              if (cl != null)
+                {
+                  for (int chi = cl.length-1; !found && chi > -1; chi--) {
+                    found =  cl[chi].equals(this.client.getClientHandle());
+                  }
+                 
+                } 
+              if (! found) 
+                {log.debug("client not in the list ");
+                  if( log.isDebugEnabled())
+                      log.debug("the client has not been found in the list. Adding it again :"+cl);
+                    addClient(client);
+              }
+              else
+                log.debug("client is in the list");
+
+            }
+          log.debug("isWatchEnable "+isWatchEnable);
+          return isWatchEnable;
+        }
+      }
+  
 /**
  *  
  * removes a client from the current session
@@ -351,28 +453,264 @@ public class VamsasSession {
  *  if the client is the last one from the session  (ClientList), the current session is removed 
  *  from active session list.
  *  
+ *  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,
+ *  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
+ *  
  * @param client client to remove
  */
-  protected void removeClient(IClient client)
+  protected void removeClient(SimpleClient client)//IClient client)
   {
     if (client == null)
       {
-      //System.out.println("Try to remove a null client.");
-        this.slog.error("Try to remove a null client.");
+        log.error("Null client passed to removeClient");
         return;
       }
-    this.clist.removeClient(client.getClientHandle(), getClientWatcher().getChangedState());
-    if (this.clist.retrieveClientList() == null|| this.clist.retrieveClientList().length<1)
-      {//assume it is the last client has been removed shutting down session
-        System.out.println("last client removed: removing session");
-        this.getSessionManager().removeSession(client.getSessionHandle());
+    ClientSessionFileWatcherElement cwe=getClientWatcherElement();
+    if (cwe!=null && cwe.isWatchEnabled()) {
+      cwe.haltWatch();
+    };
+    //set handler to check is the the last active client of the session
+    //Wait for several watchers cycle to see if the current client was the last client active in the session.
+    //if yes, close the session
+    
+   // getClientWatcherElement().setHandler(new RemoveClientWatchCallBack (client));
+    getClientWatcherElement().setTimeoutBeforeLastCycle(this.watchCycleCountBeforeLastClient);
+    log.info("remove client from list");
+    clist.clearList();
+    log.info("list cleared");
+    if (cwe!=null) {
+      cwe.enableWatch();
+      
+      
+      log.debug("Stopping EventGenerator..");
+      client.evgen.stopWatching();
+      cwe.setHandler(null);
+      
+      // ask to the client to copy application data into the document
+      client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
+      boolean closeSession=isLastActiveClient(client);
+      if (closeSession)
+      {
+        log.debug("Raising request-to-save event");
+        client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client, null);
+        client.evgen._raise(Events.SESSION_SHUTDOWN, null, client.getSessionHandle(), null);
+      }
+      try 
+        {
+          log.debug("Attempting to release active client locks");
+          client.releaseActiveClientFile();
+        }
+      catch (IOException e)
+        {
+          log.error("error during active file client release");
+         }
+      if (closeSession)
+      {
+        log.debug("Last active client: closing session");
+        log.info("Closing session");
+          getSessionManager().removeSession(client.getSessionHandle());
       }
-    else
-    {
-      this.slog.debug("Still "+this.clist.retrieveClientList().length +" active clients");
-      System.out.println("Still "+(this.clist.retrieveClientList()==null?"null":this.clist.retrieveClientList().length+"") +" active clients");
     }
   }
+  
+  
+  private boolean isLastActiveClient(SimpleClient client)
+    {
+    log.debug("Testing if current client is the last one.");
+    log.debug("current client lockfile is '"+client.getClientlockFile()+"'");
+      boolean noOtherActiveClient = true;
+      //create, if need,  subdirectory to contain client files
+      File clientlockFileDir = new File (this.sessionDir, clientFileDirectory);
+      if( !clientlockFileDir.exists())
+        {
+          log.error("Something wrong the active client file does not exits... should not happen");
+          return false;
+        }
+      
+      try {
+    
+        //no check every file in the directory and try to get lock on it.
+        File [] clientFiles = clientlockFileDir.listFiles();
+        if(clientFiles == null || clientFiles.length==0)
+          {//there is not file on the directory. the current client should be the last one.
+            return true;
+          }
+       
+        for (int i = clientFiles.length - 1; i>-1&& noOtherActiveClient  ;i--)
+          {
+             File clientFile = clientFiles[i];
+             log.debug("testing file for lock: "+clientFile.getAbsolutePath());
+             if(client.getClientLock().isTargetLockFile(clientFile)) 
+               {
+               log.debug("current client file found");
+                 continue;
+               }
+             if (clientFile != null &&  clientFile.exists() )
+             {
+               try
+                 {
+                 log.debug("Try to acquire a lock on the file");
+                 // Get a file channel for the file
+                   FileChannel channel = new RandomAccessFile(clientFile, "rw").getChannel();
+         
+                 // Use the file channel to create a lock on the file.
+                 // This method blocks until it can retrieve the lock.
+                 //  java.nio.channels.FileLock activeClientFilelock = channel.lock();
+         
+                 // Try acquiring the lock without blocking. This method returns
+                 // null or throws an exception if the file is already locked.
+                   try
+                     {
+                     java.nio.channels.FileLock  activeClientFilelock = channel.tryLock();
+                     
+                       //the lock has been acquired. 
+                       //the file was not lock and so the corresponding application seems to have die
+                     if(activeClientFilelock != null)
+                       {
+                         log.debug("lock obtained : file must be from a crashed application");
+                         
+                     
+                         activeClientFilelock.release();
+                         log.debug("lock released");
+                         
+                         channel.close();
+                         log.debug("channel closed");
+                         
+                       //delete file 
+                         clientFile.delete();
+                         log.debug("crashed application file deleted");
+                         
+                       }
+                     else
+                       {
+                       noOtherActiveClient  = false;
+                         log.debug("lock not obtained : another application is active");
+                       }
+                     }
+                   catch (OverlappingFileLockException e) 
+                     {
+                     // File is already locked in this thread or virtual machine
+                     //that the expected behaviour
+                     log.debug("lock not accessible ",e);
+                     }
+                 } 
+               catch (Exception e) 
+                 {
+                 log.debug("error during lock testing ",e);
+                 }
+             }
+          }
+      
+      } catch (Exception e) {
+       log.error("error during counting active clients");
+      }
+      return noOtherActiveClient;
+    }
+  /**
+   * Handler for the client watcher. after a client have been removed
+   * 
+   * Checks if the client is not the last active one.
+   * 
+   * If (the current client is not in the client list readd it;)
+   */  
+  private class RemoveClientWatchCallBack  implements WatcherCallBack
+  {
+   
+    private SimpleClient client ;
+    private boolean manualCheckOfClientCount = false;
+    /**
+    *Inits the handler with the client to check in the list
+     * @param client client to monitor in the client list
+     */
+    protected  RemoveClientWatchCallBack (SimpleClient client)
+      {
+        this.client = client;
+      }
+    
+      /**
+       * If the client list is modified, checks if the current is still in the list. otherwise, readds ti.
+       * @return true to enable watcher, or false to disable it in future WatcherThread cycles.
+       */
+      public boolean handleWatchEvent(WatcherElement watcher, Lock lock)
+        { 
+        // if lock is null, no client has been added since last, clear.
+        //the client is then the last client
+          if (client != null)
+            {
+           
+              if (lock == null )
+              {
+            
+            //checks if the client is not already in the lists
+           //   ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
+
+              boolean islastClient =  true;
+              if (manualCheckOfClientCount)
+                {
+                log.debug("manual checking of count of client");
+  //checks if the client is not already in the lists
+                  ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList();
+                  if(cl == null || cl.length<1 )
+       //  {//no client has registered as active 
+                  {
+                    islastClient = true;
+                    log.debug("list is empty");
+                  }
+                  else
+                    islastClient = false;
+                  log.debug("list is not empty");
+                }
+            //  if(cl == null || cl.length<1 )
+              //  {//no client has registered as active
+              if (islastClient)
+              {
+                //the client is the last one, so close current session
+                  log.info("last client removed: closing session");
+                  closeSession(client);
+              }
+            }
+              else
+              {
+                log.debug("not the last client found ");
+//              ask to the client to cpoy application data into the document
+       //         client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null);
+
+           //   /  }
+         
+            }
+              log.debug("Stopping EventGenerator..");
+              // TODO: ensure ClientsFile lock is really released!! clist.unlockFile();
+          client.evgen.stopWatching();
+        }
+          watcher.setHandler(null);//Do not check if the client is the last client. watcher will shutdown anyway
+        //  watcher.haltWatch();
+         // watcher.
+          return false;
+        }
+      }
+
+  /**
+   * closes the current session, 
+   * and send an event to the last client to close the document
+   * @param client the last client of the client
+   */
+  private void closeSession(SimpleClient client)
+    {
+//   close document 
+      client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client,null);
+      log.debug("close document request done");
+      closeSession(client.getSessionHandle());
+    }
+  
+  /**
+   * CLoses the current session
+   * @param sessionHandle sessionHandle of the session to remove
+   */
+  private void closeSession(SessionHandle sessionHandle)
+    {
+      getSessionManager().removeSession(sessionHandle);
+      log.debug("Session removed");
+    }
 /**
  * @return the sessionManager
  */
@@ -385,6 +723,36 @@ protected SimpleSessionManager getSessionManager() {
 protected void setSessionManager(SimpleSessionManager sessionManager) {
   this.sessionManager = sessionManager;
 }
+public ClientsFile getStoreDocFile() {
+  if (storedocfile==null) {
+    
+  }
+  return storedocfile;
+}
+
+ClientSessionFileWatcherElement clistWatchElement=null;
+public ClientSessionFileWatcherElement getClientWatcherElement() {
+  if (clistWatchElement==null) {
+    clistWatchElement=new ClientSessionFileWatcherElement(clist,null);
+  }
+  return clistWatchElement;
+}
+/**
+ * writes a vector of vorba Ids to the session.
+ * @param modObjects 
+public void setModObjectList(Vector modObjects) {
+  log.debug("Writing "+modObjects.size()+" ids to ModObjectList");
+  // TODO Auto-generated method stub
+}
+**
+ * get current list of modified objects.
+ * @return null or Vector of objects
+ *
+public Vector getModObjectList() {
+  log.debug("Reading modObjectList");
+  return null;
+}
+*/
 }