refactored org to uk
authorjprocter <jprocter@compbio.dundee.ac.uk>
Thu, 14 Dec 2006 17:52:15 +0000 (17:52 +0000)
committerjprocter <jprocter@compbio.dundee.ac.uk>
Thu, 14 Dec 2006 17:52:15 +0000 (17:52 +0000)
git-svn-id: https://svn.lifesci.dundee.ac.uk/svn/repository/trunk@269 be28352e-c001-0410-b1a7-c7978e42abec

28 files changed:
src/uk/ac/vamsas/client/simpleclient/AppDataInputStream.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/AppDataOutputStream.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/ArchiveUrn.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/ClientDocument.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/ClientsFile.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/EventGeneratorThread.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/FileLock.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/FileWatcher.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/IdFactory.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/ListFile.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/Lock.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/LockFactory.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/LockTimeoutException.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/LockedFileOutputStream.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/NativeLock.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SessionFile.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SessionFlagFile.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SessionUrn.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SessionsFile.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SimpleClient.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SimpleClientAppdata.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SimpleClientFactory.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SimpleDocBinding.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/SimpleDocument.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/VamsasArchive.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/VamsasArchiveReader.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/VamsasFile.java [new file with mode: 0644]
src/uk/ac/vamsas/client/simpleclient/VamsasSession.java [new file with mode: 0644]

diff --git a/src/uk/ac/vamsas/client/simpleclient/AppDataInputStream.java b/src/uk/ac/vamsas/client/simpleclient/AppDataInputStream.java
new file mode 100644 (file)
index 0000000..c16d0cf
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.JarInputStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * @author jimp
+ * LATER: this may not be a necessary or useful class to return from IClientAppdata get*InputStream() methods
+ */
+public class AppDataInputStream extends DataInputStream implements DataInput {
+  private Log log = LogFactory.getLog(AppDataInputStream.class);
+  private boolean isOpen = false;
+  /**
+   * Wrapper for writing to/from AppData Entries in a Vamsas Document.
+   */
+  public AppDataInputStream(InputStream inputstream) {
+    super(inputstream);
+    isOpen=true;
+  }
+
+  /* (non-Javadoc)
+   * @see java.io.FilterInputStream#close()
+   */
+  public void close() throws IOException {
+    if (!isOpen) {
+      log.debug("close() called on closed AppDataInputStream.");
+      // throw new IOException("Attempt to close an already closed AppDataInputStream");
+    } else {
+      isOpen=false;
+    }
+  }
+
+  /**
+   * Will return zero if stream has been closed.
+   * @see java.io.FilterInputStream#available()
+   */
+  public int available() throws IOException {
+    if (isOpen)
+      return super.available();
+    else
+      return 0;
+  }
+  
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/AppDataOutputStream.java b/src/uk/ac/vamsas/client/simpleclient/AppDataOutputStream.java
new file mode 100644 (file)
index 0000000..339ec0d
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * @author jimp
+ *
+ */
+public class AppDataOutputStream extends DataOutputStream {
+  private Log log = LogFactory.getLog(AppDataOutputStream.class);
+  private boolean isOpen=true;
+  /**
+   * @param out
+   */
+  public AppDataOutputStream(OutputStream out) {
+    super(out);
+    isOpen=true;
+  }
+  /* (non-Javadoc)
+   * @see java.io.DataOutputStream#flush()
+   */
+  public void flush() throws IOException {
+    if (isOpen)
+      super.flush();
+    else
+      log.warn("flush() called on closed AppDataOutputStream");
+  }
+  /* (non-Javadoc)
+   * @see java.io.DataOutputStream#write(byte[], int, int)
+   */
+  public synchronized void write(byte[] b, int off, int len) throws IOException {
+    if (isOpen) {
+      super.write(b, off, len);
+    } else {
+      log.debug("write(b,off,len) called on closed AppDataOutputStream");
+      throw new IOException("Attempt to write to closed AppDataOutputStream");
+    }
+  }
+  /* (non-Javadoc)
+   * @see java.io.DataOutputStream#write(int)
+   */
+  public synchronized void write(int b) throws IOException {
+    if (isOpen) {
+      super.write(b);
+    } else {
+      log.debug("write(b) called on closed AppDataOutputStream");
+      throw new IOException("Attempt to write to closed AppDataOutputStream");
+    }
+  }
+  /**
+   * Sets an internal flag preventing further write operations 
+   * to the AppData output stream and flushes any pending writes.
+   * @see java.io.FilterOutputStream#close()
+   */
+  public void close() throws IOException {
+    isOpen=false;
+    super.flush();
+    log.debug("AppDataOutputStream was closed.");
+  }
+  /* (non-Javadoc)
+   * @see java.io.FilterOutputStream#write(byte[])
+   */
+  public void write(byte[] b) throws IOException {
+    if (isOpen) {
+      super.write(b);
+    } else {
+      log.debug("write(b[]) called on closed AppDataOutputStream");
+      throw new IOException("Attempt to write to closed AppDataOutputStream");
+    }
+  }
+  /**
+   * @return true if stream is still Open.
+   */
+  public boolean isOpen() {
+    return isOpen;
+  }
+  
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/ArchiveUrn.java b/src/uk/ac/vamsas/client/simpleclient/ArchiveUrn.java
new file mode 100644 (file)
index 0000000..e3cdd3d
--- /dev/null
@@ -0,0 +1,34 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+import java.net.MalformedURLException;
+
+/**
+ * Vamsas Document URN for files understood by ArchiveReader and 
+ * written by VamsasArchive.
+ * vdoc://{Absolute path to file}
+ * @author jimp
+ *
+ */
+public class ArchiveUrn extends org.vamsas.client.SessionUrn {
+  /**
+   * a simple vamsas document urn prefix
+   */
+  public static String VAMSASDOCUMENT="vdoc";
+  static {
+    TYPES.put(ArchiveUrn.VAMSASDOCUMENT, ArchiveUrn.class);
+  }
+
+  public ArchiveUrn(File docLocation) throws MalformedURLException {
+    super(VAMSASDOCUMENT, docLocation.getAbsoluteFile().toURL());
+  }
+
+  /**
+   * TODO: LATER: think about this again.
+   * @return File(urn.getPath())
+   */
+  public File asFile() {
+    return new File(urn.getPath());
+  }
+  // TODO: add abstract 'handler' methods for resolving the URN to a particular class
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/ClientDocument.java b/src/uk/ac/vamsas/client/simpleclient/ClientDocument.java
new file mode 100644 (file)
index 0000000..dde9632
--- /dev/null
@@ -0,0 +1,510 @@
+/*
+ *
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.IOException;
+import java.util.Vector;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.ClientHandle;
+import org.vamsas.client.IClientAppdata;
+import org.vamsas.client.IClientDocument;
+import org.vamsas.client.UserHandle;
+import org.vamsas.client.Vobject;
+import org.vamsas.client.VorbaId;
+import org.vamsas.objects.core.ApplicationData;
+import org.vamsas.objects.core.User;
+import org.vamsas.objects.core.VAMSAS;
+import org.vamsas.objects.core.VamsasDocument;
+import org.vamsas.objects.utils.AppDataReference;
+import org.vamsas.test.objects.Core;
+
+/**
+ * Maintains a collection of vamsas objects, appdatas and states, and provides api for a SimpleClient's client.
+ * @author jimp 
+ */
+public class ClientDocument extends org.vamsas.client.ClientDocument implements IClientDocument {
+  private static Log log = LogFactory.getLog(ClientDocument.class);
+  private VamsasDocument doc;
+  protected SimpleClient sclient;
+  protected VamsasArchive archive = null;
+  /**
+   * indicate if new data has been incorporated
+   */
+  private boolean isModified = false;
+  /**
+   * Public method for internal use by SimpleClient.
+   * @return true if document has been modified.
+   */
+  public boolean isModified() {
+    return isModified;
+  }
+  private Vector updatedObjects=null;
+  /**
+   *
+   *  prepare Application-side dataset from the vamsas Document archive
+   * @param doc - the dataset
+   * @param docHandler - the sessionFile IO handler
+   * @param Factory - the source of current and new vorbaIds
+   * @param sclient - the simpleclient instance
+   */
+  protected ClientDocument(VamsasDocument doc, VamsasArchive docHandler, IdFactory Factory, SimpleClient sclient) {
+    super(Factory.getVorbaIdHash(), Factory);
+    
+    
+    /**
+     * prepare Application-side dataset from the vamsas Document archive
+     */
+    this.sclient = sclient;
+    archive = docHandler;
+    this.doc = doc;
+    updatedObjects=null;  /// TODO: correct this line
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClientDocument#getObject(org.vamsas.client.VorbaId)
+   */
+  public Vobject getObject(VorbaId id) {
+    if (vamsasObjects==null) {
+      log.debug("getObject called on null objrefs list.");
+      return null;
+    }
+    if (vamsasObjects.containsKey(id))
+      return (Vobject) vamsasObjects.get(id);
+    log.debug("Returning null Vobject reference for id "+id.getId());
+    return null;
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClientDocument#getObjects(org.vamsas.client.VorbaId[])
+   */
+  public Vobject[] getObjects(VorbaId[] ids) {
+    if (vamsasObjects==null) {
+      log.debug("getObject[]  called on null vamsasObjects list.");
+      return null;
+    }
+    Vobject[] vo = new Vobject[ids.length];
+    for (int i=0,j=ids.length; i<j;i++) 
+      if (vamsasObjects.containsKey(ids[i]))
+        vo[i] = (Vobject) vamsasObjects.get(ids[i]);
+      else
+        log.debug("Returning null Vobject reference for id "+ids[i].getId());
+    return vo;
+  }
+  /**
+   * internal reference to single copy of Document Roots array
+   */
+  private VAMSAS[] _VamsasRoots=null;
+  /*
+   * (non-Javadoc)
+   * LATER: currently there is only one Vector of roots ever passed to client - decide if this is correct (means this is not thread safe and may behave unexpectedly)
+   * @see org.vamsas.client.IClientDocument#getVamsasRoots()
+   */
+  public VAMSAS[] getVamsasRoots() {
+    if (doc==null) {
+      log.debug("Null document for getVamsasRoots(), returning null");
+      return null;
+    }
+    if (archive==null) {
+      // LATER: decide on read-only status of ClientDocument object
+      log.warn("getVamsasRoots() called on possibly read-only document.");
+    }
+    if (_VamsasRoots!=null)
+      return _VamsasRoots;
+    VAMSAS[] roots = doc.getVAMSAS();
+    if (roots == null) {
+      // Make a new one to return to client to get filled. 
+      _VamsasRoots = new VAMSAS[] { new VAMSAS() };
+      // Do provenance now. just in case.
+      doc.getProvenance().addEntry(sclient.getProvenanceEntry("Created new document root [id="+_VamsasRoots[0].getId()+"]"));
+      doc.addVAMSAS(_VamsasRoots[0]);
+    } else {
+      _VamsasRoots = new VAMSAS[roots.length];
+      for (int r=0;r<roots.length; r++)
+        _VamsasRoots[r] = roots[r];
+    }
+    return _VamsasRoots;
+  }
+  
+  private int _contains(VAMSAS root, VAMSAS[] docRoots) {
+    if (root==null)
+      return -1;
+    if (docRoots==null || docRoots.length==0)
+      return -1;
+    String r_id = root.getId();
+    for (int i=0,j=docRoots.length; i<j; i++)
+      if (docRoots[i]==root || (docRoots[i]!=null && docRoots[i].getId().equals(r_id)))
+        return i;
+    return -1;
+  }
+/**
+ * verify that newr version is really an intact version of the 
+ * @param newVersion (may be modified)
+ * @param oldVersion 
+ * @return true if newVersion is a valid root that preserves original references
+ */
+  private boolean isValidUpdate(VAMSAS newVersion, final VAMSAS oldVersion) {
+    // ideal - this cascades down the two structures, ensuring that all ID'd objects in one are present in the other.
+    if (oldVersion==newVersion) {
+      // may be a virgin root element.
+      if (!newVersion.isRegistered())
+        _registerObject(newVersion);
+      // Should retrieve original version and compare - unless local hashes can be used to determine if resultSet has been truncated.
+      // just do internal validation for moment.
+      if (newVersion.isValid())
+        return true;
+      return false;
+    } else {
+      // redundant ? if (oldVersion.is__stored_in_document())
+      if (!newVersion.isRegistered())
+        _registerObject(newVersion);
+      if (newVersion.isValid())
+        return true;
+    }
+    return false;
+    /**
+     * LATER isValidUpdate : Ideally. we efficiently walk down, comparing hashes, to deal with merging and verifying provenance for objects
+    
+    // extract root objects
+    if (newroots != null) {
+      // check newroots for objects that were present in the old document
+      // check to see if the 'old' objects have been modified
+      // if they have ? we overwrite them with their new version, ensuring that
+      // provenance is updated.
+      // if they haven't ? do nothing ?
+      
+      for (int i = 0, k = newroots.length; i < k; i++) {
+        if (newroots[i].isRegistered()) {
+          // easy - just check if anything has changed and do provenance
+          Vobject oldversion = getObject(newroots[i].getVorbaId());
+          if (oldversion instanceof VAMSAS) {
+            // LATER: appropriate merging behaviour when two clients have improperly modified the same Vobject independently.
+            if (newroots[i].get__last_hash() != newroots[i].hashCode()) {
+              // client has modified this Vobject since last retrieval.
+              if (newroots[i].get__last_hash() != oldversion.get__last_hash()) {
+                // Vobject has been modified by another client since this
+                // client's
+                // last access to document.
+              }
+            }
+          } else {
+            throw new Error(
+                "SimpleClient error when using setVamsasRoots : The vorbaId for Vobject "
+                + i
+                + " does not refer to an Vobject of type VAMSAS in the current document!");
+          }
+        } else {
+          if (!newroots[i].is__stored_in_document()) {
+            // check if Vobject is modified
+            if (newroots[i].get__last_hash() != newroots[i].hashCode()) {
+              // it is - so we add newroots[i] as a new Vobject, with updated
+              // provenance.
+            } else {
+              // do nothing
+              newroots[i] = null;
+            }
+          } else {
+            // just add newroots[i] as a new Vobject in the document
+            // - with appropriate provenance.
+          }
+        }
+      }*/
+  }
+  /**
+   * merge old and new root vectors
+   * @param newr This array may be written to
+   * @param original
+   * @param the client document (usually this) which this root set belongs to.
+   * @return merged vector of vamsas roots
+   */
+  private VAMSAS[] _combineRoots(VAMSAS[] newr, final VAMSAS[] original, ClientDocument modflag) {
+    Vector rts = new Vector();
+    boolean modified=false;
+    for (int i=0,j=original.length; i<j; i++) {
+      int k = _contains(original[i], newr);
+      if (k>-1) {
+        if (isValidUpdate(newr[k], original[i])) {
+          modified=true;
+          rts.add(newr[k]);
+          newr[k]=null;
+        } else {
+          // LATER: try harder to merge ducument roots.
+          log.warn("Couldn't merge new VAMSAS root "+newr[k].getId());
+          newr[k] = null; // LATER: this means we ignore mangled roots. NOT GOOD
+        }
+      } else {
+        // add in order.
+        rts.add(original[i]);
+      }
+    }
+    // add remaining (new) roots
+    for (int i=0,j=newr.length; i<j; i++) {
+      if (newr[i]!=null) {
+        rts.add(newr[i]);
+        modified=true;
+      }
+    }
+    newr = new VAMSAS[rts.size()];
+    for (int i=0,j=rts.size(); i<j; i++)
+      newr[i] = (VAMSAS) rts.get(i);
+    if (modflag!=null)
+      modflag.isModified = modified;
+    return newr;
+  }
+  
+  /**
+   * update the document with new roots.
+   * LATER: decide: this affects the next call to getVamsasRoots()
+   * @see org.vamsas.IClientDocument.setVamsasRoots
+   */
+  public void setVamsasRoots(VAMSAS[] newroots) {
+    if (doc==null) {
+      log.debug("setVamsasRoots called on null document.");
+      return;
+    }
+    VAMSAS[] newr;
+    if (newroots==null) {
+      log.debug("setVamsasRoots(null) - do nothing.");
+      return;
+    }
+    // are we dealing with same array ?
+    if (_VamsasRoots!=newroots) {
+      // merge roots into local version.
+      newr = new VAMSAS[newroots.length];
+      for (int i=0;i<newr.length;i++)
+        newr[i] = newroots[i];
+      newr=_combineRoots(newr,_VamsasRoots,this);
+    } else {
+      newr = new VAMSAS[_VamsasRoots.length];
+      for (int i=0;i<newr.length;i++)
+        newr[i]=_VamsasRoots[i];
+    }
+    //  actually compare with document root set for final combination (to ensure nothing is lost)
+    _VamsasRoots = _combineRoots(newr, doc.getVAMSAS(), this); 
+  }
+  
+  
+  /* (non-Javadoc)
+   * LATER: decide: this affects the next call to getVamsasRoots()
+   * @see org.vamsas.client.IClientDocument#addVamsasRoot(org.vamsas.objects.core.VAMSAS)
+   */
+  public void addVamsasRoot(VAMSAS newroot) {
+    if (doc==null) {
+      log.debug("addVamsasRoots called on null document.");
+      return;
+    }
+    VAMSAS[] newroots = _combineRoots(new VAMSAS[] {newroot}, _VamsasRoots, this);
+    _VamsasRoots = newroots;  
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClientDocument#registerObjects(org.vamsas.client.Vobject[])
+   */
+  public VorbaId[] registerObjects(Vobject[] unregistered) {
+    if (doc==null) {
+      log.warn("registerObjects[] called on null document.");
+      return null;
+    }
+    if (vamsasObjects==null) {
+      log.warn("registerObjects[] called for null vamsasObjects hasharray.");
+      return null;
+    }
+    if (unregistered!=null) {
+      VorbaId ids[] = new VorbaId[unregistered.length];
+      for (int i=0,k=unregistered.length; i<k; i++)
+        if (unregistered[i]!=null) {
+          log.warn("Null Vobject passed to registerObject[] at position "+i);
+          return null;
+        } else {
+          ids[i]=registerObject(unregistered[i]);
+        }
+      log.debug("Registered "+unregistered.length+" objects - total of "+vamsasObjects.size()+" ids.");
+      return ids;
+    }
+    return null;
+  }
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientDocument#registerObject(org.vamsas.client.Vobject)
+   */
+  public VorbaId registerObject(Vobject unregistered) {
+    if (doc==null) {
+      log.warn("registerObjects called on null document.");
+      return null;
+    }
+    if (vamsasObjects==null) {
+      log.warn("registerObjects called for null vamsasObjects hasharray.");
+      return null;
+    }
+    if (unregistered!=null) {
+      VorbaId id = _registerObject(unregistered);
+      log.debug("Registered object - total of "+vamsasObjects.size()+" ids.");
+      return id;
+    }
+    log.warn("Null Vobject passed to registerObject.");
+    return null;
+  }
+  /**
+   * IClientAppdata instance - if it exists.
+   */
+  SimpleClientAppdata scappd = null;
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientDocument#getClientAppdata()
+   */
+  public IClientAppdata getClientAppdata() {
+    if (doc==null) {
+      log.warn("getClientAppdata called on null document.");
+      return null;
+    }
+    if (scappd==null) {
+      log.debug("Creating new SimpleClientAppdata instance for "+sclient.getSessionHandle());
+      scappd = new SimpleClientAppdata(this);
+      if (scappd==null) {
+        // LATER: may not need this as a warning message.
+        log.warn("Null appdata object for "+sclient.getSessionHandle());
+      } else {
+        log.debug("Created SimpleClientAppdata successfully.");
+      }
+    } else {
+      log.debug("Returning existing SimpleClientAppdata reference.");
+    }
+    return scappd;
+  }
+  /**
+   * access the vamsas document
+   * @return the session's vamsas document
+   */
+  protected VamsasDocument getVamsasDocument() {
+    return doc;
+  }
+  /**
+   * returns the read-only IO interface for the vamsas document Jar file
+   * @return
+   */
+  protected VamsasArchiveReader getVamsasArchiveReader() {
+    try {
+      return archive.getOriginalArchiveReader();
+    } catch (Exception e) {
+      log.warn("Unable to create OriginalArchiveReader!", e);
+    }
+    return null;
+  }
+  protected boolean updateSessionDocument() throws java.io.IOException {
+    boolean docupdate = true; // 'non-serious' problems below set this false
+    if (doc==null) {
+      log.warn("updateSessionDocument called on null document.");
+      throw new java.io.IOException("Document is closed.");
+    }
+    if (archive==null) {
+      log.warn("updateSessionDocument called document archive handler.");
+      throw new java.io.IOException("Document is closed.");
+    }
+    
+    if (!isModified() && !scappd.isModified()) {
+      log.debug("Document update not necessary. returning false.");
+      return false;
+    }
+    VamsasSession session = sclient._session;
+    log.debug("updating Session Document in "+session.sessionDir);
+    // update the VamsasDocument structure with any new appData's.
+    // try to update the sessionFile
+    log.debug("Attempting to update session "+sclient.session.getSessionUrn());
+    if (scappd.isModified()) {
+      ClientHandle client = sclient.client;
+      UserHandle user = sclient.user;
+      scappd.closeForWriting();      
+      if (scappd.appsGlobal==null) {
+        log.debug("Creating new appData entry for this application...");
+        // first write for this application - add a new section in document
+        ApplicationData appd = scappd.appsGlobal = new ApplicationData();
+        appd.setName(client.getClientName());
+        // appd.setUrn(client.getClientUrn());
+        appd.setVersion(client.getVersion());
+        doc.addApplicationData(appd);
+        // embed or jarEntry ?  - for now only jarEntry's are dealt with.
+        appd.setDataReference(AppDataReference.uniqueAppDataReference(doc, sclient.client.getClientUrn()));
+        log.debug("... created.");
+      }
+      if (scappd.newAppData!=null && scappd.newAppData.sessionFile.exists()) {
+        log.debug("Beginning update for new Global Appdata...");
+        // new global appdata to write.
+        if (scappd.appsGlobal.getData()!=null) {
+          scappd.appsGlobal.setData(null);
+          scappd.appsGlobal.setDataReference(AppDataReference.uniqueAppDataReference(doc, sclient.client.getClientUrn()));
+        }
+        // LATER: use a switch to decide if the data should be written as a reference or as an embedded data chunk
+        scappd.updateAnAppdataEntry(archive, scappd.appsGlobal, scappd.newAppData);
+        log.debug("...Successfully updated Global Appdata Entry.");
+      }
+      if (scappd.newUserData!=null && scappd.newUserData.sessionFile.exists()) {
+        log.debug("Beginning to update Users Appdata entry....");
+        if (scappd.usersData==null) {
+          // create new user appdata
+          scappd.usersData = new User();
+          scappd.usersData.setFullname(user.getFullName());
+          scappd.usersData.setOrganization(user.getOrganization());
+          scappd.appsGlobal.addUser(scappd.usersData);
+        }
+        User appd = scappd.usersData;
+        if (appd.getData()!=null || appd.getDataReference()==null) {
+          // LATER make standard appDataReference constructor for client+user 
+          appd.setData(null);          
+          String safe_username = user.getFullName();
+          int t = safe_username.indexOf(" ");
+          if (t!=-1) {
+            safe_username = safe_username.substring(t);
+          }
+          appd.setDataReference(AppDataReference.uniqueAppDataReference(doc, sclient.client.getClientUrn()+safe_username));
+        }
+        scappd.updateAnAppdataEntry(archive, scappd.usersData, scappd.newUserData);
+        log.debug("...Successfully updated user AppData entry.");
+      }
+    }
+    log.debug("Updating Document...");
+    // now update the document.
+    try {
+      archive.putVamsasDocument(doc);
+      log.debug("Successfully written document entry.");
+    }
+    catch (Exception e) {
+      log.error("Marshalling error for vamsas document.",e);
+      docupdate = false; // pass on the (probable) object validation error 
+    }
+    if (archive.transferRemainingAppDatas())
+      log.debug("Remaining appdatas were transferred.");
+    else
+      log.debug("No remaining appdatas were transferred. (Correct?)");
+    archive.closeArchive();
+    log.debug("...successully finished and closed.");
+    return docupdate; // no errors ?
+  }
+  /* (non-Javadoc)
+   * @see java.lang.Object#finalize()
+   */
+  protected void finalize() throws Throwable {
+    log.debug("Garbage collecting on ClientDocument instance.");
+    if (scappd!=null) {
+      scappd.finalize();
+      scappd = null;
+    }
+    if (doc!=null) {
+      doc = null;
+    }
+    // disengage from client
+    if (sclient!=null)
+      sclient.cdocument = null;
+    sclient=null;
+    
+    super.finalize();
+  }
+  public Vector getUpdatedObjects() {
+    return updatedObjects;
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/ClientsFile.java b/src/uk/ac/vamsas/client/simpleclient/ClientsFile.java
new file mode 100644 (file)
index 0000000..89d5b41
--- /dev/null
@@ -0,0 +1,295 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import org.vamsas.client.*;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.util.Vector;
+
+/**
+ * Handler for the clientsFile within a vamsas session thread.
+ * @author jim 
+ */
+public class ClientsFile extends ListFile {
+  private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ClientsFile.class);
+  /**
+   * number of my client in list - passed back when a client 
+   * is added to list, and used (if valid) for quickly 
+   * looking up presence of client handle in the list.
+   */
+  private int syncnum = 1;
+
+  public ClientsFile(File filelist) throws IOException {
+    super(filelist);
+  }
+
+  /**
+   * internal method for getting clientList - ensures a lock has been made but
+   * does not release it.
+   * 
+   * @return list of clients
+   */
+  private ClientHandle[] retrieveClientHandles() {
+    if (lockFile()) {
+      try {
+        ClientHandle[] clients=null;
+        if (fileLock.length()>0) {
+          
+          ObjectInputStream is = new ObjectInputStream(fileLock.getBufferedInputStream(true));
+          Object o;
+          o=is.readObject();
+          if (o!=null) {
+            try {
+              clients = (ClientHandle[]) o;
+            }
+            catch (Exception e) {
+              System.err.println("Garbage in the clientHandle list "+this.sessionFile);
+            }
+          }
+        }
+        return clients;
+      } catch (FileNotFoundException e) {
+        // TODO Auto-generated catch block
+        e.printStackTrace(System.err);
+      } catch (Exception e) {
+        e.printStackTrace(System.err);
+      }
+    }
+    return null;
+  }
+  /**
+   * get the clientList from the file. May return null if lock failed!
+   * @return clientList
+   */
+  public ClientHandle[] retrieveClientList() {
+    if (lockFile()) {
+      ClientHandle[] clients = retrieveClientHandles();
+      unlockFile();
+      return clients;
+    }
+    return null;
+  }
+  /**
+   * get list from the locked ClientList.
+   * @param extantlock
+   * @return clientList or null if lock failed (or file was empty)
+   */
+  public ClientHandle[] retrieveClientList(Lock extantlock) {
+    if (lockFile(extantlock)) {
+      ClientHandle[] clients = retrieveClientHandles();
+      unlockFile();
+      return clients;
+    }
+    return null;
+  }
+  /**
+   * adds clientHandle me to the clientList under an existing lock extantLock.
+   * @param me
+   * @param extantLock
+   * @return client index in list or 0 if lock was invalid or addClient operation failed.
+   */
+  public int addClient(ClientHandle me, Lock extantLock) {
+    return addClient(me, true, extantLock);
+  }
+  
+  /**
+   * adds clientHandle me to the clientList under an existing lock.
+   * @param me - clientHandle
+   * @param disambig - if true then add will fail if an identical clientHandle already exists
+   * @param extantLock - existing lock
+   * @return client index in list or 0 if addClient (or the lock) failed.
+   */
+  
+  public int addClient(ClientHandle me, boolean disambig, Lock extantLock) {
+    if (lockFile(extantLock)) {
+      syncnum = addClient(me, disambig);
+      unlockFile();
+      return syncnum;
+    }
+    return 0;
+  }
+  
+  /**
+   * adds the ClientHandle to the list - if it is not unique, then the
+   * ClientHandle object is modified to make it unique in the list. returns the
+   * clientNumber for the client in the session.
+   * 
+   * @param me
+   * @return
+   */
+
+  public int addClient(ClientHandle me) {
+    syncnum = addClient(me, true);
+    unlockFile();
+    return syncnum;
+  }
+
+  /**
+   * removes 'me' from the session ClientList without complaint if 'me' isn't in the clientList already.
+   * @param me client handle to be removed
+   * @param clientlock existing lock passed from watcher.
+   */
+  public void removeClient(ClientHandle me, Lock clientlock) {
+    int mynum=-1;
+    if (lockFile(clientlock)) {
+      ClientHandle[] clients = retrieveClientHandles();
+      if (clients != null) {
+        if ((syncnum<=0 || syncnum>clients.length) || clients[syncnum-1]!=me) {
+          for (int i = 0, j = clients.length; i < j; i++) 
+            if (clients[i].equals(me)) {
+              mynum=i;
+              break;
+            }
+        } else {
+          mynum=syncnum-1;
+        }
+        if (mynum>-1) {
+          ClientHandle[] newlist = new ClientHandle[clients.length - 1];
+          for (int k=0,i = 0, j = clients.length; i < j; i++)
+            if (i!=mynum)
+              newlist[k++] = clients[i];
+          if (!putClientList(newlist))
+            throw new Error("Failed to write new clientList!"); // failed to put the clientList to disk.
+        }
+      }
+      unlockFile();
+    } else {
+      throw new Error("Couldn't get lock for "+((sessionFile==null) ? "Unitialised sessionFile in ClientsFile" : sessionFile.getAbsolutePath()));
+    }
+  }
+  /**
+   * Adds a ClientHandle to the ClientList file - optionally disambiguating 
+   * the ClientHandle (modifes the URN). 
+   * Note: Caller is left to release the lock on the ClientList.
+   * @param me
+   * @param disambiguate -
+   *          flag indicating if the URN for me should be disambiguated to
+   *          differentiate between sessions.
+   * @return index of clientHandle in new list, or -1-position of existing
+   *         clientHandle (if disambiguate is true)
+   */
+  protected int addClient(ClientHandle me, boolean disambiguate) {
+    int newclient = 0;
+    int tries=5;
+    while (tries-->0 && !lockFile())
+      try { Thread.sleep(1); } catch (Exception e){};
+    if (lockFile()) {
+      ClientHandle[] clients = retrieveClientHandles();
+      if (me.getClientUrn()==null) {
+        // TODO: move this into ClientUrn as a standard form method.
+        me.setClientUrn("vamsas://"+me.getClientName()+":"+me.getVersion()+"/");
+      }
+      if (clients == null) {
+        clients = new ClientHandle[1];
+        clients[0] = me;
+        newclient = 1;
+      } else {
+        int k = 0;
+        for (int i = 0, j = clients.length; i < j; i++) {
+          if (clients[i].equals(me)) {
+            if (disambiguate) {
+              while (clients[i].equals(me)) {
+                me.setClientUrn(me.getClientUrn() + k++); // TODO: make a better
+                                                          // disambiguation of
+                                                          // urn.
+              }
+            } else {
+              // will not write the ambiguous clientHandle to disk, just return
+              // its index.
+              return -1 - i;
+            }
+          }
+        }
+        int i, j;
+        ClientHandle[] newlist = new ClientHandle[clients.length + 1];
+        for (i = 0, j = clients.length; i < j; i++)
+          newlist[i] = clients[i];
+        newlist[j] = me;
+        clients = newlist;
+        newclient = j+1;
+      }
+      if (!putClientList(clients))
+        return 0; // failed to put the clientList to disk.
+    }
+    return newclient;
+  }
+  /**
+   * when set true - get FileNotFoundExceptions on WinXP when writing to locked stream after the backup has been made (via the backupFile method)
+   */
+  boolean backup=false;
+  /**
+   * safely writes clients array to the file referred to by sessionFile.
+   * 
+   * @param clients
+   * @return true if successful write. Throws Errors otherwise.
+   */
+  protected boolean putClientList(ClientHandle[] clients) {
+    if (lockFile()) {
+      File templist=null;
+      if (!backup || (templist = backupSessionFile()) != null) {
+        int retries=3;
+        while (retries-->0) {
+          try {
+            ObjectOutputStream os = 
+              new ObjectOutputStream(fileLock.getBufferedOutputStream(true));
+            log.debug("About to write "+clients.length+" clientHandles to output stream.");
+            os.writeObject(clients);
+            os.close();
+          // All done - remove the backup.
+          if (backup)
+            templist.delete();
+          templist = null;
+          retries=-1;
+          } catch (Exception e) {
+            System.err
+            .println("Serious - problems writing to sessionFile.");
+            if (retries>0 && templist != null) {
+              System.err.println("Recovering from Backup in "
+                      + templist.getAbsolutePath());
+              templist.renameTo(fileLock.target);
+            }
+            e.printStackTrace(System.err);
+          }
+        }
+        if (retries>-2) {
+          System.err
+          .println("Serious - problems writing to sessionFile. Giving Up.");
+          return false;
+        }
+      } else {
+        throw new Error(
+            "Couldn't create backup of the clientList before writing to it!");
+      }
+    } else {
+      throw new Error("Could not lock the clientList: "
+          + ((sessionFile == null) ? "Unitialized ClientsFile"
+              : " failed to get lock on " + sessionFile.getAbsolutePath()));
+    }
+    // successful!
+    return true;
+  }
+
+  public void clearList() {
+    if (lockFile()) {
+      try {
+        FileOutputStream fout = fileLock.getFileOutputStream(true);
+        fout.flush();
+        fout.close();
+      } catch (Exception e) {
+        throw new Error("Problems trying to clear clientlist!",e);
+        
+      }
+    }
+    
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/EventGeneratorThread.java b/src/uk/ac/vamsas/client/simpleclient/EventGeneratorThread.java
new file mode 100644 (file)
index 0000000..8d028ef
--- /dev/null
@@ -0,0 +1,277 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.Hashtable;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.Events;
+
+/**
+ * monitors watcher objects and generates events.
+ */
+public class EventGeneratorThread extends Thread implements Runnable {
+  private static Log log = LogFactory.getLog(EventGeneratorThread.class);
+  private SimpleClient client;
+  private Hashtable handlers; // manager object
+  private VamsasSession session;
+
+  /** 
+   * list with all the clientHandles for the session
+   */
+  protected FileWatcher clientfile=null;
+  /**
+   * the session's vamsasDocument
+   */
+  protected FileWatcher vamsasfile=null;
+  /**
+   * written to by client when its app calls storeDocument.
+   */
+  protected FileWatcher storeFile=null;
+  
+  private boolean watch=false;
+  
+  
+  EventGeneratorThread(VamsasSession s, SimpleClient _client, Hashtable eventhandlers) {
+    if (eventhandlers==null || s==null || _client==null)
+      throw new Error("Null arguments to EventGeneratorThread constructor.");
+    handlers = eventhandlers;
+    session = s;
+    client = _client;
+    setName(s.sessionDir.getName());
+    initWatchers();
+  }
+  
+  private void initWatchers() {
+    if (clientfile==null)
+      clientfile = session.getClientWatcher();
+    if (vamsasfile ==null)
+      vamsasfile = session.getDocWatcher();
+    if (storeFile == null)
+      storeFile = session.getStoreWatcher();
+    clientfile.setState();
+    vamsasfile.setState();
+    storeFile.setState();
+  }
+  boolean ownsf = false;
+  /**
+   * scans all watchers and fires changeEvents if necessary
+   * @return number of events generated.
+   */
+  private int checkforEvents() {
+    Lock watchlock;
+    //TODO : leave slog.info messages for the events that occur.
+    int raised=0;
+    // could make this general - but for now keep simple
+    if ((watchlock=storeFile.getChangedState())!=null) {
+      // TODO: define the storeFile semaphore mechanism : file exists - all clients inform their apps, and then the client that wrote the file should delete the file (it should hold the lock to it).
+      if (storeFile.exists) { 
+        PropertyChangeSupport h = (PropertyChangeSupport) handlers.get(Events.DOCUMENT_FINALIZEAPPDATA);
+        if (h!=null) {
+          log.debug("Triggering DOCUMENT_FINALIZEAPPDATA");
+          raised++;
+          h.firePropertyChange(client.getSessionUrn(), null, client);
+          // expect client to 
+          vamsasfile.setState();
+        }
+      }
+    }
+    if ((watchlock=clientfile.getChangedState())!=null) {
+      // see what happened to the clientfile - compare our internal version with the one in the file, or just send the updated list out...?
+      //
+      /**
+       * Generated when a new vamsas client is attached to a session (Handle is
+       * passed) Note: the newly created client does not receive the event.
+       *
+      public static final String CLIENT_CREATION = "org.vamsas.client.events.clientCreateEvent";
+  */ // as the test
+      /**
+       * Generated when a vamsas client leaves a session (Handle is passed to all
+       * others).
+      public static final String CLIENT_FINALIZATION = "org.vamsas.client.events.clientFinalizationEvent";
+       */ // again - as the test.
+          raised++;
+    }
+    if ((watchlock=vamsasfile.getChangedState())!=null) {
+      
+      /**
+       * Generated when a client has finished updating the document. Passes
+       * applicationHandle of client so the updating client can recognise its own
+       * updates.
+      public static final String DOCUMENT_UPDATE = "org.vamsas.client.events.documentUpdateEvent";
+       */
+      // read apphandle from 'lastUpdate' session file.
+      // pass apphandle name to appHandler ?
+      
+    }
+    /**
+     * Generated when a new vamsas document is created (perhaps from some existing
+     * Vamsas data) so an application may do its own data space initialization.
+     * TODO: decide if this is called when an app is connected to a stored
+     * session...
+     public static final String DOCUMENT_CREATE = "org.vamsas.client.events.documentCreateEvent";
+    */
+    // check if this session's appInit flag is set - if not - generate event for this app.
+    // prolly don't need this at the moment - when an app does getDocument it can to the initing then.
+    
+
+    /**
+     * Generated prior to session Shutdown, after the last participating vamsas
+     * client has finalized.
+     *  TODO: decide on purpose of this ?  is this for benefit of multi-session Apps only ?
+    public static final String SESSION_SHUTDOWN = "org.vamsas.client.events.SessionShutdownEvent";
+     */
+
+    /**
+     * Generated for all clients when any client calls IClient.storeDocument() to
+     * allow them to store any updates before an offline copy of the session is
+     * created. Any client that handles this should call the
+     * IClient.getDocument(), update and then IClient.updateDocument in the same
+     * handler thread.
+     * EventName: <Vamsas-session URN>
+     * NewValue: org.vamsas.client.IClient for session.
+     *
+    public static final String DOCUMENT_FINALIZEAPPDATA = "org.vamsas.client.events.DocumentFinalizeAppData";
+*/
+    // watch for finalization semaphore (last finalised sessionFile).
+    
+    /**
+     * Generated by Vorba stub after the penultimate client makes a call to
+     * closeDocument(). Sequence is as follows : 1. All other vamsas clients have
+     * called closeDocument() 2. Final living client monitors closures, and
+     * realises that it is last. 3. Final client generates event to prompt
+     * associated application to inquire if the user wishes to save the document
+     * for future reference.
+     *  * Any call to closeDocument in a thread other than the registered
+     * EventListener will block until the RequestToClose handler has exited.
+     * 
+     */
+    //    public static final String DOCUMENT_REQUESTTOCLOSE = "org.vamas.client.DocumentRequestToCloseEvent";
+
+    return raised;
+  }
+  
+  private void initEvents() {
+    
+  }
+  /**
+   * Events raised by IClient and propagated to others in session
+   */
+  
+  /**
+   * number of milliseconds between any file state check.
+   */
+  long POLL_UNIT = 20;
+  protected void wait(int u) {
+    if (u<=0)
+      u=1;
+    long l = System.currentTimeMillis()+POLL_UNIT*u;
+      while (System.currentTimeMillis()<l)
+        ;
+  }
+    
+  
+  private boolean block_document_updates=false;
+  int STORE_WAIT=5; // how many units before we decide all clients have finalized their appdatas
+  
+  /**
+   * client App requests offline storage of vamsas data. 
+   * Call blocks whilst other apps do any appData finalizing
+   * and then returns (after locking the vamsasDocument in the session)
+   * Note - the calling app may also receive events through the EventGeneratorThread for document updates.
+   * 
+   * @return Lock for session.vamArchive 
+   * @param STORE_WAIT indicates how lock the call will block for when nothing appears to be happening to the session.
+   */
+  protected Lock want_to_store() {
+    log.debug("Setting flag for document_update requests to be ignored");
+    block_document_updates=true;
+    log.debug("Waiting for other apps to do FinalizeApp handling.");
+    try {
+      session.addStoreDocumentRequest(client.getClientHandle(), client.getUserHandle());
+    } catch (Exception e) {
+      log.warn("Whilst writing StoreDocumentRequest for "+client.getClientHandle().getClientUrn()+" "+client.getUserHandle(),
+          e);
+      log.info("trying to continue.");
+    }
+    // LATER: refine this semaphore process 
+    // to make a robust signalling mechanism:
+    // app1 requests, app1..n do something (or don't - they may be dead), 
+    // app1 realises all apps have done their thing, it then continues with synchronized data.
+    // this probably needs two files - a request file, 
+    //  and a response file which is acknowledged by the app1 requestor for each app.
+    //  eventually, no more responses are received for the request, and the app can then only continue with its store.
+    int units = 0;
+    while (units<STORE_WAIT) {
+      wait(1);
+      if (storeFile.hasChanged() || vamsasfile.hasChanged())
+        units=0;
+      else
+        units++;
+    }
+    
+    block_document_updates=false;
+    log.debug("Cleared flag for ignoring document_update requests");
+    // wait around again (until our own watcher has woken up and synchronized).
+    while (units<STORE_WAIT) {
+      wait(1);
+      if (storeFile.hasChanged() || vamsasfile.hasChanged())
+        units=0;
+      else
+        units++;
+    }
+    
+    
+    log.debug("finished waiting.");
+    return session.vamArchive.getLock();
+  }
+  /**
+   * count handlers for a particular vamsas event 
+   * @param event string enumeration from org.vamsas.client.Events
+   * @return -1 for an invalid event, otherwise the number of handlers
+   */
+  protected int countHandlersFor(String event) {
+    if (handlers.containsKey(event)) {
+      PropertyChangeSupport handler = (PropertyChangeSupport) handlers.get(event);
+      PropertyChangeListener[] listeners;
+      if (handler!=null)
+        return ((listeners=handler.getPropertyChangeListeners())==null) 
+        ? -1 : listeners.length;
+    }
+    return -1;
+  }
+  /**
+   * probably don't need any of these below.
+   */
+  /* (non-Javadoc)
+   * @see java.lang.Thread#destroy()
+   */
+  public void destroy() {
+    super.destroy();
+  }
+  /* (non-Javadoc)
+   * @see java.lang.Thread#interrupt()
+   */
+  public void interrupt() {
+    // TODO Auto-generated method stub
+    super.interrupt();
+  }
+  /* (non-Javadoc)
+   * @see java.lang.Thread#isInterrupted()
+   */
+  public boolean isInterrupted() {
+    // TODO Auto-generated method stub
+    return super.isInterrupted();
+  }
+  /* (non-Javadoc)
+   * @see java.lang.Thread#run()
+   */
+  public void run() {
+    // TODO Auto-generated method stub
+    super.run();
+  }
+  
+
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/FileLock.java b/src/uk/ac/vamsas/client/simpleclient/FileLock.java
new file mode 100644 (file)
index 0000000..4ab0a65
--- /dev/null
@@ -0,0 +1,205 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+/**
+ * File based Locking mechanism to get around some bizarre limitations of JarEntry seeking.
+ * Abstract locks have a target file, to which access is controlled when a lock is held. Native locks on WindowsXP seem to conflict with Jar seek operations, so a file lock creates an advisory lock.
+ * Method:
+ * A lock file is created, if it doesn't already exist - the naming convention is TargetFile+suffixSeparator+_LockSuffix.
+ * A lock is obtained by locking the lock file with a native lock. The NativeLock is used for this.
+ * @author JimP
+ *
+ */
+public class FileLock extends Lock {
+  private File _lock = null;
+  protected static String _LockSuffix="lck";
+  private NativeLock advisory=null;
+  /**
+   * ensure that the _lock file exists
+   * and create a lock
+   */
+  private boolean ensureLockFile(boolean block) {
+    if (_lock==null)
+      return false;
+    if (advisory!=null && advisory.isLocked())
+      return true;
+    try { 
+      advisory=new NativeLock(_lock, block);
+    } catch (Exception e) {
+      log.fatal("Failed to create advisory lock file "+_lock,e);
+      throw new Error("Failed to create advisory lock file "+_lock);          
+    }
+    return advisory.isLocked();
+  }
+  /**
+   * call to clear up a filelock file after its been made
+   *
+   */
+  private void tidy() {
+    if (_lock!=null) { 
+      if ( advisory!=null) 
+        advisory.release(true);
+      advisory.target.deleteOnExit();
+      advisory=null;
+      _lock=null;
+    }
+  }
+  /**
+   * @param lockfile
+   * @param block true means thread blocks until FileLock is obtained.
+   */
+  public FileLock(File lockfile, boolean block) {
+    super(lockfile);
+    // try and get a lock.
+    try {
+      _lock = new File(lockfile.getParentFile(), lockfile.getName()+"."+_LockSuffix);
+      if (!ensureLockFile(block)) {
+        log.debug("Couldn't get lock on "+_lock);
+        tidy();
+        return;
+      }
+      // create target file ready to be written to if necessary.
+      if (!lockfile.exists())
+        if (!lockfile.createNewFile()) {
+          log.warn("Failed to create locked file "+lockfile);
+          return;
+        }
+      //openRaFile();
+    } catch (FileNotFoundException e) {
+      //
+      log.debug("FileLock failed with target="+lockfile+" and lockfile suffix of "+_LockSuffix);
+      //log.error("Error! Couldn't create a lockfile at "
+      //  + lockfile.getAbsolutePath(), e);
+    } catch (IOException e) {
+      log.error("Error! Problems with IO when creating a lock on "
+          + lockfile.getAbsolutePath(),e);
+    }
+  }
+  
+  private boolean openRaFile() throws IOException {
+    if (target==null)
+      return false;
+    if (advisory==null || !advisory.isLocked())
+      return false;
+    if (rafile==null || rafile.getFD()==null || !rafile.getFD().valid()) {
+      rafile=new RandomAccessFile(target,"rw");
+    } else {
+      if (log.isDebugEnabled())
+        log.debug("Reusing existing RandomAccessFile on "+target);
+    }
+    return (rafile.getChannel()!=null) && rafile.getChannel().isOpen();
+  }
+  
+  public boolean isLocked() {
+    if (advisory != null) {
+      if (advisory.isLocked())
+        return true;
+      advisory=null;
+      if (log.isDebugEnabled())
+        log.debug("Lockfile "+_lock+" unexpectedly deleted ?");
+    }
+    return false;
+  }
+  
+  public void release() {
+    release(true);
+  }
+  
+  public void release(boolean closeChannel) {
+    if (!isLocked())
+      return;
+    if (log.isDebugEnabled())
+      log.debug("Releasing advisory lock on "+target);
+    if (closeChannel) {
+      if (rafile!=null) 
+        try {
+          rafile.close();
+        } catch (Exception e) {
+          log.debug("Unexpected exception whilst closing RandomAccessFile on "+target, e);
+        }
+        rafile=null; 
+    }
+    tidy();
+  }
+  
+  public FileInputStream getFileInputStream(boolean atStart) throws IOException {
+    if (!isLocked()) {
+      log.debug("Don't hold lock on "+target+" to get locked FileInputStream.");
+      return null;
+    }
+    openRaFile();
+    if (atStart)
+      rafile.seek(0);
+    return new FileInputStream(rafile.getFD());
+  }
+  
+  
+  public FileOutputStream getFileOutputStream(boolean clear) throws IOException {
+    if (!isLocked()) {
+      log.debug("Don't hold lock on "+target+" to get locked FileOutputStream.");
+      return null;
+    }
+    openRaFile();
+    if (clear) {
+      rafile.seek(0);
+      rafile.setLength(0);
+    } else
+      rafile.seek(rafile.length());
+    return new LockedFileOutputStream(rafile.getFD());
+  }
+  
+  
+  public BufferedOutputStream getBufferedOutputStream(boolean clear) throws IOException {
+    log.debug("Getting BufferedOutputStream (clear="+clear+")");
+    FileOutputStream fos = getFileOutputStream(clear);
+    if (fos!=null)
+      return new BufferedOutputStream(fos);
+    return null;
+  }
+  
+  /* (non-Javadoc)
+   * @see uk.ac.vamsas.client.simpleclient.Lock#getLength()
+   */
+  public long length() {
+    if (isLocked()) {
+      if (!target.exists()) {
+        try {
+          target.createNewFile();
+        } catch (Exception e) {
+          log.error("Invalid lock:Failed to create target file "+target);
+          tidy();
+          return -1;
+        }
+        return 0;
+      }
+      return target.length();
+    }
+    return -1;
+  }
+  protected void finalize() throws Throwable {
+    release(true); // we explicitly lose the lock here.
+    super.finalize();
+  }
+  public RandomAccessFile getRaFile() throws IOException {
+    if (isLocked() && openRaFile()) {
+      return rafile;
+    }
+    log.debug("Failed to getRaFile on target "+target);
+    return null;
+  }
+  public FileChannel getRaChannel() throws IOException {
+    if (isLocked() && openRaFile()) {
+      return rafile.getChannel();
+    }
+    log.debug("Failed to getRaChannel on target "+target);
+    return null;
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/FileWatcher.java b/src/uk/ac/vamsas/client/simpleclient/FileWatcher.java
new file mode 100644 (file)
index 0000000..7b47bb7
--- /dev/null
@@ -0,0 +1,168 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+
+
+/**
+ * Watches a particular file for its creation, deletion, or
+ * modification. The watcher is thread safe and different 
+ * instances watching the state of a particular file carry
+ * their own state record for the file.
+ */
+public class FileWatcher {
+
+  private File subject = null;
+
+  private long lastStat[];
+  boolean waslocked=false;
+  boolean exists = false;
+  /**
+   * transient lock on subject - can be passed back to calling class
+   * to preserve new state of file for immediate reading.
+   */
+  private Lock subjectLock = null;
+  /**
+   * clear local locks on subject.
+   *
+   */
+  private void clearLock() {
+    if (subjectLock!=null)
+      subjectLock.release();
+    subjectLock=null;
+  }
+  /**
+   *  
+   * @return true if subject exists and is locked by another process.
+   */
+  private boolean checkLock() {
+    if (subject!=null && subject.exists()) {
+      if (subjectLock!=null) {
+        subjectLock.release();
+      }
+      subjectLock = LockFactory.tryLock(subject);
+      if (subjectLock.isLocked()) {
+        return false;
+      }
+      clearLock();
+      return true;
+    }
+    return false;
+  }
+  
+  private long[] getStat(File subject) {
+    return new long[] { subject.lastModified(), subject.length() };
+  }
+  private boolean compStat(long[] stat, long[] newstat) {
+    if (stat[0]!=newstat[0] || stat[1]!=newstat[1])
+      return false;
+    return true;
+  }
+  /**
+   * Detect changes in file state and release of any
+   * lock in place during change.
+   * @return true if file state has changed. Leaves lock in subjectLock (ready to be passed to caller)
+   */
+  private boolean check() {
+    if (subject != null) {
+      if (!subject.exists()) {
+        if (exists) {
+          if (!waslocked) {
+            // !checkLock()) {
+          
+            exists = false;
+            // waslocked=false;
+            return true;
+          }
+        }
+        // locked - state change registered after lock is released
+        return false;
+      } else {
+        long[] newStat = getStat(subject); // subject.lastModified();
+        if (!checkLock()) {
+          // file is free to access, return state change
+          if (!exists || !compStat(lastStat, newStat)) {
+            waslocked=false;
+            exists=true;
+            lastStat=newStat;
+            return true;
+          }
+          // no change
+          return false;
+        } else {
+          waslocked=true;
+          return false;
+        }
+      }
+    }
+    return false;
+  }
+  /**
+   * updates internal record of file state when caller has intentionally
+   * modified subject. (ignores locked-state of subject)
+   */
+  public void setState() {
+    if (subject!=null) {
+      if (exists = subject.exists()) {
+        lastStat = getStat(subject);
+        waslocked = false;
+      }
+    }
+  }
+  
+  /**
+   * Make a watcher for a particular file. If the file doesn't exist, the
+   * watcher will watch for its creation (and indicate a change of state) 
+   * For locked files, the removal of a lock constitutes a change of 
+   * state if the file was modified.
+   * 
+   * @param subject
+   */
+  
+  public FileWatcher(File subject) {
+    this.subject = subject;
+    setState();
+  }
+  /**
+   * Test for change in file state. Only indicates a change 
+   * after any lock on a file has been released.
+   * @return true if file has been modified.
+   */
+  public boolean hasChanged() {
+    boolean res = check();
+    clearLock();
+    return res;
+  }
+  /**
+   * passes lock back to caller if hasChanged returned true.
+   * @return
+   */
+  public Lock getChangedState() {
+    boolean res = check();
+    if (res)
+      return subjectLock;
+    else {
+      clearLock();
+      return null;
+    }
+  }
+  /**
+   * safely? getting current state of the watched file
+   * @return
+   */
+  public long[] getCurrentState() {
+    return getStat(subject);
+  }
+  /**
+   * safely compare an externally recorded state with the current state
+   * for significant modifications.
+   * @param a
+   * @param b
+   * @return
+   */
+  public boolean diffState(long[] a, long[] b) {
+    return compStat(a,b);
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/IdFactory.java b/src/uk/ac/vamsas/client/simpleclient/IdFactory.java
new file mode 100644 (file)
index 0000000..3a7f2bb
--- /dev/null
@@ -0,0 +1,159 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.ClientHandle;
+import org.vamsas.client.SessionHandle;
+import org.vamsas.client.UserHandle;
+import org.vamsas.client.VorbaId;
+import org.vamsas.client.VorbaIdFactory;
+import org.vamsas.client.Vobject;
+import org.vamsas.objects.utils.document.VersionEntries;
+
+import java.util.Hashtable;
+import java.util.zip.CRC32;
+/**
+ * Simplest VorbaId constructor
+ * @author jimp
+ *
+ */
+public class IdFactory extends VorbaIdFactory {
+  static Log log = LogFactory.getLog(IdFactory.class);
+  private SessionHandle session=null;
+  private ClientHandle client;
+  private UserHandle user;
+  private CRC32 unique=new CRC32(); // used to attempt a unique but predictable stream for IDs
+  private String idstring;
+  int sequence=1; // incrementing value for next new ID
+  /**
+   * 
+   */
+  public IdFactory() {
+    super();
+    // TODO Auto-generated constructor stub
+  }
+  
+  /**
+   * @param session
+   * @param client
+   * @param user
+   */
+  protected IdFactory(SessionHandle session, ClientHandle client, UserHandle user) {
+    super();
+    this.session = session;
+    this.client = client;
+    this.user = user;
+    unique.reset();
+    unique.update(new Object[] { session, client, user}.toString().getBytes());
+    // TODO: Ensure format of URNs and use standard composition methods.
+    idstring = client.getClientName()+":"+unique.getValue()+".";
+    extantids=new Hashtable();
+    this.extanthashv=new Hashtable();
+  }
+  /**
+   * values for keys in this hash can be used to reference the org.vamsas.client.Vobject instance for the VorbaId string.
+   * @return the hash of all VorbaIds
+   */
+  protected Hashtable getVorbaIdHash() {
+    return extantids;
+  }
+  /**
+   * values for keys in this hash are Vobjhash objects created for each Vobj with a VorbaId
+   * after this factory has been used to write a vamsas archive.
+   * @return the hash of all VorbaIds and their hash values.
+   */
+  protected Hashtable getVobjhashVals() {
+    return extanthashv;
+  }
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#makeVorbaId()
+   */
+  public VorbaId makeVorbaId(Vobject vobject) {
+    if (session==null)
+      throw new Error("makeVorbaId called on improperly initialised IdFactory Vobject!");
+    if (!vobject.isRegisterable())
+      throw new Error("makeVorbaId called on unregisterable object.");
+    if (vobject.isRegistered())
+      throw new Error("makeVorbaId called on already registered object.");
+    String newidstring;
+    do {
+      if (sequence>0) {
+        sequence++;
+      } else {
+        idstring+="1/";
+        sequence=1;
+      }
+      newidstring=idstring+Integer.toString(sequence);
+    } while (extantids.containsKey(newidstring));
+    extantids.put(newidstring, vobject); // hash the Vobject by its new Id
+    VorbaId id = newId(newidstring); // VorbaId.hash()==newidstring.hash() so we can still recover vobject
+    return id;
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#setSession(org.vamsas.client.SessionHandle)
+   */
+  protected void setSession(SessionHandle sessionhandle) {
+    if (sessionhandle!=null)
+      session=sessionhandle;
+    else
+      log.warn("setSession(null) called.");
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#getSessionHandle()
+   */
+  public SessionHandle getSessionHandle() {
+    return session;
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#setClient(org.vamsas.client.ClientHandle)
+   */
+  protected void setClient(ClientHandle appHandle) {
+    if (appHandle!=null)
+      client = appHandle;
+    else
+      log.warn("setClient(null) called.");
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#getClientHandle()
+   */
+  public ClientHandle getClientHandle() {
+    return client;
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#setUser(org.vamsas.client.UserHandle)
+   */
+  protected void setUser(UserHandle userHandle) {
+  if (userHandle!=null)
+    user = userHandle;
+  else
+    log.warn("setUser(null) called.");
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.VorbaIdFactory#getUserHandle()
+   */
+  public UserHandle getUserHandle() {
+    return user;
+  }
+  /**
+   * Convenience method used for default behaviour in testing 
+   * and any anonymous internal vamsasDocument unmarshalling
+   * @param clientname
+   * @return
+   */
+  protected  static IdFactory getDummyFactory(String clientname) {
+    if (clientname==null)
+      clientname="uk.ac.vamsas.client.simpleclient.IdFactory";
+    return new IdFactory(new SessionHandle("dummy.session"), 
+        new ClientHandle(clientname,VersionEntries.latestVersion()), 
+        new UserHandle(clientname, "Arnold User's Inc."));
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/ListFile.java b/src/uk/ac/vamsas/client/simpleclient/ListFile.java
new file mode 100644 (file)
index 0000000..29bb29e
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+
+
+/**
+ * base class for generic list storage and retrieval operations from a locked IO file
+ *  TODO: LATER: generalize for all list storage and retrieval operations 
+ *  (pull-up from ClientsFile object)
+ */
+public class ListFile extends SessionFile {
+
+  /**
+   * @param file
+   */
+  public ListFile(File file) throws java.io.IOException {
+    super(file);
+    if (!this.sessionFile.exists())
+      this.sessionFile.createNewFile();
+  }
+
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/Lock.java b/src/uk/ac/vamsas/client/simpleclient/Lock.java
new file mode 100644 (file)
index 0000000..a124745
--- /dev/null
@@ -0,0 +1,96 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * transient object representing a file lock
+ * This lock should hold for all processes interacting in a session.
+ * @author jimp
+ */
+
+public abstract class Lock {
+  protected org.apache.commons.logging.Log log = LogFactory.getLog(Lock.class);
+  File target = null; // The file that is being locked
+  protected RandomAccessFile rafile=null;
+  
+  /**
+   * creates a valid Lock (test with <method>isLocked</method>)
+   * if a lock could be obtained for <param>lockfile</param>
+   * @param lockfile
+   */
+  protected Lock(java.io.File lockfile) {
+    target = lockfile;
+  }
+  /**
+   * 
+   * @return true if lock is held on the target
+   */
+  public abstract boolean isLocked();
+  /**
+   * release lock and close all managed channels to file
+   *
+   */
+  public abstract void release();
+  /**
+   * optionally close the open random access channel on the file when releasing lock
+   * @param closeChannel
+   */
+  public abstract void release(boolean closeChannel);
+
+  /**
+   * gets Locked Stream for reading from
+   * @param atStart true to start reading at beginning of file.
+   * @return null if file not locked
+   * @throws IOException
+   */
+  public abstract FileInputStream getFileInputStream(boolean atStart) throws IOException;
+
+  /**
+   * gets Locked stream to write to
+   * FileInput always starts at the *end* of the file (after any truncation)
+   * @param clear true means file will be cleared to zero length
+   * @return null if file is not locked
+   * @throws IOException
+   */
+  public abstract FileOutputStream getFileOutputStream(boolean clear) throws IOException;
+  /**
+   * return buffered output stream to locked file.
+   * @param clear - true means file is truncated to 0 length before writing 
+   * @return
+   */
+  public abstract BufferedOutputStream getBufferedOutputStream(boolean clear) throws IOException;
+  
+  protected void finalize() throws Throwable {
+    target=null;
+  }
+  /**
+   * return buffered input stream for locked file.
+   * @param atStart - true means read from begining of file
+   * @return null if file is not locked.
+   */
+  public BufferedInputStream getBufferedInputStream(boolean atStart) throws IOException {
+    FileInputStream fis = getFileInputStream(atStart);
+    if (fis!=null)
+      return new BufferedInputStream(fis);
+    return null;
+  }
+  /**
+   * safe lock target length() function.
+   * @return -1 for non-lockable target, otherwise target's file length 
+   */
+  public abstract long length();
+  public abstract RandomAccessFile getRaFile() throws IOException;
+  public abstract FileChannel getRaChannel() throws IOException;
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/LockFactory.java b/src/uk/ac/vamsas/client/simpleclient/LockFactory.java
new file mode 100644 (file)
index 0000000..d8098d7
--- /dev/null
@@ -0,0 +1,52 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class LockFactory {
+  protected static Log log = LogFactory.getLog(LockFactory.class);
+  public static int locktype=0; // use file lock by default
+  public static String[] locktypes = {"file","native"};
+  {
+    String lockt = System.getProperty("vamsas.locktype");
+    if (lockt!=null) {
+      int i,j;
+      for (i=0, j=locktypes.length; i<j && locktypes[i].equalsIgnoreCase(lockt); i++)
+        ;
+      if (i>=j) {
+        String lt = "'"+locktypes[0]+"'";
+        for (i=1; i<j; i++)
+          lt += ",'"+locktypes[i]+"'";
+        log.warn("System property vamsas.locktype takes one of "+lt);
+        log.warn("Defaulting to Locktype of "+locktypes[locktype]);
+      }
+    } else
+      log.debug("Defaulting to Locktype of "+locktypes[locktype]);
+  }
+  /**
+   * lock target (blocks until lock is obtained)
+   * @param target
+   * @return lock
+   */
+  public static Lock getLock(java.io.File target) {
+    return getLock(target, true);
+  }
+  public static Lock getLock(java.io.File target, boolean block) { 
+    if (locktype==0)
+      return new FileLock(target, block);
+    if (locktype==1)
+      return new NativeLock(target, block);
+    log.fatal("Implementation Error! No valid Locktype value");
+    return null;
+  }
+  /**
+   * try to lock target 
+   * @param target
+   * @return null if lock was not possible
+   */
+  public static Lock tryLock(File target) {
+    return getLock(target, false);
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/LockTimeoutException.java b/src/uk/ac/vamsas/client/simpleclient/LockTimeoutException.java
new file mode 100644 (file)
index 0000000..ef5c644
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+/**
+ * @author jimp
+ *
+ */
+public class LockTimeoutException extends Exception {
+  static final long serialVersionUID=1;
+  static final String defaultMessage = "Timeout whilst waiting for lock on VamsasDocument";
+  public LockTimeoutException() {
+    super(defaultMessage);
+  }
+  public LockTimeoutException(String arg0, Throwable arg1) {
+    super(arg0+" (timeout whilst waiting for lock)", arg1);
+    // TODO Auto-generated constructor stub
+  }
+  public LockTimeoutException(String arg0) {
+    super(arg0+" (timeout whilst waiting for lock)");
+    // TODO Auto-generated constructor stub
+  }
+  public LockTimeoutException(Throwable arg0) {
+    super(defaultMessage, arg0);
+    // TODO Auto-generated constructor stub
+  }
+  
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/LockedFileOutputStream.java b/src/uk/ac/vamsas/client/simpleclient/LockedFileOutputStream.java
new file mode 100644 (file)
index 0000000..178c74d
--- /dev/null
@@ -0,0 +1,159 @@
+/**\r
+ * \r
+ */\r
+package uk.ac.vamsas.client.simpleclient;\r
+\r
+import java.io.File;\r
+import java.io.FileDescriptor;\r
+import java.io.FileNotFoundException;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.nio.channels.FileChannel;\r
+\r
+import org.apache.commons.logging.LogFactory;\r
+\r
+/**\r
+ * @author Jim\r
+ *\r
+ */\r
+public class LockedFileOutputStream extends FileOutputStream {\r
+  private static org.apache.commons.logging.Log log = LogFactory.getLog(LockedFileOutputStream.class);\r
+  //FileOutputStream ostream=null;\r
+  boolean closed=true;\r
+  private void init() {\r
+    FileChannel ch = super.getChannel();\r
+    if (ch!=null) {\r
+      try { closed = !ch.isOpen();\r
+      } catch (Exception e) {\r
+        closed=true;\r
+        log.debug("Invalid LockedOutputStream marked closed.",e);\r
+      }\r
+    }\r
+  }\r
+  /**\r
+   * @param file\r
+   * @throws FileNotFoundException\r
+   */\r
+  public LockedFileOutputStream(File file) throws FileNotFoundException {\r
+    super(file); // super(file);\r
+    init();\r
+  }\r
+\r
+  /**\r
+   * @param file\r
+   * @param append\r
+   * @throws FileNotFoundException\r
+   */\r
+  public LockedFileOutputStream(File file, boolean append)\r
+      throws FileNotFoundException {\r
+    super(file, append);\r
+    init();\r
+  }\r
+\r
+  /**\r
+   * @param fdObj\r
+   */\r
+  public LockedFileOutputStream(FileDescriptor fdObj) {\r
+    super(fdObj);\r
+    init();\r
+    if (fdObj.valid())\r
+      closed=false;\r
+  }\r
+\r
+  /**\r
+   * @param name\r
+   * @throws FileNotFoundException\r
+   */\r
+  public LockedFileOutputStream(String name) throws FileNotFoundException {\r
+    super(name);\r
+    init();\r
+  }\r
+\r
+  /**\r
+   * @param name\r
+   * @param append\r
+   * @throws FileNotFoundException\r
+   */\r
+  public LockedFileOutputStream(String name, boolean append)\r
+      throws FileNotFoundException {\r
+    super(name, append);\r
+    init();\r
+  }\r
+  /**\r
+   * closes - actually just flushes the stream instead.\r
+   */\r
+  public void close() throws IOException {\r
+    if (!closed) {\r
+      super.flush();\r
+      super.getChannel().force(true);\r
+      log.debug("Marking Lockedoutputstream closed.");\r
+    } else\r
+      throw new IOException("Close on already closed FileOutputStream.");\r
+    closed=true;\r
+  }\r
+\r
+\r
+  /**\r
+   * @throws IOException\r
+   * @see java.io.OutputStream#flush()\r
+   */\r
+  public void flush() throws IOException {\r
+    if (!closed)\r
+      super.flush();\r
+    else\r
+      throw new IOException("flush on closed FileOutputStream");\r
+  }\r
+\r
+  /**\r
+   * @return\r
+   * @see java.io.FileOutputStream#getChannel()\r
+   */\r
+  public FileChannel getChannel() {\r
+    if (!closed)\r
+      return super.getChannel();\r
+    else\r
+      return null;\r
+  }\r
+\r
+\r
+\r
+  /**\r
+   * @param b\r
+   * @param off\r
+   * @param len\r
+   * @throws IOException\r
+   * @see java.io.FileOutputStream#write(byte[], int, int)\r
+   */\r
+  public void write(byte[] b, int off, int len) throws IOException {\r
+    if (!closed)\r
+      super.write(b, off, len);\r
+    else\r
+      throw new IOException("write on Closed FileOutputStream");\r
+  }\r
+\r
+  /**\r
+   * @param b\r
+   * @throws IOException\r
+   * @see java.io.FileOutputStream#write(byte[])\r
+   */\r
+  public void write(byte[] b) throws IOException {\r
+    if (!closed)\r
+      super.write(b);\r
+    else\r
+      throw new IOException("write on Closed FileOutputStream");\r
+  }\r
+\r
+  /**\r
+   * @param b\r
+   * @throws IOException\r
+   * @see java.io.FileOutputStream#write(int)\r
+   */\r
+  public void write(int b) throws IOException {\r
+    if (!closed)\r
+      super.write(b);\r
+    else\r
+      throw new IOException("write on Closed FileOutputStream");\r
+  }\r
+  \r
+}\r
diff --git a/src/uk/ac/vamsas/client/simpleclient/NativeLock.java b/src/uk/ac/vamsas/client/simpleclient/NativeLock.java
new file mode 100644 (file)
index 0000000..46484b1
--- /dev/null
@@ -0,0 +1,173 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.ReadableByteChannel;
+
+/**
+ * @author JimP
+ *
+ */
+public class NativeLock extends Lock {
+
+  protected FileLock lock = null;
+
+  /**
+   * @param lockfile
+   * @param block true means thread will block until a lock is obtained.
+   */
+  public NativeLock(File lockfile, boolean block) {
+    super(lockfile);
+    // try and get a lock.
+    lock = null;
+    
+    try {
+      if (!lockfile.exists())
+        if (!lockfile.createNewFile()) {
+          log.warn("Failed to create locked file "+lockfile);
+          return;
+        }
+      
+      rafile=new RandomAccessFile(lockfile,"rw");
+      if (block)
+        lock = rafile.getChannel().lock();
+      else
+        lock = rafile.getChannel().tryLock();
+      if (lock==null || !lock.isValid()) {
+        // failed to get lock. Close the file channel
+        log.debug("failed to get lock for "+lockfile);
+        rafile.getChannel().close();
+        lock=null;
+      }
+    } catch (FileNotFoundException e) {
+      //
+      log.debug("Lock failed - normal behaviour for windows locking.");
+      //log.error("Error! Couldn't create a lockfile at "
+      //  + lockfile.getAbsolutePath(), e);
+    } catch (IOException e) {
+      log.error("Error! Problems with IO when creating a lock on "
+          + lockfile.getAbsolutePath(),e);
+    }
+  }
+
+  public boolean isLocked() {
+    if (lock != null && lock.isValid()) {
+      return true;
+    }
+    return false;
+  }
+
+  public void release() {
+    release(true);
+  }
+
+  public void release(boolean closeChannel) {
+    try {
+      // channel.close should be called before release() for rigourous locking.
+      if (rafile!=null && rafile.getFD().valid() && rafile.getChannel()!=null && lock.isValid()) {
+        if (closeChannel && rafile.getChannel().isOpen()) {
+            rafile.close();
+            rafile=null; 
+        }
+        if (lock!=null && lock.isValid())
+          lock.release();
+        
+      }
+    } catch (IOException e) {
+      log.warn("Whilst releasing lock",e);
+    }
+    lock=null;
+  }
+
+  /**
+   * gets Locked Stream for reading from
+   * @param atStart true to start reading at beginning of file.
+   * @return null if file not locked
+   * @throws IOException
+   */
+  public FileInputStream getFileInputStream(boolean atStart) throws IOException {
+    if (!isLocked())
+      return null;
+    if (atStart)
+      rafile.seek(0);
+    return new FileInputStream(rafile.getFD());
+  }
+
+  /**
+   * gets Locked stream to write to
+   * FileInput always starts at the *end* of the file (after any truncation)
+   * @param clear true means file will be cleared to zero length
+   * @return null if file is not locked
+   * @throws IOException
+   */
+  public FileOutputStream getFileOutputStream(boolean clear) throws IOException {
+    if (!isLocked())
+      return null;
+    if (clear) {
+      rafile.seek(0);
+      rafile.setLength(0);
+    } else
+      rafile.seek(rafile.length());
+    return new LockedFileOutputStream(rafile.getFD());
+  }
+
+  /**
+   * return buffered output stream to locked file.
+   * @param clear - true means file is truncated to 0 length before writing 
+   * @return
+   */
+  public BufferedOutputStream getBufferedOutputStream(boolean clear) throws IOException {
+    OutputStream fos = getFileOutputStream(clear);
+    if (fos!=null)
+      return new BufferedOutputStream(fos);
+    return null;
+  }
+  /**
+   * @see uk.ac.vamsas.client.simpleclient.Lock#finalize()
+   */
+  protected void finalize() throws Throwable {
+    release(true); // we explicitly lose the lock here.
+    // log.debug("lock closing through garbage collection ?");
+    super.finalize();
+  }
+
+  /* (non-Javadoc)
+   * @see uk.ac.vamsas.client.simpleclient.Lock#getLength()
+   */
+  public long length() {
+    if (isLocked()){
+      try {
+        return rafile.length();
+      } catch (Exception e) {
+        log.debug("getLength exception:",e);
+      }
+    }
+    return -1;
+  }
+
+  public RandomAccessFile getRaFile() throws IOException {
+    if (isLocked())
+      return rafile;
+    else
+      log.debug("Failed to getRaFile on "+target);
+    return null;
+  }  
+  
+  public FileChannel getRaChannel() throws IOException {
+    if (isLocked())
+      return rafile.getChannel();
+    else
+      log.debug("Failed to getRaChannel on "+target);
+    return null;
+  }
+
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SessionFile.java b/src/uk/ac/vamsas/client/simpleclient/SessionFile.java
new file mode 100644 (file)
index 0000000..2d213b2
--- /dev/null
@@ -0,0 +1,215 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.ReadableByteChannel;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Basic methods for classes handling locked IO on files 
+ * monitored by all (simpleclient) clients in a vamsas session.
+ * @author jimp
+ *TODO: support non nio file locking capable systems
+ */
+public class SessionFile {
+  private static Log log = LogFactory.getLog(SessionFile.class);
+  protected File sessionFile;
+  protected Lock fileLock = null;
+
+  protected SessionFile(File file) {
+    super();
+    sessionFile = file;
+  }
+
+  protected boolean lockFile(Lock extantlock) {
+    if (fileLock!=null && !fileLock.isLocked()) {
+      fileLock.release();// tidy up invalid lock
+      fileLock=null;
+    }
+    if (extantlock!=null && extantlock.isLocked())
+      fileLock=extantlock; // THIS IS BROKEN - lockFile then nulls the lock.
+    return lockFile();
+  }
+  private boolean ensureSessionFile() {
+    if (sessionFile != null) {
+      if (!sessionFile.exists()) {
+        // create new file
+        try {
+          if (!sessionFile.createNewFile()) {
+            log.error("Failed to create file prior to locking: "+sessionFile);
+            return false;
+          }
+        } catch (IOException e) {
+          log.error("Exception when trying to create file "+sessionFile, e);
+          return false;
+        }
+      }
+      return true;
+    }
+    log.error("ensureSessionFile called for non-initialised SessionFile!");
+    return false;
+  }
+  /**
+   * Get a lock for the SessionFile
+   * 
+   * @return true if lock was made
+   */
+  protected boolean lockFile() {
+    if (fileLock != null) {
+      if (fileLock.isLocked()) {
+        if (!ensureSessionFile())
+          return false;
+        return true;
+      } else {
+        // lock failed for some reason.
+        fileLock.release();
+        log.info("Unexpected session file lock failure. Trying to get it again.");
+        fileLock=null;
+      }
+    }
+    if (!ensureSessionFile())
+      return false;
+    // TODO: see if we need to loop-wait for locks or they just block until
+    // lock is made...
+    long tries=5000;
+    do {
+      tries--;
+      if (fileLock==null || !fileLock.isLocked()) {
+        //try { Thread.sleep(1); } catch (Exception e) {};
+        fileLock = LockFactory.getLock(sessionFile,true); // TODO: wait around if we can't get the lock.
+          }
+      } while (tries>0 && !fileLock.isLocked());
+    if (!fileLock.isLocked())
+      log.error("Failed to get lock for "+sessionFile);
+    // fileLock = new Lock(sessionFile);
+    return fileLock.isLocked();    
+  }
+
+  /**
+   * Explicitly release the SessionFile's lock.
+   * 
+   * @return true if lock was released.
+   */
+  protected void unlockFile() {
+    if (fileLock != null) {
+      fileLock.release();    
+      fileLock = null;
+    }
+  }
+
+  /**
+   * Makes a backup of the sessionFile.
+   * @return Backed up SessionFile or null if failed to make backup.
+   */
+  protected File backupSessionFile() {
+    return backupSessionFile(null, sessionFile.getName(),".old", sessionFile.getParentFile());
+  }
+
+  protected File backupSessionFile(Lock extantLock, String backupPrefix, String backupSuffix, File backupDir) {
+    File tempfile=null;
+    if (lockFile(extantLock)) {
+      try {
+        tempfile = File.createTempFile(backupPrefix, backupSuffix, backupDir);
+        if (fileLock.length()>0) {
+          FileOutputStream tos = new FileOutputStream(tempfile);
+          ReadableByteChannel channel;
+          tos.getChannel().transferFrom(channel=fileLock.getRaChannel(), 0,
+              fileLock.length());
+          tos.close();
+          if (!channel.isOpen())
+            throw new Error(tos.getChannel().getClass()+".transferFrom closes source channel!");
+          if (!lockFile(extantLock))
+            throw new Error("Lost lock for "+sessionFile.getName()+" after backup.");
+          
+        }
+      } catch (FileNotFoundException e1) {
+        log.warn("Can't create temp file for "+sessionFile.getName(),e1);
+        tempfile=null;
+      } catch (IOException e1) {
+        log.warn("Error when copying content to temp file for "+sessionFile.getName(),e1);
+        tempfile=null;
+      }
+    }
+    return tempfile;
+  }
+  /**
+   * Replaces data in sessionFile with data from file handled by another sessionFile
+   * passes up any exceptions.
+   * @param newData source for new data
+   */
+  protected void updateFrom(Lock extantLock, SessionFile newData) throws IOException {
+    log.debug("Updating "+sessionFile.getAbsolutePath()+" from "+newData.sessionFile.getAbsolutePath());
+    if (newData==null)
+      throw new IOException("Null newData object.");
+    if (newData.sessionFile==null)
+      throw new IOException("Null SessionFile in newData.");
+      
+    if (!lockFile(extantLock))
+      throw new IOException("Failed to get write lock for "+sessionFile);
+    if (!newData.lockFile())
+      throw new IOException("Failed to get lock for updateFrom "+newData.sessionFile);
+    RandomAccessFile nrafile = newData.fileLock.getRaFile();
+    nrafile.seek(0);
+    RandomAccessFile trafile = fileLock.getRaFile();
+    /*long tries=5000;
+    while (trafile==null && --tries>0) {
+      log.debug("Lost lock on "+sessionFile+"! Re-trying for a transfer.");
+      lockFile();
+      trafile = fileLock.getRaFile();
+    }*/
+    trafile.seek(0);
+    trafile.getChannel().transferFrom(nrafile.getChannel(), 0, 
+        nrafile.length());
+  }
+  /**
+   * remove all trace of the sessionFile file
+   *
+   */
+  protected void eraseExistence() {
+    unlockFile();
+    if (sessionFile!=null) {
+      sessionFile.delete();
+      sessionFile = null;
+    }
+  }
+  /* (non-Javadoc)
+   * @see uk.ac.vamsas.client.simpleclient.Lock#getBufferedInputStream(boolean)
+   */
+  public BufferedInputStream getBufferedInputStream(boolean atStart) throws IOException {
+    lockFile();
+    return fileLock.getBufferedInputStream(atStart);
+  }
+
+  /* (non-Javadoc)
+   * @see uk.ac.vamsas.client.simpleclient.Lock#getBufferedOutputStream(boolean)
+   */
+  public BufferedOutputStream getBufferedOutputStream(boolean clear) throws IOException {
+    lockFile();
+    return fileLock.getBufferedOutputStream(clear);
+  }
+
+  /* (non-Javadoc)
+   * @see uk.ac.vamsas.client.simpleclient.Lock#getFileInputStream(boolean)
+   */
+  public FileInputStream getFileInputStream(boolean atStart) throws IOException {
+    lockFile();
+    return fileLock.getFileInputStream(atStart);
+  }
+
+  /* (non-Javadoc)
+   * @see uk.ac.vamsas.client.simpleclient.Lock#getFileOutputStream(boolean)
+   */
+  public FileOutputStream getFileOutputStream(boolean clear) throws IOException {
+    lockFile();
+    return fileLock.getFileOutputStream(clear);
+  }
+  
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SessionFlagFile.java b/src/uk/ac/vamsas/client/simpleclient/SessionFlagFile.java
new file mode 100644 (file)
index 0000000..27f83c7
--- /dev/null
@@ -0,0 +1,85 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * methods for setting and checking 
+ * binary flags in a vamsas session directory.
+ * all methods apart from the constructor will 
+ * throw a fatal error if the flagFile is not
+ * a valid java.io.File object.
+ * LATER: extract SessionFlag interface for generalizing the vamsas session code 
+ * @author jimp
+ *
+ */
+public class SessionFlagFile {
+  private static Log log = LogFactory.getLog(SessionFlagFile.class);
+  protected File flagFile=null;
+  private void checkFlagFile() {
+    if (flagFile==null) {
+      log.fatal("Implementation error - uninitialized SessionFlagFile", 
+          new Error("Implementation error - uninitialized SessionFlagFile"));
+    }
+  }
+  /**
+   * will log a warning if exceptions occur during flag creation.
+   * @return true if flag was set successfully
+   */
+  public boolean setFlag() {
+    checkFlagFile();
+    try {
+      if (flagFile.createNewFile()) {
+        log.debug("Set session flag "+flagFile);
+      } else {
+        log.debug("Session flag already set "+flagFile);
+      }
+      return true;
+    }
+    catch (Exception e) {
+      log.warn("Couldn't set session flag "+flagFile, e);
+    }
+    return false;
+  }
+  /**
+   * 
+   * @return true if flag was cleared successfully
+   */
+  public boolean clearFlag() {
+    checkFlagFile();
+    if (flagFile.exists()) {
+      log.debug("clearing session flag "+flagFile);
+      if (!flagFile.delete()) {
+        log.warn("failed to clear session flag "+flagFile);
+        return false;
+      }
+    } else {
+      log.debug("clearFlag called for already cleared flag "+flagFile);
+    }
+    return true;
+  }
+    /**
+     * 
+     * @return state of session flag
+     */
+  public boolean checkFlag() {
+    checkFlagFile();
+    if (flagFile.exists()) {
+      if (log.isDebugEnabled())
+        log.debug("Flag '"+flagFile+"' is set.");
+      return true;
+    }
+    if (log.isDebugEnabled())
+      log.debug("Flag '"+flagFile+"' is not set.");    
+    return false;
+  }
+    /**
+     * @param flagFile
+     */
+    public SessionFlagFile(File flagFile) {
+      super();
+      this.flagFile = flagFile;
+    };
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SessionUrn.java b/src/uk/ac/vamsas/client/simpleclient/SessionUrn.java
new file mode 100644 (file)
index 0000000..08516cd
--- /dev/null
@@ -0,0 +1,40 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+import java.net.MalformedURLException;
+
+/**
+ * SessionUrn for simpleclient sessions:
+ * simpleclient://{Absolute path to session directory}
+ * @author jimp
+ *
+ */
+public class SessionUrn extends org.vamsas.client.SessionUrn {
+  /**
+   * a simple client session urn prefix
+   */
+  public static final String SIMPLECLIENT="simpleclient";
+  public static String VAMSASDOCUMENT="vdoc";
+  static {
+    TYPES.put(SIMPLECLIENT, SessionUrn.class);
+    TYPES.put(SessionUrn.VAMSASDOCUMENT, SessionUrn.class);
+  }
+  
+  public SessionUrn(File sessionLocation) throws MalformedURLException {
+  // TODO: LATER: implement switch to have vamsas document or simpleclient sessions for same constructor
+    super(SIMPLECLIENT, sessionLocation.getAbsoluteFile().toURL());
+    //else
+      // super(VAMSASDOCUMENT, sessionLocation);
+  }
+  public SessionUrn(VamsasSession session) throws MalformedURLException {
+    super(SIMPLECLIENT, session.sessionDir.getAbsoluteFile().toURL());
+  }
+  /**
+   * TODO: LATER: think about this again.
+   * @return File(urn.getPath())
+   */
+  public File asFile() {
+    return new File(urn.getPath());
+  }
+  // TODO: add abstract 'handler' methods for resolving the URN to a particular class
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SessionsFile.java b/src/uk/ac/vamsas/client/simpleclient/SessionsFile.java
new file mode 100644 (file)
index 0000000..f931118
--- /dev/null
@@ -0,0 +1,342 @@
+/* EMBL - The European Bioinformatics institute
+* MSD Group
+* VAMSAS Project
+*
+*  Copyright (c) 2005-2006 Thr European Bioinformatics Institute. All rights reserved.
+*
+*   Redistribution and use in source and binary forms, with or without
+*   modification, are permitted provided that the following conditions
+*   are met:
+*  
+*   1. Redistributions of source code must retain the above copyright
+*      notice, this list of conditions and the following disclaimer. 
+*  
+*   2. Redistributions in binary form must reproduce the above copyright
+*      notice, this list of conditions and the following disclaimer in
+*      the documentation and/or other materials provided with the
+*      distribution.
+*  
+*   3. The name MSD must not be used to endorse or promote products 
+*      derived from this software without prior written permission. For 
+*      written permission, please contact msd-help@ebi.ac.uk
+*  
+*   4. Products derived from this software may not be called "MSD"
+*      nor may "MSD" appear in their names without prior written
+*      permission of the MSD developers.
+*  
+*   5. Redistributions of any form whatsoever must retain the following
+*      acknowledgment:
+*      "This product includes software developed by MSD 
+*       (http://www.ebi.ac.uk/)"
+*  
+*   THIS SOFTWARE IS PROVIDED BY THE MSD GROUP ``AS IS'' AND ANY
+*   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+*   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+*   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE ENSEMBL GROUP OR
+*   ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+*   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+*   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+*   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+*   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+*   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+*   OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+* The European Bioinformatics Institute may publish revised and/or new
+* versions of this license with new releases of VAMSAS software.
+*==============================================================================
+*
+*  @author <a href="mailto:pierre@ebi.ac.uk">Pierre MARGUERITE</a>
+* 
+* Dec 13, 2006  - VamsasClientV4
+*
+*/
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.SessionHandle;
+
+/**
+ * @author <a href="mailto:pierre@ebi.ac.uk">Pierre MARGUERITE</a>
+ * 
+ * 
+ */
+public class SessionsFile extends ListFile {
+  
+  private static Log log = LogFactory.getLog(SessionsFile.class);
+  /**
+   * when set true - get FileNotFoundExceptions on WinXP when writing to locked stream after the backup has been made (via the backupFile method)
+   */
+  boolean backup=false;
+  /**
+   * number of my session in list - passed back when a session 
+   * is added to list, and used (if valid) for quickly 
+   * looking up presence of session handle in the list.
+   */
+  private int syncnum = 1;
+  /**
+   * @param file
+   */
+  public SessionsFile(File file) throws java.io.IOException {
+    super(file);
+  }
+  
+  
+  /**
+   * internal method for getting sessionsList - ensures a lock has been made but
+   * does not release it.
+   * 
+   * @return list of clients
+   */
+  private SessionHandle[] retrieveSessionHandles() {
+    if (lockFile()) {
+      try {
+        SessionHandle[] clients=null;
+        if (this.fileLock.length()>0) {
+          
+          ObjectInputStream is = new ObjectInputStream(this.fileLock.getBufferedInputStream(true));
+          Object o;
+          o=is.readObject();
+          if (o!=null) {
+            try {
+              clients = (SessionHandle[]) o;
+            }
+            catch (Exception e) {
+              log.error("Garbage in the clientHandle list "+this.sessionFile,e);
+            }
+          }
+        }
+        return clients;
+      } catch (FileNotFoundException e) {
+       // e.printStackTrace(System.err);
+        log.error(e);
+      } catch (Exception e) {
+        log.error(e);
+        //e.printStackTrace(System.err);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * get the SessionsList from the file. May return null if lock failed!
+   * @return sessionsList
+   */
+  public SessionHandle[] retrieveSessionsList() {
+    if (lockFile()) {
+      SessionHandle[] clients = retrieveSessionHandles();
+      unlockFile();
+      return clients;
+    }
+    return null;
+  }
+
+  /**
+   * get list from the locked ClientList.
+   * @param extantlock
+   * @return clientList or null if lock failed (or file was empty)
+   */
+  public SessionHandle[] retrieveSessionsList(Lock extantlock) {
+    if (lockFile(extantlock)) {
+     SessionHandle[] clients = retrieveSessionHandles();
+      unlockFile();
+      return clients;
+    }
+    return null;
+  }
+  
+  /**
+   * adds clientHandle me to the clientList under an existing lock extantLock.
+   * @param me
+   * @param extantLock
+   * @return client index in list or 0 if lock was invalid or addClient operation failed.
+   */
+  public int addSession(SessionHandle me, Lock extantLock) {
+    return addSession(me, true, extantLock);
+  }
+  
+  /**
+   * adds SessionsHandle me to the sessionsList under an existing lock.
+   * @param me - sessionsHandle
+   * @param disambig - if true then add will fail if an identical clientHandle already exists
+   * @param extantLock - existing lock
+   * @return client index in list or 0 if addClient (or the lock) failed.
+   */
+  
+  public int addSession(SessionHandle session, boolean disambig, Lock extantLock) {
+    if (lockFile(extantLock)) {
+      this.syncnum = addSession(session, disambig);
+      unlockFile();
+      return this.syncnum;
+    }
+    return 0;
+  }
+  
+  /**
+   * removes the current session from the  SessionsList without complaint if the session isn't in the sessionsList already.
+   * @param me client handle to be removed
+   * @param clientlock existing lock passed from watcher.
+   */
+  public void removeSession(SessionHandle session, Lock clientlock) {
+    int mynum =-1;
+    if (lockFile(clientlock)) {
+      SessionHandle[] sessions = retrieveSessionHandles();
+      if (sessions != null) {
+        if ((this.syncnum<=0 || this.syncnum>sessions.length) || sessions[this.syncnum-1]!=session) {
+          for (int i = 0, j = sessions.length; i < j; i++) 
+            if (sessions[i].equals(session)) {
+              mynum=i;
+              break;
+            }
+        } else {
+          mynum=this.syncnum-1;
+        }
+        if (mynum>-1) {
+          SessionHandle[] newlist = new SessionHandle[sessions.length - 1];
+          for (int k=0,i = 0, j = sessions.length; i < j; i++)
+            if (i!=mynum)
+              newlist[k++] = sessions[i];
+          if (!putSessionsList(newlist))
+            throw new Error("Failed to write new sessionsList!"); // failed to put the sessionList to disk.
+        }
+      }
+      unlockFile();
+    } else {
+      throw new Error("Couldn't get lock for "+((sessionFile==null) ? "Unitialised sessionFile in SessionsFile" : this.sessionFile.getAbsolutePath()));
+    }
+  }
+  /**
+   * Adds a SessionHandle to the SessionList file - optionally disambiguating 
+   * the SessionHandle (modifes the URN). 
+   * Note: Caller is left to release the lock on the SessionList.
+   * @param me
+   * @param disambiguate -
+   *          flag indicating if the URN for me should be disambiguated to
+   *          differentiate between sessions.
+   * @return index of sessionHandle in new list, or -1-position of existing
+   *         sessionHandle (if disambiguate is true)
+   */
+  protected int addSession(SessionHandle session, boolean disambiguate) {
+    int newsession = 0;
+    int tries=5;
+    while (tries-->0 && !lockFile())
+      try { Thread.sleep(1); } catch (Exception e){};
+    if (lockFile()) {
+      SessionHandle[] sessions = retrieveSessionHandles();
+
+      if (sessions == null) {
+        sessions = new SessionHandle[1];
+        sessions[0] = session;
+        newsession = 1;
+      } else {
+        int k = 0;
+        for (int i = 0, j = sessions.length; i < j; i++) {
+          if ( sessions[i].equals(session)) {
+            if (disambiguate) {
+              while (sessions[i].equals(session)) {
+               // me.setClientUrn(me.getClientUrn() + k++); // TODO: make a better
+                                                          // disambiguation of
+                                                          // urn.
+              }
+            } else {
+              // will not write the ambiguous clientHandle to disk, just return
+              // its index.
+              return -1 - i;
+            }
+          }
+        }
+        int i, j;
+        SessionHandle[] newlist = new SessionHandle[sessions.length + 1];
+        for (i = 0, j = sessions.length; i < j; i++)
+          newlist[i] = sessions[i];
+        newlist[j] = session;
+        sessions = newlist;
+        newsession = j+1;
+      }
+      if (!putSessionsList(sessions))
+        return 0; // failed to put the clientList to disk.
+    }
+    return newsession;
+  }
+  
+  
+  /**
+   * safely writes sessions array to the file referred to by sessionFile.
+   * 
+   * @param clients
+   * @return true if successful write. Throws Errors otherwise.
+   */
+  protected boolean putSessionsList(SessionHandle[] clients) {
+    if (lockFile()) {
+      File templist=null;
+      if (!this.backup || (templist = backupSessionFile()) != null) {
+        int retries=3;
+        while (retries-->0) {
+          try {
+            ObjectOutputStream os = 
+              new ObjectOutputStream(this.fileLock.getBufferedOutputStream(true));
+            log.debug("About to write "+clients.length+" sessionHandles to output stream.");
+            os.writeObject(clients);
+            os.close();
+          // All done - remove the backup.
+          if (this.backup)
+            templist.delete();
+          templist = null;
+          retries=-1;
+          } catch (Exception e) {
+           // System.err
+            //.println("Serious - problems writing to sessionFile.");
+            log.error("Serious - problems writing to sessionFile.",e);
+            if (retries>0 && templist != null) {
+            //  System.err.println("Recovering from Backup in "
+              //        + templist.getAbsolutePath());
+              log.error("Recovering from Backup in "+ templist.getAbsolutePath());
+              templist.renameTo(this.fileLock.target);
+            }
+            //e.printStackTrace(System.err);
+            log.error(e);
+          }
+        }
+        if (retries>-2) {
+         // System.err
+         // .println("Serious - problems writing to sessionFile. Giving Up.");
+          log.error("Serious - problems writing to sessionFile. Giving Up.");
+          return false;
+        }
+      } else {
+        throw new Error(
+            "Couldn't create backup of the clientList before writing to it!");
+      }
+    } else {
+      throw new Error("Could not lock the clientList: "
+          + ((this.sessionFile == null) ? "Unitialized ClientsFile"
+              : " failed to get lock on " + this.sessionFile.getAbsolutePath()));
+    }
+    // successful!
+    return true;
+  }
+
+  public void clearList() {
+    if (lockFile()) {
+      try {
+        FileOutputStream fout = this.fileLock.getFileOutputStream(true);
+        fout.flush();
+        fout.close();
+      } catch (Exception e) {
+        throw new Error("Problems trying to clear clientlist!",e);
+        
+      }
+    }
+    
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SimpleClient.java b/src/uk/ac/vamsas/client/simpleclient/SimpleClient.java
new file mode 100644 (file)
index 0000000..2858dd3
--- /dev/null
@@ -0,0 +1,416 @@
+/*
+ * Created on 15-Sep-2005
+ *
+ * TODO To change the template for this generated file go to
+ * Window - Preferences - Java - Code Style - Code Templates
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.beans.EventHandler;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Hashtable;
+import java.util.Vector;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.ClientHandle;
+import org.vamsas.client.Events;
+import org.vamsas.client.IClient;
+import org.vamsas.client.IClientDocument;
+import org.vamsas.client.IObjectUpdate;
+import org.vamsas.client.InvalidSessionUrnException;
+import org.vamsas.client.SessionHandle;
+import org.vamsas.client.UserHandle;
+import org.vamsas.objects.core.ApplicationData;
+import org.vamsas.objects.core.Entry;
+import org.vamsas.objects.core.LockFile;
+import org.vamsas.objects.core.VamsasDocument;
+import org.vamsas.objects.utils.AppDataReference;
+import org.vamsas.objects.utils.ProvenanceStuff;
+import org.vamsas.objects.utils.document.VersionEntries;
+
+/**
+ * @author jimp
+ */
+public class SimpleClient implements IClient {
+  
+  private static Log log = LogFactory.getLog(SimpleClient.class);
+  
+  protected UserHandle user = null;
+  
+  protected SessionUrn session = null;
+  protected VamsasSession _session;
+  protected ClientHandle client = null;
+  protected EventGeneratorThread evgen = null;
+  protected ClientDocument cdocument = null;
+  /**
+   * object hash table that persists in each client holding vorbaIds and hash values after a document write
+   */
+  protected Hashtable extantobjects=null;
+  /**
+   * construct a transient IdFactory instance - this should last only as long as the 
+   * SimpleClient object holds the lock on the vamsas document being created/manipulated.
+   * @return
+   */
+  private IdFactory makeVorbaIdFactory() {
+    return new IdFactory(getSessionHandle(), client, user);
+  }
+  
+  /**
+   * construct SimpleClient for user, client and VamsasSession directory
+   * use the SimpleClientFactory rather than this constructor directly. 
+   * @param user
+   * @param client
+   * @param sess
+   */
+  protected SimpleClient(UserHandle user, ClientHandle client, VamsasSession sess) throws InvalidSessionUrnException {
+    // TODO: validate user/client/session
+    _session = sess;
+    this.user = user;
+    this.client = client;
+    try {
+      session = new SessionUrn(_session);
+    } catch (MalformedURLException e) {
+      log.error("Couldn't form a valid SessionUrn object!",e);
+      throw new InvalidSessionUrnException(_session.toString());
+    }
+  }
+  /**
+   * construct new session by importing objects from an existing vamsas document
+   * @param user
+   * @param client
+   * @param sess
+   * @param importingArchive
+   * @throws Exception IOExceptions for Session IO problems, and general Exception if importing document is invalid.
+   */
+  protected SimpleClient(UserHandle user, ClientHandle client, VamsasSession sess, File importingArchive) throws Exception {
+    this(user, client, sess);
+    VamsasArchive sessdoc = _session.getVamsasDocument();
+    try {
+      VamsasArchiveReader odoc = new VamsasArchiveReader(importingArchive);
+      SimpleDocument sdoc = new SimpleDocument(makeVorbaIdFactory());
+      VamsasDocument doc = sdoc.getVamsasDocument(odoc);
+      sessdoc.putVamsasDocument(doc, sdoc.vorba);
+      sessdoc.closeArchive();
+    } catch (Exception e) {
+      sessdoc.cancelArchive();
+      // write a dummy archive
+      _session.slog.info("Exception when importing document data from "+importingArchive);
+      throw new Exception("Failed to import data from "+importingArchive, e);
+    }
+  }
+  
+  /*
+   * (non-Javadoc)
+   * LATER: check that build substitution variables are correct
+   * @see org.vamsas.client.IClient#getAbout()
+   */
+  public String getAbout() {
+    return new String("VORBA SimpleClient version $version$ build $build$");
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#getSessionUrn()
+   */
+  public String getSessionUrn() {
+    return session.getSessionUrn();
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#getSessionHandle()
+   */
+  public SessionHandle getSessionHandle() {
+    // TODO: eliminate SessionHandle ? need to refactor interfaces.
+    SessionHandle sh = new SessionHandle(session.getSessionUrn());
+    return sh;
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#getClientHandle()
+   */
+  public ClientHandle getClientHandle() {
+    return client;
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#getUserHandle()
+   */
+  public UserHandle getUserHandle() {
+    return user;
+  }
+  /**
+   * 
+   * @return user field for a provenance entry
+   */
+  protected String getProvenanceUser() {
+    return new String(user.getFullName());
+  }
+  /**
+   * construct a provenance entry for this client with the specified action string.
+   * @param action
+   * @return properly completed provenance entry
+   */
+  protected Entry getProvenanceEntry(String action) {
+    Entry prov = ProvenanceStuff.newProvenanceEntry(client.getClientUrn(), getProvenanceUser(), action);
+    return prov;
+  }
+  private Hashtable handlers = initHandlers();
+  
+  private Vector listeners = new Vector();
+  
+  /**
+   * make all the PropertyChangeSupport objects for the
+   * events described in org.vamsas.client.Event
+   * @return
+   */
+  private static Hashtable initHandlers() {
+    Hashtable events = new Hashtable();
+    java.util.Iterator evt = Events.EventList.iterator();
+    while (evt.hasNext()) {
+      Object ths = evt.next();
+      events.put(ths, (Object) new PropertyChangeSupport(ths));
+    }
+    return events;
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#addDocumentUpdateHandler(java.util.EventListener)
+   */
+  public void addDocumentUpdateHandler(PropertyChangeListener evt) {
+    if (handlers.containsKey(Events.DOCUMENT_UPDATE)) {
+      Object handler;
+      ((PropertyChangeSupport) (handler = handlers.get(Events.DOCUMENT_UPDATE)))
+      .addPropertyChangeListener(evt);
+      listeners.add(handler);
+      listeners.add((Object) evt);
+    }
+  }
+  boolean finalized=false;
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#finalizeClient()
+   */
+  public void finalizeClient() {
+    // TODO: determine if this is last client in session
+    // TODO: raise events like : ((lst_client && document.request.to.close), (client_finalization), (
+    
+    // if (handlers.containsKey(Events.))
+    // if (handlers.containsKey(Events.CLIENT_FINALIZATION))
+    // deregister listeners.
+    // mark this instance as finalized
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#getClientDocument()
+   */
+  public IClientDocument getClientDocument() throws IOException {
+    if (cdocument!=null) {
+      // cdocument is non-nill if the ClientDocument.finalise() method hasn't been called.
+      return cdocument;
+    }
+    VamsasArchive va = null;
+    try {
+      // LATER: bail out if it takes too long to get the lock ?
+      va = _session.getVamsasDocument();
+    }
+    catch (IOException e) {
+      throw new IOException("Failed to get lock on session document");
+    }
+    VamsasDocument doc=null;
+    IdFactory vorba = null;
+    // TODO: reduce size of vorba ids generated from these parameters to IdFactory (mainly sessionHandle rationalization ?)
+    try {
+      va.setVorba(vorba=makeVorbaIdFactory());
+      // if session currently holds data - read it in - or get a dummy
+      _session.slog.debug("Accessing document");
+      doc = 
+        va.getVamsasDocument(getProvenanceUser(),
+            "created new session document.", null);
+      if (doc!=null)
+        _session.slog.debug("Successfully retrieved document.");
+      else
+        log.error("Unexpectedly retrieved null document!");
+    }
+    catch (Exception e) {
+      log.error("Failed to get session document for session directory '"+_session.sessionDir+"'", e);
+      throw new IOException("Failed to get session document for session directory '"+_session.sessionDir+"'");
+    }
+    // Construct the IClientDocument instance
+    
+    ClientDocument cdoc = new ClientDocument(doc, va, vorba, this);
+    return cdoc;
+  }
+  
+  /*
+   * (non-Javadoc)
+   * @throws Errors for invalid newdoc parameter
+   * @see org.vamsas.client.IClient#updateDocument(org.vamsas.client.IClientDocument)
+   */
+  public void updateDocument(IClientDocument newdoc) {
+    if (!(newdoc instanceof ClientDocument)) {
+      throw new Error("Invalid IClientDocument instance for SimpleClient.");
+    }
+    if (cdocument==null)
+      throw new Error("Client Error - updateDocument() called before getClientDocument().");
+    if (newdoc!=cdocument)
+      throw new Error("Client Error - SimpleClient.updateDocument() can only take the IClientDocument instance returned from SimpleClient.getClientDocument()");
+    if (!cdocument.isModified()) {
+      if (log.isDebugEnabled())
+        log.debug("updateDocument for "+session.getSessionUrn()+" with unmodified IClientDocument.");
+    } else {
+      try {
+        if (!cdocument.updateSessionDocument()) {
+          log.warn("Session document did not update properly for session directory "+_session.sessionDir);
+          // cdocument.archive.cancelArchive(); // LATER: could be done - would need to prevent updateSessionDocument closing the archive.
+          _session.slog.warn("Session Document updated but may not be valid (false return from org.vamsas.simpleclient.ClientDocument.updateSessionDocument()");
+        }
+      }
+      catch (IOException e) {
+        log.warn("IO Problems when updating document!",e);
+        _session.slog.error("IO problems when attempting to update document.");
+      }
+    }
+    // garbage collect the ClientDocument instance.
+    try {
+      cdocument.finalize();
+
+    } catch (Throwable e) {
+      log.error("Exception when trying to garbage collect ClientDocument for "+session.getSessionUrn(), e);
+    }
+    cdocument = null; // this is probably done by finalize
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#storeDocument(java.io.File)
+   */
+  public void storeDocument(File location) {
+    
+    // write storeDocument file to inform other clients that they should raise
+    Lock vamlock = evgen.want_to_store();
+    // Events.DOCUMENT_FINALIZEAPPDATA
+    try {
+      _session.writeVamsasDocument(location, vamlock);
+      _session.clearUnsavedFlag();
+    } catch (Exception e) {
+      log.warn("Exception whilst trying to store document in "+location,e);
+    }
+    
+    vamlock.release();
+  }
+  
+  /*
+   * (non-Javadoc)
+   * 
+   * @see org.vamsas.client.IClient#addVorbaEventHandler(java.lang.String,
+   *      java.beans.PropertyChangeListener)
+   */
+  public void addVorbaEventHandler(String EventChain, PropertyChangeListener evt) {
+    if (handlers.containsKey(EventChain)) {
+      Object handler;
+      ((PropertyChangeSupport) (handler = handlers.get(EventChain)))
+      .addPropertyChangeListener(evt);
+      listeners.add(handler);
+      listeners.add((Object) evt);
+    }
+  }
+  
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClient#pollUpdate()
+   */
+  public void pollUpdate() {
+    
+    if (evgen==null) {
+      log.warn("pollUpdate called on incomplete SimpleClient object.");
+      return;
+    }
+    
+    if (!evgen.isAlive()) {
+      log.warn("pollUpdate called before joinSession() - trying to do this.");
+      try {
+        joinSession();
+      } catch (Exception e) {
+        log.error("Unexpected exception on default call to joinSession",e);
+      }
+    }
+    
+    //TODO ensure event generator robustly handles these interrupts.
+    log.debug("interrrupting event generator.");
+    evgen.interrupt();
+    log.debug("interrrupted event generator.");
+  }
+  
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClient#joinSession()
+   */
+  public void joinSession() throws Exception {
+    // start the EventGenerator thread.
+    if (evgen==null) {
+      log.warn("joinSession called on incomplete SimpleClient object.");
+      return;
+    }
+    if (evgen.isAlive())
+      throw new Error("Join session called twice for the same SimpleClient (IClient instance).");
+    evgen.start();
+    if (evgen.isAlive())
+      log.debug("Started EventGenerator thread.");
+    else {
+      log.warn("Failed to start EventGenerator thread.");
+      throw new Exception("Failed to start event generator thread - client cannot be instantiated.");
+    }
+    if (evgen.countHandlersFor(Events.DOCUMENT_CREATE)>0) {
+      //TODO: is this application connecting to a newly created session document ?
+      //evgen.raise(Events.DOCUMENT_CREATE);
+    }
+  }
+  
+  
+  
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClient#importDocument(java.io.File)
+   */
+  public void importDocument(File location) {
+    // TODO LATER: implement SimpleClient.importDocument()
+    log.error("importDocument is not yet implemented for a SimpleClient Session.");
+  }
+
+  public IObjectUpdate getUpdateHandler(Class rootObject) {
+    // TODO Auto-generated method stub
+    return null;
+  }
+
+  public IObjectUpdate[] getUpdateHandlers() {
+    // TODO Auto-generated method stub
+    return null;
+  }
+
+  public void removeUpdateHandler(Class rootObject) {
+    // TODO Auto-generated method stub
+    
+  }
+
+  public void setUpdateHandler(IObjectUpdate handler) {
+    // TODO Auto-generated method stub
+    
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SimpleClientAppdata.java b/src/uk/ac/vamsas/client/simpleclient/SimpleClientAppdata.java
new file mode 100644 (file)
index 0000000..dcaedee
--- /dev/null
@@ -0,0 +1,543 @@
+/**
+ * 
+ */
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Vector;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.IClientAppdata;
+import org.vamsas.objects.core.AppData;
+import org.vamsas.objects.core.ApplicationData;
+import org.vamsas.objects.core.User;
+import org.vamsas.objects.utils.AppDataReference;
+
+/**
+ * @author jimp
+ * Access interface to data chunks read from a VamsasArchiveReader stream 
+ * (or byte buffer input stream) or written to a VamsasArchive stream.
+ * // TODO: get VamsasArchiveReader from sclient
+ */
+public class SimpleClientAppdata implements IClientAppdata {
+  private static Log log = LogFactory.getLog(SimpleClientAppdata.class);
+  /**
+   * has the session's document been accessed to get the AppData entrys?
+   */
+  protected boolean accessedDocument = false;
+  /**
+   * has the user datablock been modified ? 
+   * temporary file containing new user specific application data chunk
+   */
+  SessionFile newUserData=null;
+  JarOutputStream newUserDataStream = null;
+  /**
+   * has the apps global datablock been modified ?
+   * temporary file containing new global application data chunk
+   */
+  SessionFile newAppData=null;
+  JarOutputStream newAppDataStream=null;
+  /**
+   * set by extractAppData
+   */
+  protected ApplicationData appsGlobal = null;
+  /**
+   * set by extractAppData
+   */
+  protected User usersData = null;
+  
+  ClientDocument clientdoc;
+  /**
+   * state flags
+   * - accessed ClientAppdata
+   * - accessed UserAppdata
+   * => inputStream from embedded xml or jar entry of backup has been created
+   * - set ClientAppdata
+   * - set UserAppdata
+   * => an output stream has been created and written to - or a data chunk has been written.
+   *  - need flag for switching between embedded and jar entry mode ? - always write a jar entry for a stream.
+   *  - need code for rewind and overwriting if the set*Appdata methods are called more than once.
+   *  - need flags for streams to except a call to set*Appdata when an output stream exists and is open.
+   *  - need 
+   * @param clientdoc The ClientDocument instance that this IClientAppData is accessing
+   */
+  protected SimpleClientAppdata(ClientDocument clientdoc) {
+    if (clientdoc==null) {
+      log.fatal("Implementation error - Null ClientDocument for SimpleClientAppdata construction.");
+      throw new Error("Implementation error - Null ClientDocument for SimpleClientAppdata construction.");
+    }
+    this.clientdoc = clientdoc;
+  }
+  /**
+   * gets appropriate app data for the application, if it exists in this dataset
+   * Called by every accessor to ensure data has been retrieved from document.
+   */
+  private void extractAppData(org.vamsas.objects.core.VamsasDocument doc) {
+    if (doc==null) {
+      log.debug("extractAppData called for null document object");
+      return;
+    }
+    if (accessedDocument) {
+      return;
+    }
+    Vector apldataset = AppDataReference.getUserandApplicationsData(
+        doc, clientdoc.sclient.getUserHandle(), clientdoc.sclient.getClientHandle());
+    accessedDocument = true;
+    if (apldataset!=null) {
+      if (apldataset.size()>0) {
+        AppData clientdat = (AppData) apldataset.get(0);
+        if (clientdat instanceof ApplicationData) {
+          appsGlobal = (ApplicationData) clientdat;
+          if (apldataset.size()>1) {
+            clientdat = (AppData) apldataset.get(1);
+            if (clientdat instanceof User) {
+              usersData = (User) clientdat;
+            }
+            if (apldataset.size()>2)
+              log.info("Ignoring additional ("+(apldataset.size()-2)+") AppDatas returned by document appdata query.");
+          } 
+        } else {
+          log.warn("Unexpected entry in AppDataReference query: id="+clientdat.getVorbaId()+" type="+clientdat.getClass().getName());
+        }
+        apldataset.removeAllElements(); // destroy references.
+      }
+    }
+  }
+  /**
+   * LATER: generalize this for different low-level session implementations (it may not always be a Jar)
+   * @param appdata
+   * @param docreader
+   * @return
+   */
+  private JarInputStream getAppDataStream(AppData appdata, VamsasArchiveReader docreader) {
+    String entryRef = appdata.getDataReference();
+    if (entryRef!=null) {
+      log.debug("Resolving appData reference +"+entryRef);
+      InputStream entry = docreader.getAppdataStream(entryRef);
+      if (entry!=null) {
+        if (entry instanceof JarInputStream) {
+          return (JarInputStream) entry;
+        }
+        log.warn("Implementation problem - docreader didn't return a JarInputStream entry.");
+      }
+    } else {
+      log.debug("GetAppDataStream called for an AppData without a data reference.");
+    }
+    return null;
+  }
+  /**
+   * yuk - size of buffer used for slurping appData JarEntry into a byte array.
+   */
+  private final int _TRANSFER_BUFFER=4096*4;
+
+  /**
+   * Resolve AppData object to a byte array.
+   * @param appdata
+   * @param archiveReader
+   * @return null or the application data as a byte array
+   */
+  private byte[] getAppDataAsByteArray(AppData appdata, VamsasArchiveReader docreader) {
+    if (appdata.getData()==null) {
+      if (docreader==null) {
+        log.warn("Silently failing getAppDataAsByteArray with null docreader.",new Exception());
+        return null;
+      }
+      // resolve and load data
+      JarInputStream entry = getAppDataStream(appdata, docreader); 
+      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+      try {
+        byte buff[] = new byte[_TRANSFER_BUFFER];
+        int olen=0;
+        while (entry.available()>0) {
+          int len = entry.read(buff, olen, _TRANSFER_BUFFER);
+          bytes.write(buff, 0, len);
+          olen+=len;
+        }
+        buff=null;
+      } catch (Exception e) {
+        log.warn("Unexpected exception - probable truncation when accessing VamsasDocument entry "+appdata.getDataReference(), e);
+      }
+      if (bytes.size()>0) {
+        // LATER: deal with probable OutOfMemoryErrors here
+        log.debug("Got "+bytes.size()+" bytes from AppDataReference "+appdata.getDataReference());
+        byte data[] = bytes.toByteArray();
+        bytes = null;
+        return data;
+      }
+      return null;
+    } else {
+      log.debug("Returning inline AppData block for "+appdata.getVorbaId());
+      return appdata.getData();
+    }
+  }
+  /**
+   * internal method for getting a DataInputStream from an AppData object.
+   * @param appdata
+   * @param docreader
+   * @return data in object or null if no data is accessible
+   */
+  private DataInput getAppDataAsDataInputStream(AppData appdata, VamsasArchiveReader docreader) {
+    if (appdata!=null && docreader!=null) {
+      String entryRef = appdata.getDataReference();
+      if (entryRef!=null) {
+        log.debug("Resolving AppData reference for "+entryRef);
+        InputStream jstrm = docreader.getAppdataStream(entryRef);
+        if (jstrm!=null)
+          return new AppDataInputStream(jstrm);
+        else {
+          log.debug("Returning null input stream for unresolved reference ("+entryRef+") id="+appdata.getVorbaId());
+          return null;
+        }
+      } else {
+        // return a byteArray input stream
+        byte[] data=appdata.getData();
+        if (data.length>0) {
+          ByteArrayInputStream stream = new ByteArrayInputStream(data);
+          return new DataInputStream(stream);
+        } else {
+          log.debug("Returning null input stream for empty Appdata data block in id="+appdata.getVorbaId());
+          return null;
+        }
+      }
+    } else {
+      log.debug("Returning null DataInputStream for appdata entry:"+appdata.getVorbaId());
+    }
+    return null;
+  }
+
+  /**
+   * internal method for getting ByteArray from AppData object
+   * @param clientOrUser - true for returning userData, otherwise return Client AppData.
+   * @return null or byte array
+   */
+  private byte[] _getappdataByteArray(boolean clientOrUser) {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    byte[] data=null;
+    String appdName;
+    if (!clientOrUser) {
+      appdName = "Client's Appdata";
+    } else {
+      appdName = "User's Appdata";
+    }    
+    log.debug("getting "+appdName+" as a byte array");
+    extractAppData(clientdoc.getVamsasDocument());
+    AppData object;
+    if (!clientOrUser) {
+      object = appsGlobal;
+    } else {
+      object = usersData;
+    }
+    if (object!=null) {
+      log.debug("Trying to resolve "+appdName+" object to byte array.");
+      data = getAppDataAsByteArray(object, clientdoc.getVamsasArchiveReader());
+    }
+    if (data == null)
+      log.debug("Returning null for "+appdName+"ClientAppdata byte[] array");
+    return data;
+    
+  }
+  
+  /**
+   * common method for Client and User AppData->InputStream accessor
+   * @param clientOrUser - the appData to resolve - false for client, true for user appdata.
+   * @return null or the DataInputStream desired.
+   */
+  private DataInput _getappdataInputStream(boolean clientOrUser) {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    String appdName;
+    if (!clientOrUser) {
+      appdName = "Client's Appdata";
+    } else {
+      appdName = "User's Appdata";
+    }
+    if (log.isDebugEnabled())
+      log.debug("getting "+appdName+" as an input stream.");
+    extractAppData(clientdoc.getVamsasDocument());
+    AppData object;
+    if (!clientOrUser) {
+      object = appsGlobal;
+    } else {
+      object = usersData;
+    }
+    if (object!=null) {
+      log.debug("Trying to resolve ClientAppdata object to an input stream.");
+      return getAppDataAsDataInputStream(object, clientdoc.getVamsasArchiveReader());
+    }
+    log.debug("getClientInputStream returning null.");
+    return null;
+  }
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#getClientAppdata()
+   */
+  public byte[] getClientAppdata() {
+    return _getappdataByteArray(false);
+  }
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#getClientInputStream()
+   */
+  public DataInput getClientInputStream() {
+    return _getappdataInputStream(false);
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#getUserAppdata()
+   */
+  public byte[] getUserAppdata() {
+    return _getappdataByteArray(true);
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#getUserInputStream()
+   */
+  public DataInput getUserInputStream() {
+    return _getappdataInputStream(true);
+  }
+  /**
+   * methods for writing new AppData entries.
+   */
+  private DataOutput _getAppdataOutputStream(boolean clientOrUser) {
+    String apdname;
+    SessionFile apdfile=null;
+    if (!clientOrUser) {
+      apdname = "clientAppData";
+      apdfile = newAppData;
+    } else {
+      apdname = "userAppData";
+      apdfile = newUserData;
+    }
+    try {
+      if (apdfile==null) {
+        apdfile=clientdoc.sclient._session.getTempSessionFile(apdname,".jar");
+        log.debug("Successfully made temp appData file for "+apdname);
+      } else {
+        // truncate to remove existing data.
+        apdfile.fileLock.getRaFile().setLength(0);
+        log.debug("Successfully truncated existing temp appData for "+apdname);
+      }
+      } catch (Exception e) {
+      log.error("Whilst opening temp file in directory "+clientdoc.sclient._session.sessionDir, e);
+    }
+    // we do not make another file for the new entry if one exists already
+    if (!clientOrUser) {
+      newAppData = apdfile;
+    } else {
+      newUserData = apdfile;
+    }
+    try {
+      apdfile.lockFile();
+      // LATER: Refactor these local AppDatastream IO stuff to their own class.
+      JarOutputStream dstrm = 
+        new JarOutputStream(apdfile.fileLock.getBufferedOutputStream(true));
+      if (!clientOrUser) {
+        newAppDataStream = dstrm;
+      } else {
+        newUserDataStream = dstrm;
+      }
+      dstrm.putNextEntry(new JarEntry("appData_entry.dat"));
+      // LATER: there may be trouble ahead if an AppDataOutputStream is written to by one thread when another truncates the file. This situation should be prevented if possible
+      return new AppDataOutputStream(dstrm);
+    }
+    catch (Exception e) {
+      log.error("Whilst opening jar output stream for file "+apdfile.sessionFile);
+    }
+    // tidy up and return null
+    apdfile.unlockFile();
+    return null;
+   }
+  /**
+   * copy data from the appData jar file to an appropriately 
+   * referenced jar or Data entry for the given ApplicationData
+   * Assumes the JarFile is properly closed. 
+   * @param vdoc session Document handler
+   * @param appd the AppData whose block is being updated
+   * @param apdjar the new data in a Jar written by this class
+   */
+  protected void updateAnAppdataEntry(VamsasArchive vdoc, AppData appd, SessionFile apdjar) throws IOException {
+    if (apdjar==null || apdjar.sessionFile==null || !apdjar.sessionFile.exists()) {
+      throw new IOException("No temporary Appdata to recover and transfer.");
+    }
+    if (vdoc==null) {
+      log.fatal("FATAL! NO DOCUMENT TO WRITE TO!");
+      throw new IOException("FATAL! NO DOCUMENT TO WRITE TO!");
+    }
+    log.debug("Recovering AppData entry from "+apdjar.sessionFile);
+    JarInputStream istrm = new JarInputStream(apdjar.getBufferedInputStream(true));
+    JarEntry je=null;
+    while (istrm.available()>0 && (je=istrm.getNextJarEntry())!=null && !je.getName().equals("appData_entry.dat")) {
+      if (je!=null)
+        log.debug("Ignoring extraneous entry "+je.getName());
+    }
+    if (istrm.available()>0 && je!=null) {
+      log.debug("Found appData_entry.dat in Jar");
+      String ref = appd.getDataReference();
+      if (ref==null) {
+        throw new IOException("Null AppData.DataReference passed.");
+      }
+      if (vdoc.writeAppdataFromStream(ref, istrm)) {
+        log.debug("Entry updated successfully.");
+      } else {
+        throw new IOException("writeAppdataFromStream did not return true - expect future badness."); // LATER - verify why this might occur.
+      }
+    } else {
+      throw new IOException("Couldn't find appData_entry.dat in temporary jar file "+apdjar.sessionFile.getAbsolutePath());
+    }
+    istrm.close();
+  }
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#getClientOutputStream()
+   */
+  public DataOutput getClientOutputStream() {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    if (log.isDebugEnabled())
+      log.debug("trying to getClientOutputStream for "+clientdoc.sclient.client.getClientUrn());   
+    return _getAppdataOutputStream(false);
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#getUserOutputStream()
+   */
+  public DataOutput getUserOutputStream() {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    if (log.isDebugEnabled())
+      log.debug("trying to getUserOutputStream for ("
+          +clientdoc.sclient.getUserHandle().getFullName()+")"+clientdoc.sclient.client.getClientUrn());   
+    return _getAppdataOutputStream(true);
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#hasClientAppdata()
+   */
+  public boolean hasClientAppdata() {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    extractAppData(clientdoc.getVamsasDocument());
+    // LATER - check validity of a DataReference before we return true
+    if ((appsGlobal!=null) && (appsGlobal.getDataReference()!=null || appsGlobal.getData()!=null))
+      return true;
+    return false;
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#hasUserAppdata()
+   */
+  public boolean hasUserAppdata() {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    extractAppData(clientdoc.getVamsasDocument());
+    // LATER - check validity of a DataReference before we return true
+    if ((appsGlobal!=null) && (appsGlobal.getDataReference()!=null || appsGlobal.getData()!=null))
+      return true;
+    return false;
+  }
+  private boolean _writeAppDataStream(JarOutputStream ostrm, byte[] data) {
+    try {
+      if (data!=null && data.length>0) 
+        ostrm.write(data);
+      ostrm.closeEntry();
+      return true;
+    }
+    catch (Exception e) {
+      log.error("Serious! - IO error when writing AppDataStream to file "+newAppData.sessionFile, e);
+    }
+    return false;
+  }
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#setClientAppdata(byte[])
+   */
+  public void setClientAppdata(byte[] data) {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    _getAppdataOutputStream(false);
+    if (newAppDataStream==null) {
+      // LATER: define an exception for this ? - operation may fail even if file i/o not involved
+      log.error("Serious! - couldn't open new AppDataStream in session directory "+clientdoc.sclient._session.sessionDir);
+    } else {
+      _writeAppDataStream(newAppDataStream, data);
+      // LATER: deal with error case - do we make session read only, or what ?
+    }
+  }
+
+  /* (non-Javadoc)
+   * @see org.vamsas.client.IClientAppdata#setUserAppdata(byte[])
+   */
+  public void setUserAppdata(byte[] data) {
+    if (clientdoc==null)
+      throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
+    _getAppdataOutputStream(true);
+    if (newUserDataStream==null) {
+      // LATER: define an exception for this ? - operation may fail even if file i/o not involved
+      log.error("Serious! - couldn't open new UserDataStream in session directory "+clientdoc.sclient._session.sessionDir);
+    } else {
+      _writeAppDataStream(newUserDataStream, data);
+      // LATER: deal with error case - do we make session read only, or what ?
+    }
+  }
+  /**
+   * flush and close outstanding output streams. 
+   *  - do this before checking data length.
+   * @throws IOException
+   */
+  protected void closeForWriting() throws IOException {
+    if (newAppDataStream!=null) {
+      newAppDataStream.flush();
+      newAppDataStream.closeEntry();
+      newAppDataStream.close();
+    }
+    if (newUserDataStream!=null) {
+      newUserDataStream.flush();
+      newUserDataStream.closeEntry();
+      newUserDataStream.close();
+    }
+  }
+
+
+  /**
+   * 
+   * @return true if any AppData blocks have to be updated in session Jar
+   */
+  protected boolean isModified() {
+    // LATER differentiate between core xml modification and Jar Entry modification.
+    if (newAppData.sessionFile.exists() || newUserData.sessionFile.exists())
+      return true;
+    return false;
+  }
+  /* (non-Javadoc)
+   * @see java.lang.Object#finalize()
+   */
+  protected void finalize() throws Throwable {
+    if (newAppDataStream!=null) {
+      newAppDataStream = null;
+    }
+    if (newAppDataStream!=null) {
+      newUserDataStream = null;
+    }
+    if (newAppData!=null) {
+      newAppData.eraseExistence();
+      newAppData = null;
+    }
+    if (newUserData!=null) {
+      newUserData.eraseExistence();
+      newUserData = null;
+    }
+    super.finalize();
+  }
+
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SimpleClientFactory.java b/src/uk/ac/vamsas/client/simpleclient/SimpleClientFactory.java
new file mode 100644 (file)
index 0000000..cd05e26
--- /dev/null
@@ -0,0 +1,262 @@
+/*
+* VAMSAS Project
+*
+
+* 
+* Dec 13, 2006 
+*
+*/
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+
+import java.io.IOException;
+
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.ClientHandle;
+import org.vamsas.client.IClient;
+import org.vamsas.client.IClientFactory;
+import org.vamsas.client.InvalidSessionUrnException;
+import org.vamsas.client.NoDefaultSessionException;
+import org.vamsas.client.SessionHandle;
+import org.vamsas.client.UserHandle;
+
+/**
+ * 
+ * 
+ */
+public class SimpleClientFactory implements IClientFactory {
+
+  private static Log log = LogFactory.getLog(SimpleClientFactory.class);
+
+  private File sessionArena = null;
+  
+  private String vamsasSubdirectoryName = ".vamsas";
+  
+  private SessionsFile sessionFile = null;
+  private static final String SESSION_LIST="sessions.obj";
+  
+  //private   String[] currentlyAvailableDessions = null; 
+  
+  /**
+   * default constructor - called by CreateClientFactory only.
+   *
+   *Inits the sessionarena to the directory .vamsas of the user home directory. 
+   *
+   */
+  public SimpleClientFactory() throws IOException
+  {
+   // sessionArena
+    
+    //retrieves user home directory
+    String userHomeDirectory = System.getProperty("user.home");
+    if (userHomeDirectory == null || userHomeDirectory.length()<1)
+      {
+        new IOException("Unable to detect user home directory");
+      }
+    String sessionArenaPath =  userHomeDirectory.concat(File.separator.concat(this.vamsasSubdirectoryName));
+    
+    this.initSessionArena(sessionArenaPath);
+    this.initFactoryObjects();
+  }
+  
+  
+  /**
+   * Create a client factory that works with sessions at the given
+   * path.
+   * @param path path to directory called  session arena, where will be created session directories and session files.
+   */
+  public SimpleClientFactory(String path) throws IOException
+  {
+    this.initSessionArena(path);
+  }
+  /**
+   * Inits sessionArena to a given path.
+   * checks if path is valid.
+   * 
+   * @param path path to a directory to use 
+   * @throws IOException if the path is incorrect
+   */
+  private void  initSessionArena (String path) throws IOException
+  {
+    // Check path is valid and read/writeable.
+    File arenaFile = new File (path);
+    if (!arenaFile.exists())
+    {
+      if (! arenaFile.mkdirs())
+      {
+        this.sessionArena = null;
+        throw(new IOException("Unable to create a directory called "+path));
+      }
+    }
+    if (arenaFile.exists() && arenaFile.isDirectory() && arenaFile.canRead() && arenaFile.canWrite()) 
+      {
+        this.sessionArena = arenaFile;
+      } 
+    else
+      {
+      this.sessionArena = null;
+        throw(new IOException("Cannot read and write to a directory called "+path));
+      }
+  }
+  
+  /**
+   * construct SessionFile objects and watchers for each
+   */
+  private void initFactoryObjects() throws IOException {
+    if (this.sessionFile!=null )
+      throw new IOException("initFactoryObjects called for initialised ClientFactory object.");
+    this.sessionFile = new SessionsFile(new File(this.sessionArena,SESSION_LIST));
+
+  }
+  /**
+   * @see org.vamsas.client.IClientFactory#getCurrentSessions()
+   */
+  public String[] getCurrentSessions() 
+  { 
+    String[] sessions = null;
+    if (this.sessionFile!=null )
+      {
+        SessionHandle[] sessionHandles =  this.sessionFile.retrieveSessionsList();
+        if (sessionHandles != null)
+          {
+            sessions = new String[sessionHandles.length];
+            for (int i = sessionHandles.length -1; i > 0; i--)
+              {
+                SessionHandle sessionHandle = sessionHandles[i];
+                sessions [i] = sessionHandle.getSessionUrn();
+              }
+          }
+      }
+    return sessions;
+  }
+  
+  
+  private void discoverSession()
+  {
+   
+  }
+
+  /**
+   * @see org.vamsas.client.IClientFactory#getIClient(org.vamsas.client.ClientHandle)
+   * 
+   * Creates a IClient object, using default UserHandle with system variables:"user.name" or "USERNAME")),
+            "host.name" or "HOSTNAME" 
+   */
+  public IClient getIClient(ClientHandle applicationHandle)
+      throws NoDefaultSessionException {
+    
+    return this.getIClient(applicationHandle, (UserHandle) null);
+  }
+
+  /**
+   * @see org.vamsas.client.IClientFactory#getIClient(org.vamsas.client.ClientHandle, java.lang.String)
+   */
+  public IClient getIClient(ClientHandle applicationHandle, String sessionUrn) {
+    // TODO Auto-generated method stub
+    return null;
+  }
+
+  /**
+   * @see org.vamsas.client.IClientFactory#getIClient(org.vamsas.client.ClientHandle, org.vamsas.client.UserHandle, java.lang.String)
+   */
+  public IClient getIClient(ClientHandle applicationHandle, UserHandle userId,
+      String sessionUrn) {
+    // TODO Auto-generated method stub
+    return null;
+  }
+
+  /**
+   * @see org.vamsas.client.IClientFactory#getIClient(org.vamsas.client.ClientHandle, org.vamsas.client.UserHandle)
+   */
+  public IClient getIClient(ClientHandle applicationHandle, UserHandle userId)
+      throws NoDefaultSessionException {
+    SimpleClient client = null;
+    if (this.sessionArena==null)
+      throw new Error("Improperly initialised SimpleClientFactory object - null sessionArena.");
+    
+    ClientHandle clientHandle =applicationHandle;
+    //create default clientHandle with "SimpleVamsasClientApp","0.1",
+    if (clientHandle == null)
+     clientHandle = new ClientHandle("SimpleVamsasClientApp","0.1");
+    
+    //check if any available session(s)
+    String[] availableSessions = this.getCurrentSessions();
+    if (availableSessions != null) 
+      {//there are available sessions
+        if (availableSessions.length>1)
+          {//more than one session if available... can not choose
+          
+          //represents list of session as String
+            StringBuffer sessionURNs = new StringBuffer("");
+            for (int i = 0; i< availableSessions.length ; i++)
+              {
+                sessionURNs.append(availableSessions[i]+" ");
+              }
+            throw new  NoDefaultSessionException("Several sessions available, please pick one: "+sessionURNs);
+          }
+      
+        //check if only one session available. if yes, open it
+        if (availableSessions.length == 1)
+          {
+          //only one session available, open it.
+            return this.getIClient(clientHandle,  availableSessions[0]);
+          }
+      }
+    //no session available  - create a new one
+    
+    
+    try 
+      {
+        //create sessionDirectory
+        File sessdir = File.createTempFile("sess", ".simpleclient", this.sessionArena);
+        log.debug("Creating new session  directory");
+       if (!(sessdir.delete() && sessdir.mkdir()))
+          throw new IOException("Could not make session directory "+sessdir);
+      //create session
+        VamsasSession vamsasSession = new VamsasSession(sessdir);
+      
+        this.getSessionFile().addSession(new SessionHandle(new SessionUrn(vamsasSession).getSessionUrn()), false);
+        if (userId == null)
+          {
+      //create a default userHandle
+      //with current OS user and hostname
+            userId = new UserHandle(System.getProperty("user.name", System.getProperty("USERNAME","Joe Doe")),
+              System.getProperty("host.name",System.getProperty("HOSTNAME", "Unknown") ));// clientName, clientVersion,  sessionPath);
+          }
+     
+      
+      //create simple client
+         client = new SimpleClient(userId,  clientHandle,  vamsasSession);
+      } 
+    catch (IOException e) 
+      {
+        log.error("error while creating new IClient",e);
+      }
+    catch (InvalidSessionUrnException e) 
+      {
+        log.error("Unable to create new IClient. The session urn is incorrect ",e);
+      }
+   
+      return client;
+  }
+
+
+  /**
+   * @return the sessionFile
+   */
+  private SessionsFile getSessionFile()  throws IOException
+    {
+      if (this.sessionFile == null)
+        {
+          this.sessionFile = new SessionsFile( new File (this.sessionArena, SESSION_LIST));
+        }
+      return this.sessionFile;
+    }
+
+  
+
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SimpleDocBinding.java b/src/uk/ac/vamsas/client/simpleclient/SimpleDocBinding.java
new file mode 100644 (file)
index 0000000..9eee917
--- /dev/null
@@ -0,0 +1,138 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Vector;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.Vobject;
+import org.vamsas.client.VorbaIdFactory;
+import org.vamsas.client.VorbaXmlBinder;
+import org.vamsas.objects.core.VAMSAS;
+import org.vamsas.objects.core.VamsasDocument;
+import org.vamsas.objects.utils.AppDataReference;
+import org.vamsas.objects.utils.DocumentStuff;
+import org.vamsas.objects.utils.ProvenanceStuff;
+import org.vamsas.objects.utils.document.VersionEntries;
+
+/**
+ * Base class for SimpleClient Vamsas Document Object Manipulation
+ * holds static vamsasDocument from XML routines and
+ * state objects for a particular unmarshalled Document instance.
+ * @author jimp
+ */
+  
+
+public class SimpleDocBinding {
+
+  protected VorbaIdFactory vorba;
+  protected static Log log = LogFactory.getLog(SimpleDocBinding.class);
+
+  /**
+   * @return Returns the vorba.
+   */
+  public VorbaIdFactory getVorba() {
+    return vorba;
+  }
+
+  /**
+   * @param vorba The vorba to set.
+   */
+  public void setVorba(VorbaIdFactory vorba) {
+    this.vorba = vorba;
+  }
+
+  /**
+   * Uses VorbaXmlBinder to retrieve the VamsasDocument from the given stream
+   */
+  public VamsasDocument getVamsasDocument(VamsasArchiveReader oReader) throws IOException, org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    if (oReader!=null) {
+      // check the factory
+      if (vorba==null) {
+        log.error("Invalid SimpleDocument construction - no VorbaIdFactory defined!");
+        return null;
+      }
+  
+      if (oReader.isValid()) {
+        // Read vamsasDocument.xsd instance
+        InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
+        Object unmarsh[] = VorbaXmlBinder.getVamsasObjects(vdoc, vorba, new VamsasDocument());
+        if (unmarsh==null)
+          log.fatal("Couldn't unmarshall document!");
+        
+        Vobject vobjs = (Vobject) unmarsh[0];
+        if (vobjs!=null) { 
+          VamsasDocument doc=(VamsasDocument) vobjs;
+          if (doc!=null)
+            return doc;
+        }
+        log.debug("Found no VamsasDocument object in properly formatted Vamsas Archive.");
+      } else {        
+        // deprecated data handler (vamsas.xsd instance)
+        InputStream vxmlis = oReader.getVamsasXmlStream();
+        if (vxmlis!=null) { // Might be an old vamsas file.
+          BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
+          InputStreamReader vxml = new InputStreamReader(ixml);
+          Object unmarsh[] = VorbaXmlBinder.getVamsasObjects(vxml, vorba, new VAMSAS());
+          
+          if (unmarsh==null)
+            log.fatal("Couldn't unmarshall document!");
+          
+          VAMSAS root[]= new VAMSAS[] { null};
+          root[0] = (VAMSAS) unmarsh[0]; 
+          
+          if (root[0]==null) {
+            log.debug("Found no VAMSAS object in VamsasXML stream.");
+          } else {
+            log.debug("Making new VamsasDocument from VamsasXML stream.");
+            VamsasDocument doc = DocumentStuff.newVamsasDocument(root, 
+                ProvenanceStuff.newProvenance(
+                    vorba.getUserHandle().getFullName(), 
+                    "Vamsas Document constructed from vamsas.xml"), VersionEntries.ALPHA_VERSION);
+            // VAMSAS: decide on 'system' operations provenance form
+            // LATER: implement classes for translating Vorba properties into provenance user fields.
+            // VAMSAS: decide on machine readable info embedding in provenance should be done
+            root[0]=null;
+            root=null;
+            return doc;
+          }
+        }
+      }
+    }
+    // otherwise - there was no valid original document to read.
+    return null;    
+  }
+
+  /**
+   * Extract all jarEntries in an archive referenced by the vamsas document
+   * LATER: a family of methods for finding extraneous jarEntries , and invalid appDataReferences
+   * @param doc
+   * @param oReader
+   * @return array of the subset of JarEntry names that are referenced in doc 
+   */
+  public Vector getReferencedEntries(VamsasDocument doc, VamsasArchiveReader oReader) {
+    if (oReader==null)
+     return null;
+    if (doc==null) {
+     try { doc = getVamsasDocument(oReader); } 
+     catch (Exception e) { log.warn("Failed to get document from "+oReader.jfile.getName()); };
+    }
+    Vector docrefs = AppDataReference.getAppDataReferences(doc);
+    if (docrefs==null)
+      return null;
+    Vector entries = oReader.getExtraEntries();
+    if (entries!=null && docrefs.size()>0) {
+      int i=0, j=entries.size();
+      do {
+        if (!docrefs.contains(entries.get(i))) {
+            entries.remove(i);
+            j--;
+        } else
+          i++;
+      } while (i<j);
+    }
+    return entries;
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/SimpleDocument.java b/src/uk/ac/vamsas/client/simpleclient/SimpleDocument.java
new file mode 100644 (file)
index 0000000..6a0d198
--- /dev/null
@@ -0,0 +1,28 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.VorbaIdFactory;
+
+/**
+ * @see SimpleDocBinding
+ * Additional constructors for 'headless' Vamsas Document access
+ * @author jimp
+ */
+public class SimpleDocument extends SimpleDocBinding {
+  private static Log log = LogFactory.getLog(SimpleDocument.class);
+  
+  private VorbaIdFactory makeDefaultFactory(String name) {
+    return IdFactory.getDummyFactory(name);
+  }
+
+  public SimpleDocument(String name) {
+    vorba = makeDefaultFactory(name);
+  }
+  public SimpleDocument(VorbaIdFactory Vorba) {
+    if (Vorba!=null)
+      vorba = Vorba;
+    else
+      log.error("Invalid SimpleDocument construction - no VorbaIdFactory defined!");
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/VamsasArchive.java b/src/uk/ac/vamsas/client/simpleclient/VamsasArchive.java
new file mode 100644 (file)
index 0000000..a94b688
--- /dev/null
@@ -0,0 +1,809 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Vector;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.client.ClientHandle;
+import org.vamsas.client.IVorbaIdFactory;
+import org.vamsas.client.SessionHandle;
+import org.vamsas.client.UserHandle;
+import org.vamsas.client.VorbaIdFactory;
+import org.vamsas.client.VorbaXmlBinder;
+import org.vamsas.client.Vobject;
+import org.vamsas.objects.core.ApplicationData;
+import org.vamsas.objects.core.VAMSAS;
+import org.vamsas.objects.core.VamsasDocument;
+import org.vamsas.objects.utils.AppDataReference;
+import org.vamsas.objects.utils.DocumentStuff;
+import org.vamsas.objects.utils.ProvenanceStuff;
+import org.vamsas.objects.utils.document.VersionEntries;
+
+/**
+ * Class for high-level io and Jar manipulation involved in creating 
+ * or updating a vamsas archive (with backups).
+ * Writes to a temporary file and then swaps new file for backup.
+ * uses the sessionFile locking mechanism for safe I/O
+ * @author jimp
+ *
+ */
+public class VamsasArchive {
+  private static Log log = LogFactory.getLog(VamsasArchive.class);
+  /**
+   * Access original document if it exists, and get VAMSAS root objects.
+   * @return vector of vamsas roots from original document
+   * @throws IOException
+   */
+  public static Vobject[] getOriginalRoots(VamsasArchive ths) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
+    if (oReader!=null) {
+      
+      if (oReader.isValid()) {
+        InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
+        VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
+        if (doc!=null) 
+          return doc.getVAMSAS();
+        // TODO ensure embedded appDatas are garbage collected to save memory
+      } else {
+        InputStream vxmlis = oReader.getVamsasXmlStream();
+        if (vxmlis!=null) { // Might be an old vamsas file.
+          BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
+          InputStreamReader vxml = new InputStreamReader(ixml);
+          VAMSAS root[] = new VAMSAS[1];
+          root[0] = VAMSAS.unmarshal(vxml);
+          if (root[0]!=null)
+            return root;
+        }
+      }
+    }
+    return null;
+  }
+  /**
+   * Access the original vamsas document for a VamsasArchive class, and return it.
+   * Users of the VamsasArchive class should use the getVamsasDocument method to retrieve
+   * the current document - only use this one if you want the 'backup' version.
+   * TODO: catch OutOfMemoryError - they are likely to occur here.
+   * NOTE: vamsas.xml datastreams are constructed as 'ALPHA_VERSION' vamsas documents.
+   * @param ths
+   * @return null if no document exists.
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   */
+  public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    return VamsasArchive.getOriginalVamsasDocument(ths, null);
+  } 
+  /**
+   * Uses VorbaXmlBinder to retrieve the VamsasDocument from the original archive referred to by ths
+   * @param ths
+   * @param vorba
+   * @return
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   */
+  public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths, VorbaIdFactory vorba) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
+    if (oReader!=null) {
+      ths.setVorba(vorba);
+      return ths.vorba.getVamsasDocument(oReader);
+    }
+    // otherwise - there was no valid original document to read.
+    return null;    
+  }
+  /**
+   * destination of new archive data (tempfile if virginarchive=true, original archive location otherwise)
+   */
+  java.io.File archive=null;
+  /**
+   * locked IO handler for new archive file
+   */
+  SessionFile rchive=null;
+  /**
+   * original archive file to be updated (or null if virgin) where new data will finally reside
+   */
+  java.io.File original=null;
+  /**
+   * original archive IO handler
+   */
+  SessionFile odoclock = null;
+  Lock destinationLock = null;
+  /**
+   * Original archive reader class
+   */
+  VamsasArchiveReader odoc = null;
+  /**
+   * true if a real vamsas document is being written.
+   */
+  boolean vamsasdocument=true;
+  /**
+   * Output stream for archived data
+   */
+  JarOutputStream newarchive=null;
+  /**
+   * JarEntries written to archive
+   */
+  Hashtable entries = null;
+  
+  /**
+   * true if we aren't just updating an archive
+   */
+  private boolean virginArchive=false;
+  
+  /**
+   * name of backup of existing archive that has been updated/overwritten.
+   * only one backup will be made - and this is it.
+   */
+  File originalBackup = null;
+  
+  boolean donotdeletebackup=false;
+  private final int _TRANSFER_BUFFER=4096*4;
+  protected SimpleDocument vorba = null;
+  /**
+   * LATER: ? CUT'n'Paste error ?
+   * Access and return current vamsas Document, if it exists, or create a new one 
+   * (without affecting VamsasArchive object state - so is NOT THREAD SAFE)
+   * _TODO: possibly modify internal state to lock low-level files 
+   * (like the IClientDocument interface instance constructer would do) 
+   * @see org.vamsas.simpleclient.VamsasArchive.getOriginalVamsasDocument for additional caveats
+   * 
+   * @return
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   * ????? where does this live JBPNote ?
+   */
+  private VamsasDocument _doc=null;
+  
+  /**
+   * Create a new vamsas archive
+   * File locks are made immediately to avoid contention
+   *  
+   * @param archive - file spec for new vamsas archive
+   * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive
+   * @throws IOException if call to accessOriginal failed for updates, or openArchive failed.
+   */
+  public VamsasArchive(File archive, boolean vamsasdocument) throws IOException {
+    this(archive, false, vamsasdocument, null);
+  }
+  public VamsasArchive(File archive, boolean vamsasdocument, boolean overwrite) throws IOException {
+    this(archive, overwrite, vamsasdocument, null);
+  }
+  /**
+   * Constructor for accessing Files under file-lock management (ie a session file)
+   * @param archive
+   * @param vamsasdocument
+   * @param overwrite
+   * @throws IOException
+   */
+  public VamsasArchive(VamsasFile archive, boolean vamsasdocument, boolean overwrite) throws IOException {
+    this(archive.sessionFile, overwrite, vamsasdocument, archive);
+    // log.debug("using non-functional lock-IO stream jar access constructor");
+  }
+  /**
+   * read and write to archive - will not overwrite original contents, and will always write an up to date vamsas document structure.
+   * @param archive
+   * @throws IOException
+   */
+  public VamsasArchive(VamsasFile archive) throws IOException {
+    this(archive, true, false); 
+  }
+  /**
+   * 
+   * @param archive file to write
+   * @param overwrite true if original contents should be deleted
+   * @param vamsasdocument true if a proper VamsasDocument archive is to be written.
+   * @param extantLock SessionFile object holding a lock for the <object>archive</object> 
+   * @throws IOException
+   */
+  public VamsasArchive(File archive, boolean overwrite, boolean vamsasdocument, SessionFile extantLock) throws IOException {
+    super();
+    if (archive==null || (archive!=null && !(archive.getAbsoluteFile().getParentFile().canWrite() && (!archive.exists() || archive.canWrite())))) {
+      log.fatal("Expect Badness! -- Invalid parameters for VamsasArchive constructor:"+((archive!=null) 
+          ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
+      return;
+    }
+    
+    this.vamsasdocument = vamsasdocument;
+    if (archive.exists() && !overwrite) {
+      this.original = archive;
+      if (extantLock!=null) {
+        this.odoclock = extantLock;
+        if (odoclock.fileLock==null || !odoclock.fileLock.isLocked())
+          odoclock.lockFile();
+      } else { 
+        this.odoclock = new SessionFile(archive);
+      }
+      odoclock.lockFile(); // lock the file *immediatly*
+      this.archive = null;       // archive will be a temp file when the open method is called
+      virginArchive=false;
+      try {
+        this.accessOriginal();
+      } catch (IOException e)  {
+        throw new IOException("Lock failed for existing archive"+archive);
+      }
+    } else {
+      this.original = null;
+      this.archive = archive; // archive is written in place.
+      if (extantLock!=null)
+        rchive=extantLock;
+      else
+        rchive = new SessionFile(archive);
+      rchive.lockFile();
+      if (rchive.fileLock==null || !rchive.fileLock.isLocked())
+        throw new IOException("Lock failed for new archive"+archive);
+      rchive.fileLock.getRaFile().setLength(0); // empty the archive.
+      virginArchive = true;
+    }
+    this.openArchive(); // open archive
+  }
+  /**
+   * open original archive file for exclusive (locked) reading.
+   * @throws IOException
+   */
+  private void accessOriginal() throws IOException {
+    if (original!=null && original.exists()) {
+      if (odoclock==null) 
+        odoclock = new SessionFile(original);
+      odoclock.lockFile();
+      if (odoc == null) 
+        odoc = new VamsasArchiveReader(original);
+        // this constructor is not implemented yet odoc = new VamsasArchiveReader(odoclock.fileLock);
+    }
+  }
+  
+  /**
+   * Add unique entry strings to internal JarEntries list.
+   * @param entry
+   * @return true if entry was unique and was added.
+   */
+  private boolean addEntry(String entry) {
+    if (entries!=null)
+      entries=new Hashtable();
+    if (entries.containsKey(entry))
+      return false;
+    entries.put(entry, new Integer(entries.size()));
+    return true;
+  }
+  /**
+   * adds named entry to newarchive or returns false.
+   * @param entry
+   * @return true if entry was unique and could be added
+   * @throws IOException if entry name was invalid or a new entry could not be made on newarchive
+   */
+  private boolean addValidEntry(String entry) throws IOException {
+    JarEntry je = new JarEntry(entry);
+    if (!addEntry(entry))
+      return false;
+    newarchive.flush();
+    newarchive.putNextEntry(je);
+    return true;
+  }
+  /**
+   * called by app to get name of backup if it was made.
+   * If this is called, the caller app *must* delete the backup themselves.
+   * @return null or a valid file object
+   */
+  public File backupFile() {
+    
+    if (!virginArchive) {
+      makeBackup();
+      donotdeletebackup=false; // external reference has been made.
+      return ((original!=null) ? originalBackup : null);
+    }
+    return null;
+  }
+  
+  /**
+   * Stops any current write to archive, and reverts to the backup if it exists.
+   * All existing locks on the original will be released. All backup files are removed.
+   */
+  public boolean cancelArchive() {
+    if (newarchive!=null) {
+      try { 
+        newarchive.closeEntry();
+        newarchive.putNextEntry(new JarEntry("deleted"));
+        newarchive.closeEntry();
+        newarchive.close();
+        
+      } catch (Exception e) {
+        log.debug("Whilst closing newarchive",e);
+      };
+      if (!virginArchive) {
+        // then there is something to recover.
+        try {
+          recoverBackup();
+        }
+        catch (Exception e) {
+          log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
+          return false;
+        }
+      }
+      
+    } else {
+      log.warn("Client Error: cancelArchive called before archive("+original.getAbsolutePath()+") has been opened!");
+    }
+    closeAndReset(); // tidy up and release locks.
+    return true;
+  }
+  
+  /**
+   * only do this if you want to destroy the current file output stream
+   *
+   */
+  private void closeAndReset() {
+    if (rchive!=null) {
+      rchive.unlockFile();
+      rchive=null;
+    }
+    if (original!=null) {
+      if (odoc!=null) {
+        odoc.close();
+        odoc=null;
+      }
+      if (archive!=null)
+        archive.delete();
+      if (odoclock!=null) {
+        odoclock.unlockFile();
+        odoclock = null;
+      }
+    }
+    removeBackup();
+    newarchive=null;
+    original=null;
+    entries=null;
+  }
+  /**
+   * Tidies up and closes archive, removing any backups that were created.
+   * NOTE: It is up to the caller to delete the original archive backup obtained from backupFile()
+   * TODO: ensure all extant AppDataReference jar entries are transferred to new Jar
+   * TODO: provide convenient mechanism for generating new unique AppDataReferences and adding them to the document
+   */
+  public void closeArchive() throws IOException {
+    if (newarchive!=null) {
+      newarchive.flush();
+      newarchive.closeEntry();
+      if (!isDocumentWritten())
+        log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
+      newarchive.finish();// close(); // use newarchive.finish(); for a stream IO
+      newarchive.flush();
+      //
+      updateOriginal();
+      closeAndReset();
+    } else {
+      log.warn("Attempt to close archive that has not been opened for writing.");
+    }
+  }
+  /**
+   * Opens and returns the applicationData output stream for the appdataReference string.
+   * @param appdataReference
+   * @return Output stream to write to
+   * @throws IOException
+   */
+  public AppDataOutputStream getAppDataStream(String appdataReference) throws IOException {
+    if (newarchive==null)
+      throw new IOException("Attempt to write to closed VamsasArchive object.");
+    if (addValidEntry(appdataReference)) {
+      return new AppDataOutputStream(newarchive);
+    }
+    return null;
+  }
+  
+  /**
+   * 
+   * @return JarEntry name for the vamsas XML stream in this archive
+   */
+  protected String getDocumentJarEntry() {
+    if (vamsasdocument)
+      return VamsasArchiveReader.VAMSASDOC;
+    return VamsasArchiveReader.VAMSASXML;
+  }
+  /**
+   * Safely initializes the VAMSAS XML document Jar Entry. 
+   * @return Writer to pass to the marshalling function.
+   * @throws IOException if a document entry has already been written. 
+   */
+  public PrintWriter getDocumentOutputStream() throws IOException {
+    if (newarchive==null)
+      openArchive();
+    if (!isDocumentWritten()) {
+      try {
+        if (addValidEntry(getDocumentJarEntry())) 
+          return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
+      } catch (Exception e) {
+        log.warn("Problems opening XML document JarEntry stream",e);
+      }
+    } else {
+      throw new IOException("Vamsas Document output stream is already written.");
+    }
+    return null;
+  }
+  
+  /**
+   * Access original archive if it exists, pass the reader to the client
+   * Note: this is NOT thread safe and a call to closeArchive() will by necessity 
+   * close and invalidate the VamsasArchiveReader object.
+   * @return null if no original archive exists.
+   */
+  public VamsasArchiveReader getOriginalArchiveReader() throws IOException {
+    if (!virginArchive) {
+      accessOriginal();
+      return odoc;
+    }
+    return null;
+  }
+  /**
+   * returns original document's root vamsas elements.
+   * @return
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   */
+  public Vobject[] getOriginalRoots() throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException  {
+    return VamsasArchive.getOriginalRoots(this);
+  }
+  /**
+   * @return original document or a new empty document (with default provenance)
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   */
+  public VamsasDocument getVamsasDocument() throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    return getVamsasDocument("org.vamsas.simpleclient.VamsasArchive", "Created new empty document", null);
+  }
+  /**
+   * Return the original document or a new empty document with initial provenance entry.
+   * @param provenance_user (null sets user to be the class name)
+   * @param provenance_action (null sets action to be 'created new document')
+   * @param version (null means use latest version)
+   * @return (original document or a new vamsas document with supplied provenance and version info)
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   */
+  public VamsasDocument getVamsasDocument(String provenance_user, String provenance_action, String version) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    if (_doc!=null)
+      return _doc;
+    _doc = getOriginalVamsasDocument(this, getVorba());
+    if (_doc!=null)
+      return _doc;
+    // validate parameters
+    if (provenance_user==null)
+      provenance_user = "org.vamsas.simpleclient.VamsasArchive";
+    if (provenance_action == null)
+      provenance_action="Created new empty document";
+    if (version==null)
+      version = VersionEntries.latestVersion();
+    // Create a new document and return it
+    _doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS()}, 
+        ProvenanceStuff.newProvenance(provenance_user, provenance_action), version);
+    return _doc;
+  }
+  /**
+   * @return Returns the current VorbaIdFactory for the archive.
+   */
+  public VorbaIdFactory getVorba() {
+    if (vorba==null)
+      vorba = new SimpleDocument("simpleclient.VamsasArchive");
+    return vorba.getVorba();
+  }
+  /**
+   * @return true if Vamsas Document has been written to archive
+   */
+  protected boolean isDocumentWritten() {
+    if (newarchive==null)
+      log.warn("isDocumentWritten() called for unopened archive.");
+    if (entries!=null) {
+      if (entries.containsKey(getDocumentJarEntry()))
+        return true;
+    }
+    return false;
+  }
+  private void makeBackup() {
+    if (!virginArchive) {
+      if (originalBackup==null && original!=null && original.exists()) {
+        try {
+          accessOriginal();
+          originalBackup = odoclock.backupSessionFile(null, original.getName(), ".bak", original.getParentFile());
+        }
+        catch (IOException e) {
+          log.warn("Problem whilst making a backup of original archive.",e);
+        }
+      }
+    }
+  }
+  /**
+   * opens the new archive ready for writing. If the new archive is replacing an existing one, 
+   * then the existing archive will be locked, and the new archive written to a temporary file. 
+   * The new archive will be put in place once close() is called.
+   * @param doclock LATER - pass existing lock on document, if it exists.... no need yet?
+   * @throws IOException
+   */
+  private void openArchive() throws IOException {
+    
+    if (newarchive!=null) {
+      log.warn("openArchive() called multiple times.");
+      throw new IOException("Vamsas Archive '"+archive.getAbsolutePath()+"' is already open.");
+    }
+    if (archive==null && (virginArchive || original==null)) {
+      log.warn("openArchive called on uninitialised VamsasArchive object.");
+      throw new IOException("Badly initialised VamsasArchive object - no archive file specified.");
+    }
+    if (!virginArchive) {
+      // lock the original
+      accessOriginal();
+      // make a temporary file to write to
+      archive = File.createTempFile(original.getName(), ".new",original.getParentFile());
+    } else {
+      if (archive.exists())
+        log.warn("New archive file name already in use! Possible lock failure imminent?");
+    }
+    
+    if (rchive==null)
+      rchive = new SessionFile(archive);
+    if (!rchive.lockFile()) 
+      throw new IOException("Failed to get lock on file "+archive);
+    // LATER: locked IO stream based access.
+    Manifest newmanifest = new Manifest();
+    newarchive = new JarOutputStream(rchive.fileLock.getBufferedOutputStream(true), newmanifest);  
+    //newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
+    entries = new Hashtable();
+  }
+  public void putVamsasDocument(VamsasDocument doc) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    putVamsasDocument(doc, getVorba());
+  }
+  /**
+   * 
+   * @param doc
+   * @param vorba
+   * @return (vorbaId string, Vobjhash) pairs for last hash of each object in document
+   * @throws IOException
+   * @throws org.exolab.castor.xml.MarshalException
+   * @throws org.exolab.castor.xml.ValidationException
+   */
+  public void putVamsasDocument(VamsasDocument doc, VorbaIdFactory vorba) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    if (vamsasdocument)
+      doc.setVersion(VersionEntries.latestVersion()); // LATER: ensure this does the correct thing.
+    VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);
+  }
+  
+  /**
+   * recovers the original file's contents from the (temporary) backup. 
+   * @throws Exception if any SessionFile or file removal operations fail.
+   */
+  private void recoverBackup() throws Exception {
+    if (originalBackup!=null) {
+      // backup has been made.
+      // revert from backup and delete it (changing backup filename)
+      if (rchive==null) {
+        rchive = new SessionFile(original);
+      }
+      SessionFile bckup = new SessionFile(originalBackup);
+      
+      rchive.updateFrom(null, bckup); // recover from backup file.
+      bckup.unlockFile();
+      bckup=null;
+      removeBackup();
+    }
+  }
+  
+  /**
+   * forget about any backup that was made - removing it first if it was only temporary.
+   */
+  private void removeBackup() {
+    if (originalBackup!=null) {
+      log.debug("Removing backup in "+originalBackup.getAbsolutePath());
+      if (!donotdeletebackup)
+        if (!originalBackup.delete())
+          log.info("VamsasArchive couldn't remove temporary backup "+originalBackup.getAbsolutePath());
+      originalBackup=null;
+    }
+  } 
+  /**
+   * @param vorba the VorbaIdFactory to use for accessing vamsas objects.
+   */
+  public void setVorba(VorbaIdFactory Vorba) {
+    if (Vorba!=null) {
+      if (vorba==null)
+        vorba = new SimpleDocument(Vorba);
+      else
+        vorba.setVorba(Vorba);
+    } else
+      getVorba();
+  }
+  /**
+   * Convenience method to copy over the referred entry from the backup to the new version.
+   * Warning messages are raised if no backup exists or the 
+   * entry doesn't exist in the backed-up original.
+   * Duplicate writes return true - but a warning message will also be raised.
+   * @param AppDataReference
+   * @return true if AppDataReference now exists in the new document
+   * @throws IOException
+   */
+  public boolean transferAppDataEntry(String AppDataReference) throws IOException {
+    return transferAppDataEntry(AppDataReference, AppDataReference);
+  }
+  /**
+   * Validates the AppDataReference: not null and not already written to archive.
+   * @param AppDataReference
+   * @return true if valid. false if not
+   * @throws IOException for really broken references!
+   */
+  protected boolean _validNewAppDataReference(String newAppDataReference) throws IOException {
+    // LATER: Specify valid AppDataReference form in all VamsasArchive handlers
+    if (newAppDataReference==null)
+      throw new IOException("null newAppDataReference!");
+    if (entries.containsKey(newAppDataReference)) {
+      log.warn("Attempt to write '"+newAppDataReference+"' twice! - IGNORED");
+      // LATER: fix me? warning message should raise an exception here.
+      return false;
+    }
+    return true;
+  }
+  /**
+   * Transfers an AppDataReference from old to new vamsas archive, with a name change.
+   * @see transferAppDataEntry(String AppDataReference)
+   * @param AppDataReference
+   * @param NewAppDataReference - AppDataReference in new Archive
+   * @return
+   * @throws IOException
+   */
+  public boolean transferAppDataEntry(String AppDataReference, String NewAppDataReference) throws IOException {
+    if (original==null || !original.exists()) {
+      log.warn("No backup archive exists.");
+      return false;
+    }
+    if (AppDataReference==null)
+      throw new IOException("null AppDataReference!");
+
+    if (!_validNewAppDataReference(NewAppDataReference))
+      return false;
+    
+    accessOriginal();
+    
+    java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
+    
+    if (adstream==null) {
+      log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
+      return false;
+    }
+    
+    java.io.OutputStream adout = getAppDataStream(NewAppDataReference);
+    // copy over the bytes
+    int written=-1;
+    long count=0;
+    byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
+    do {
+      if ((written = adstream.read(buffer))>-1) {
+        adout.write(buffer, 0, written);
+        log.debug("Transferring "+written+".");
+        count+=written;
+      }
+    } while (written>-1);
+    log.debug("Sucessfully transferred AppData for '"
+        +AppDataReference+"' as '"+NewAppDataReference+"' ("+count+" bytes)");
+    return true;
+  }
+  /**
+   * write data from a stream into an appData reference.
+   * @param AppDataReference - New AppDataReference not already written to archive
+   * @param adstream Source of data for appData reference - read until .read(buffer) returns -1
+   * @return true on success.
+   * @throws IOException for file IO or invalid AppDataReference string
+   */
+  public boolean writeAppdataFromStream(String AppDataReference, java.io.InputStream adstream) throws IOException {
+    if (!_validNewAppDataReference(AppDataReference)) {
+      log.warn("Invalid AppDataReference passed to writeAppdataFromStream");
+      throw new IOException("Invalid AppDataReference! (null, or maybe non-unique)!");
+    }
+      
+    if (AppDataReference==null) {
+      log.warn("null appdata passed.");
+      throw new IOException("Null AppDataReference");
+    }
+    
+    java.io.OutputStream adout = getAppDataStream(AppDataReference);
+    // copy over the bytes
+    int written=-1;
+    long count=0;
+    byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
+    do {
+      if ((written = adstream.read(buffer))>-1) {
+        adout.write(buffer, 0, written);
+        log.debug("Transferring "+written+".");
+        count+=written;
+      }
+    } while (written>-1);
+    return true;
+  }
+  /**
+   * transfers any AppDataReferences existing in the old document 
+   * that haven't already been transferred to the new one
+   * LATER: do the same for transfers requiring a namechange - more document dependent.
+   *  @return true if data was transferred.
+   */
+  public boolean transferRemainingAppDatas() throws IOException {
+    boolean transfered=false;
+    if (original==null || !original.exists()) {
+      log.warn("No backup archive exists.");
+      return false;
+    }
+    accessOriginal();
+    
+    if (getVorba()!=null) {
+      Vector originalRefs=null;
+      try {
+        originalRefs = vorba.getReferencedEntries(getVamsasDocument(), getOriginalArchiveReader());
+      } catch (Exception e) {
+        log.warn("Problems accessing original document entries!",e);
+      }
+      if (originalRefs!=null) {
+        Iterator ref = originalRefs.iterator();
+        while (ref.hasNext()) {
+          String oldentry = (String) ref.next();
+          if (oldentry!=null && !entries.containsKey(oldentry)) {
+            log.debug("Transferring remaining entry '"+oldentry+"'");
+            transfered |= transferAppDataEntry(oldentry);
+          }
+        }
+      }
+    } 
+    return transfered;
+  }
+  /**
+   * called after archive is written to put file in its final place
+   */
+  private void updateOriginal() {
+    if (!virginArchive) {
+      // make sure original document really is backed up and then overwrite it.
+      if (odoc!=null) {
+        // try to shut the odoc reader.
+        odoc.close();
+        odoc = null;
+      }
+      // Make a backup if it isn't done already
+      makeBackup();
+      try {
+        // copy new Archive data that was writen to a temporary file
+        odoclock.updateFrom(null, rchive);
+      }
+      catch (IOException e) {
+        // LATER: decide if leaving nastily named backup files around is necessary.
+        File backupFile=backupFile();
+        if (backupFile!=null)
+          log.error("Problem updating archive from temporary file! - backup left in '"
+            +backupFile().getAbsolutePath()+"'",e);
+        else
+          log.error("Problems updating, and failed to even make a backup file. Ooops!", e);
+      }
+      // Tidy up if necessary.
+      removeBackup();
+    } else {
+      
+      
+    }
+  }
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/VamsasArchiveReader.java b/src/uk/ac/vamsas/client/simpleclient/VamsasArchiveReader.java
new file mode 100644 (file)
index 0000000..e8df789
--- /dev/null
@@ -0,0 +1,313 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.RandomAccessFile;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Vector;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.vamsas.objects.utils.document.VersionEntries;
+/**
+ * Basic methods for accessing an existing Vamsas Archive, 
+ * and Jar entry names for creating new vamsas archives.
+ * 
+ * @author jimp
+ *
+ */
+public class VamsasArchiveReader {
+  private static Log log = LogFactory.getLog(VamsasArchiveReader.class);
+  JarFile jfile=null;
+  boolean stream=false; // true if we are seeking on the stream.
+  RandomAccessFile rfile;
+  ZipInputStream jstream=null;
+  Hashtable strmentries = null;
+  private void streamInit() {
+    //throw new Error("VamsasArchiveReader(Stream) Not implemented!");
+     if (!stream) {
+      log.debug("Skipping init for Jar Stream input.");
+      return;
+    }
+    strmentries = new Hashtable();
+    log.debug("Jar Stream input Initialisation");
+    try {
+      rfile.seek(0);
+      // no buffering - we need to be able to move around the random access stream.
+      jstream = new ZipInputStream(new FileInputStream(rfile.getFD())); // no manifest (probably)
+      if (jstream.available()==0)
+        log.warn("Can't read from JarInputStream (Locked stream!)");
+      ZipEntry entry=null;
+      long pos=0;
+      do {
+        if ((entry=jstream.getNextEntry())!=null) {
+          if (strmentries.containsKey(entry.getName())) {
+            log.info("Only recording last of duplicate entries '"+entry.getName()+"'");
+          } 
+          strmentries.put(entry.getName(), new Long(pos++));
+          jstream.closeEntry();
+        }
+      } while (entry!=null);
+    }
+    catch (Exception e) {
+      log.warn("Exceptions during init!",e);
+      jstream=null;
+    }
+  }
+  
+  public VamsasArchiveReader(File vamsasfile) {
+    jfile=null;
+    if (vamsasfile.exists()) {
+      try {
+        jfile=new JarFile(vamsasfile);
+      }
+      catch (Exception e) {
+        log.debug("non-serious? couldn't open new JarFile on "+vamsasfile,e);
+        jfile=null;
+      }
+    }
+    
+  }
+  /**
+   * in an ideal world - this constructor will create a reader object
+   * for the locked file's random access stream.
+   * 
+   * @param vamsaslock
+   */
+  public VamsasArchiveReader(Lock vamsaslock) {
+    // LATER: implement or remove
+    if (vamsaslock==null || !vamsaslock.isLocked())
+      throw new Error("IMPLEMENTATION ERROR: Cannot create a VamsasArchiveReader without a valid lock.");
+    // throw new Error("VamsasArchiveReading from locked IO stream not yet implemented.");
+    try {
+      rfile = vamsaslock.getRaFile();
+    } catch (Exception e) {
+      log.warn("Unexpected IO Exception when accessing locked vamsas archive stream "+vamsaslock.target,e);
+    }
+    stream = true;
+    streamInit();
+    if (jstream==null)
+      throw new Error("Failed to open archive from Locked random access stream.");
+  }
+  
+  /**
+   * the vamsas document version(s) handled by this Reader
+   */
+  final public static String DOCUMENT_VERSION=VersionEntries.BETA_VERSION; 
+  /**
+   * name of the jarEntry containing a well formatted vamsas XML Document
+   */
+  
+  final public static String VAMSASDOC="vamsasDocument.xml";
+  
+  /**
+   * name of the jarEntry containing a root VAMSAS element, and containing a 
+   * random sequence of VAMSAS DataSet elements 
+   */
+  
+  final public static String VAMSASXML="vamsas.xml";
+  /**
+   * seeks jstream to the given entry name and reads it.
+   * @param entryname
+   * @return
+   */
+  private JarEntry seekEntry(String entryname) {
+    if (jstream==null)
+      return null;
+    if (!strmentries.containsKey(entryname)) 
+      return null;
+    Long entrypos = (Long) strmentries.get(entryname);
+    if (entrypos==null) {
+      log.error("Null entry position for "+entryname);
+      return null;
+    }
+    try {
+      jstream=null;
+      rfile.seek(0);
+      jstream = new ZipInputStream(new FileInputStream(rfile.getFD()));
+      ZipEntry entry = null;
+      long epos = entrypos.longValue();
+      do {
+        entry = jstream.getNextEntry();
+      } while (entry!=null && --epos>=0);  
+        // rfile.seek(entrypos.longValue());
+      // make a Jar entry from a zip entry.
+      return new JarEntry(entry);
+    }
+    catch (Exception e) {
+      log.warn("Whilst seeking for "+entryname, e);
+    }
+    return null;
+  }
+  /**
+   * 
+   * @return JarEntry for VamsasArchiveReader.VAMSASDOC
+   */
+  protected JarEntry getVamsasDocumentEntry() {
+    return getJarEntry(VAMSASDOC);
+  }
+  /**
+   * 
+   * @return JarEntry for VamsasArchiveReader.VAMSASXML
+   */
+  protected JarEntry getVamsasXmlEntry() {
+    return getJarEntry(VAMSASXML);
+  }
+  /**
+   * Test for valid vamsas document archive
+   * @return true if getVamsasDocumentStream will return a stream likely to contain valid XML
+   */
+  public boolean isValid() {
+    // TODO: check if VAMSASDOC is well formed (follows www.vamsas.ac.uk/schemas/vamsasDocument.xsd) and all appData references are resolvable - preferably as jar entries
+    if (jfile!=null || jstream!=null)
+      return (getVamsasDocumentEntry()!=null);
+    return false;   
+  }
+  
+  
+  protected JarEntry getAppdataEntry(String AppdataRef) {
+    JarEntry entry;
+    if ((jfile==null && jstream==null) || !isValid() || (entry=getJarEntry(AppdataRef))==null)
+      return null;
+    
+    return entry;
+  }
+  
+  public InputStream getAppdataStream(String AppdataRef) {
+    JarEntry entry=getAppdataEntry(AppdataRef);
+    try {
+      if (entry!=null)
+        return getInputStream(entry);
+    } catch (IOException e) {
+      log.error("Failed when opening AppdataStream for "+AppdataRef, e);
+    }
+    return null;
+  }
+  /**
+   * get the VamsasDocument input stream, if it exists.
+   * @return null or valid input stream
+   */
+  public InputStream getVamsasDocumentStream() {
+    InputStream vdoc;
+    if ((jfile==null && jstream==null) || !isValid())
+      return null;
+    try {
+      vdoc = getInputStream(getVamsasDocumentEntry());
+    } catch (IOException e) {
+      log.error("Whilst geting document stream",e);
+      vdoc=null;
+    }
+    return vdoc;
+  }
+  
+  /**
+   * get the VamsasXML input stream, if it exists.
+   * Note: Deprecated beyond our prealpha testing.
+   * @return null or valid input stream.
+   */
+  
+  public InputStream getVamsasXmlStream() {
+    // log.warn("Deprecated call");
+    JarEntry xmle=getVamsasXmlEntry();
+    InputStream vdoc;
+    if (xmle==null)
+      return null;
+    try {
+      vdoc = getInputStream(xmle);
+    } catch (IOException e) {
+      log.error("Whilst getting VamsasXmlStream",e);
+      vdoc=null;
+    }
+    return vdoc;
+  }
+  
+  /**
+   * silently close the jar file.
+   *
+   */
+  public void close() {
+    if (jfile!=null) {
+      try {
+        jfile.close();
+      } catch (IOException e) {
+        log.error("Whilst closing JarFile "+jfile.getName(), e);
+      }
+    }
+    if (jstream!=null) {
+      try {
+        jstream.closeEntry();
+        jstream=null;
+        // LATER: reference counting for random access file instances is necessary.
+      }
+      catch (Exception e) {
+        log.error("Whilst finishing reading from jar input stream",e);
+      }
+    }
+  }
+  
+  /**
+   * returns all entries not matching the filespec of a vamsas xml entry
+   * @return array of entries.
+   */
+  public Vector getExtraEntries() {
+    if ((jfile==null && jstream==null)|| !isValid())
+      return null;
+    Vector e = new Vector();
+    if (jstream!=null) {
+      Enumeration entries = strmentries.keys();
+      if (entries!=null && entries.hasMoreElements()) {
+        do {
+          JarEntry el = (JarEntry) entries.nextElement();
+          if (!el.getName().equals(VAMSASDOC) && !el.getName().equals(VAMSASXML))
+            e.add(new String(el.getName())); // avoid references
+        } while (entries.hasMoreElements());
+      }
+    } else {
+      Enumeration entries = jfile.entries();
+      if (entries!=null && entries.hasMoreElements()) {
+        do {
+          JarEntry el = (JarEntry) entries.nextElement();
+          if (!el.getName().equals(VAMSASDOC) && !el.getName().equals(VAMSASXML))
+            e.add(new String(el.getName())); // avoid references
+        } while (entries.hasMoreElements());
+      }
+      return e;
+    }
+    return null;
+  }
+  
+  /* (non-Javadoc)
+   * @see java.util.jar.JarFile#getInputStream(java.util.zip.ZipEntry)
+   */
+  private InputStream getInputStream(ZipEntry ze) throws IOException {
+    if (jfile!=null)
+      return jfile.getInputStream(ze);
+    if (jstream!=null) {
+      seekEntry(ze.getName());
+      return new AppDataInputStream(jstream);
+    }
+    return null;
+  }
+    
+    /* (non-Javadoc)
+     * @see java.util.jar.JarFile#getJarEntry(java.lang.String)
+     */
+    private JarEntry getJarEntry(String name) {
+      if (jfile!=null)
+        return jfile.getJarEntry(name);
+      if (jstream!=null)
+        return seekEntry(name);
+      return null;
+    }
+  }
diff --git a/src/uk/ac/vamsas/client/simpleclient/VamsasFile.java b/src/uk/ac/vamsas/client/simpleclient/VamsasFile.java
new file mode 100644 (file)
index 0000000..c30e5e6
--- /dev/null
@@ -0,0 +1,100 @@
+
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.util.Timer;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+
+import org.vamsas.objects.core.LockFileDescriptor;
+
+/**
+ * low level vamsas document management routines
+ * analogous to ClientsFile
+ * Grew out of io tests on VamsasArchive class in org.vamsas.test.simpleclient.VamsasArchive
+ * This class is not thread safe.
+ * @author jimp
+ *
+ */
+public class VamsasFile extends SessionFile {
+  /**
+   * 
+   * Connect to an existing Vamsas document in a given sessionDir
+   * or create a new one.
+   * 
+   * @param sessionDir
+   * @throws java.io.IOException
+   */
+  public VamsasFile(File sessionFile) throws java.io.IOException {
+    super(sessionFile);
+  }
+  /**
+   * 
+   * @return the VamsasFile
+   */
+  public File getVamsasFile() {
+    return sessionFile;
+  }
+  /**
+   * Expand a previously stored session into the sessionDir
+   * @param sessionDir
+   * @param storedSession
+   
+   public VamsasFile(File sessionDir, JarFile storedSession) throws IOException {
+    // check if sessionDir is live or not
+    if (!sessionDir.exists()) {
+      sessionDir.mkdir();
+    }
+    
+    {
+      // check its actually a writable directory
+    }
+    
+    File sfile = new File(sessionDir, "vamsas.jar");
+    VamsasFile(sfile);
+    // if live - try to merge storedSession with sessionDir
+    //  - will probably fail through duplicate Vobject references needing to be dereferenced.
+    // TODO: think of a way of specifying vorba_id scope for an application's references to allow merging of one vamsasDocument with another.
+    
+  }
+  */
+  /**
+   * public interface for getting a lock.
+   * The lock object is internally referenced 
+   * so the lock will persist even after the
+   * return value of the method goes out of scope.
+   * @return null if lock couldn't be got or a valid Lock object.
+   */
+  public Lock getLock() {
+    if (lockFile())
+      return fileLock;
+    return null;
+  }
+  /**
+   * 
+   * @param extantLock
+   * @return null, extantLock or new Lock.
+   */
+  public Lock getLock(Lock extantLock) {
+    if (lockFile(extantLock))
+      return fileLock;
+    return null;
+  }
+  /**
+   * explicitly unlocks vamsas file.
+   * if you have called getLock() you *must* 
+   * call this to release the lock.
+   */
+  public void unLock() {
+    this.unlockFile();
+  }
+  
+}
diff --git a/src/uk/ac/vamsas/client/simpleclient/VamsasSession.java b/src/uk/ac/vamsas/client/simpleclient/VamsasSession.java
new file mode 100644 (file)
index 0000000..d230ead
--- /dev/null
@@ -0,0 +1,312 @@
+package uk.ac.vamsas.client.simpleclient;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.RandomAccessFile;
+import java.io.Writer;
+
+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.vamsas.client.ClientHandle;
+import org.vamsas.client.UserHandle;
+/**
+ * Does all the IO operations for a SimpleClient instance accessing 
+ * a SimpleClient vamsas session.
+ * 
+ * Basically, it defines the various standard names for the files 
+ * in the session directory (that maps to the sessionUrn), 
+ * provides constructors for the file handlers and watchers of 
+ * those file entities, and some higher level methods 
+ * to check and change the state flags for the session.
+ * 
+ * TODO: move the stuff below to the SimpleClientFactory documentation.
+ * much may not be valid now :
+ * Vamsas client is intialised with a path to create live session directories. 
+ * This path may contain a vamsas.properties file 
+ * that sets additional parameters (otherwise client 
+ * just uses the one on the classpath).
+ * 
+ * A vamsas session consists of :
+ *  SessionDir - translates to urn of a live session.
+ *  Contains: Vamsas Document (as a jar), Session client list file, 
+ *  both of which may be locked, and additional 
+ *  temporary versions of these files when write 
+ *  operations are taking place.
+ * 
+ * Zip file entries
+ *  - vamsasdocument.xml : core info
+ *  one or more:
+ *  - <applicationname>.version.sessionnumber.raw (string given in vamsasdocument.xml applicationData entry)
+ *  
+ * Lockfile
+ *  - filename given in the vamsasdocument.xml. Should be checked for validity by any client and rewritten if necessary. 
+ *    The lockfile can point to the jar itself.
+ * Mode of operation.
+ * Initially - documentHandler either:
+ *  - creates a zip for a new session for the client
+ *  - connect to an existing session zip 
+ *   1. reads session urn file
+ *   2. waits for lock
+ *   3. examines session - decide whether to create new application data slice or connect to one stored in session.
+ *   4. writes info into session file
+ *   5. releases lock and generates local client events.
+ *   6. Creates Watcher thread to generate events.
+ * 
+ * During the session
+ *  - Update watcher checks for file change - 
+ * 
+ * Procedures for file based session message exchange
+ *  - session document modification flag
+ *    
+ */
+
+public class VamsasSession {
+  /**
+   * indicator file for informing other processes that 
+   * they should finalise their vamsas datasets for 
+   * storing into a vamsas archive.
+   */
+  public static final String CLOSEANDSAVE_FILE="stored.log";
+  /**
+   * session file storing the last_stored_stat data 
+   */
+  public static final String MODIFIEDDOC_FILE="modified";
+
+  /**
+   * called to clear update flag after a successful offline storage event
+   */
+  protected void clearUnsavedFlag() {
+    SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
+    if (!laststored.clearFlag())
+      log.warn("Unsaved flag was not cleared for "+sessionDir);
+  }
+  /**
+   * called to indicate session document has been modified.
+   *
+   */
+  protected void setUnsavedFlag() {
+    SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
+    if (!laststored.setFlag())
+      log.warn("Couldn't set the Unsaved flag for "+sessionDir);
+  }
+  /**
+   * 
+   * @return true if session document has been modified since last offline storage event 
+   */
+  protected boolean getUnsavedFlag() {
+    SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE));
+    return laststored.checkFlag();
+  }
+  /**
+   * log file location
+   */
+  public static final String SESSION_LOG="Log.txt";
+  private static Log log = LogFactory.getLog(VamsasSession.class);
+  protected Logger slog = Logger.getLogger("org.vamsas.client.SessionLog");
+  /**
+   * setup the sessionLog using Log4j.
+   * @throws IOException
+   */
+  private void initLog() throws IOException {
+    // 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()));
+  }
+  
+  /**
+   * the sessionDir is given as the session location for new clients.
+   */
+  protected File sessionDir;
+  /**
+   * holds the list of attached clients
+   */
+  ClientsFile clist;
+  public static final String CLIENT_LIST="Clients.obj";
+  /**
+   * holds the data
+   */
+  VamsasFile vamArchive; 
+  public static final String VAMSAS_OBJ="VamDoc.jar";
+  
+  /**
+   * sets up the vamsas session files and watchers in sessionDir
+   * @param sessionDir
+   */
+  protected VamsasSession(File sessionDir) throws IOException {
+    if (sessionDir==null)
+      throw new Error("Null directory for VamsasSession.");
+    if (sessionDir.exists()) {
+      if (!sessionDir.isDirectory() || !sessionDir.canWrite() || !sessionDir.canRead())
+        throw new IOException("Cannot access '"+sessionDir+"' as a read/writable Directory.");
+      if (checkSessionFiles(sessionDir)) {
+        // session files exist in the directory
+        this.sessionDir = sessionDir;
+        initSessionObjects();
+        slog.debug("Initialising additional VamsasSession instance");
+        log.debug("Attached to VamsasSession in "+sessionDir);
+      } 
+    } else {
+      // start from scratch
+      if (!sessionDir.mkdir())
+        throw new IOException("Failed to make VamsasSession directory in "+sessionDir);
+      this.sessionDir = sessionDir; 
+      createSessionFiles();
+      initSessionObjects();
+      slog.debug("Session directory created.");
+      log.debug("Initialised VamsasSession in "+sessionDir);
+    }
+  }
+  /**
+   * tests presence of existing sessionfiles files in dir
+   * @param dir
+   * @return
+   */
+  private boolean checkSessionFiles(File dir) throws IOException {
+    File c_file = new File(dir,CLIENT_LIST);
+    File v_doc = new File(dir,VAMSAS_OBJ);
+    if (c_file.exists() && v_doc.exists())
+      return true;
+    return false;
+  }
+  /**
+   * create new empty files in dir
+   *
+   */
+  private void createSessionFiles() throws IOException {
+    if (sessionDir==null)
+      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())
+      log.debug("Created new ClientFile "+c_file); // don't care if this works or not
+    if (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 {
+    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));
+    initLog();
+  }
+  /**
+   * make a new watcher object for the clientFile
+   * @return new ClientFile watcher instance
+   */
+  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;
+  /**
+   * 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));
+
+  }
+  /**
+   * write to the StoreWatcher file to indicate that a storeDocumentRequest has been made.
+   * The local client's storeWatcher FileWatcher object is updated so the initial change is not registered.
+   * @param client
+   * @param user
+   * @return
+   */
+  public void addStoreDocumentRequest(ClientHandle client, UserHandle user) throws IOException {
+    SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE));
+    while (!sfw.lockFile())
+      log.debug("Trying to get lock for "+CLOSEANDSAVE_FILE);
+    RandomAccessFile sfwfile=sfw.fileLock.getRaFile();
+    sfwfile.setLength(0); // wipe out any old info.
+    // TODO: rationalise what gets written to this file (ie do we want other clients to read the id of the requestor?)
+    sfwfile.writeUTF(client.getClientUrn()+":"+user.getFullName()+"@"+user.getOrganization());
+    sfw.unlockFile();
+    if (store_doc_file!=null)
+      store_doc_file.setState();
+    slog.info("FinalizeAppData request from "+user.getFullName()+" using "+client.getClientUrn()+"");
+  }
+  /**
+   * create a new session with an existing vamsas Document - by copying it into the session.
+   * @param archive
+   */
+  public void setVamsasDocument(File archive) throws IOException {
+    log.debug("Transferring vamsas data from "+archive+" to session:"+vamArchive.sessionFile);
+    SessionFile xtantdoc = new SessionFile(archive);
+    vamArchive.updateFrom(null, xtantdoc);
+    // LATER: decide if session archive provenance should be updated to reflect access.
+    // TODO: soon! do a proper import objects from external file 
+    log.debug("Transfer complete.");
+  }
+  /**
+   * write session as a new vamsas Document (this will overwrite any existing file without warning)
+   * TODO: test
+   * TODO: verify that lock should be released for vamsas document.
+   * @param destarchive
+   */
+  protected void writeVamsasDocument(File destarchive, Lock extlock) throws IOException {
+    log.debug("Transferring vamsas data from "+vamArchive.sessionFile+" to session:"+destarchive);
+    SessionFile newdoc = new SessionFile(destarchive);
+    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.
+    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.");
+  }
+  
+  /**
+   * Creates a VamsasArchive Vobject for accessing and updating document
+   * Note: this will lock the Vamsas Document for exclusive access to the client.
+   * @return session vamsas document
+   * @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()) 
+      throw new IOException("Failed to get lock for vamsas archive.");
+    
+    VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true, vamArchive);
+    
+    return va;
+  }
+  /**
+   * create a uniquely named file in the session Directory
+   * @see java.io.File.createTempFile
+   * @param pref Prefix for name
+   * @param suff Suffix for name
+   * @return SessionFile object configured for the new file (of length zero)
+   * @throws IOException
+   */
+  protected SessionFile getTempSessionFile(String pref, String suff) throws IOException {
+    File tfile = File.createTempFile(pref,suff,sessionDir);
+    SessionFile tempFile = new SessionFile(tfile);
+    return tempFile;
+  }
+}
+
+