fixed the User element and the form of the ApplicationData dataType.xsd definition.
[vamsas.git] / src / org / vamsas / client / simpleclient / VamsasArchive.java
index e008464..39681ad 100644 (file)
 package org.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.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 
+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.object;
+import org.vamsas.objects.core.ApplicationData;
+import org.vamsas.objects.core.VAMSAS;
+import org.vamsas.objects.core.VamsasDocument;
+import org.vamsas.objects.utils.DocumentStuff;
+import org.vamsas.objects.utils.ProvenanceStuff;
+import org.vamsas.objects.utils.document.VersionEntries;
+
 /**
- * Class for creating a vamsas archive
- * (with backups) 
+ * 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 {
-  java.io.File archive;
-  java.io.File backup=null;
-  boolean vamsasdocument=true;  // make a document archive (rather than a vamsas.xml archive)
+  private static Log log = LogFactory.getLog(VamsasArchive.class);
+  /**
+   * 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;
+  /**
+   * 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;
+  /**
    * 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) {
+  public VamsasArchive(File archive, boolean vamsasdocument) throws IOException {
     super();
-    this.archive = archive;
-    
+    if (archive==null || (archive!=null && !archive.canWrite())) {
+      log.fatal("Invalid parameters for VamsasArchive constructor:"+((archive!=null) 
+          ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
+    }
     this.vamsasdocument = vamsasdocument;
+    if (archive.exists()) {
+      this.original = archive;
+      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.
+      virginArchive = true;
+    }
+    this.openArchive(); // open archive
+  }
+  /**
+   * name of backup of existing archive that has been updated/overwritten.
+   * onlu one backup will be made - and this is it.
+   */
+  File originalBackup = null;
+  
+  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);
+        }
+      }
+    }
+  }
+  
+  /**
+   * called after archive is written to put file in its final place
+   * TODO: FINISH ?? original should have sessionFile, and archive should also have sessionFile
+   */
+  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
+      if (originalBackup==null)
+        makeBackup();
+      try {
+        // copy new Archive data that was writen to a temporary file
+        odoclock.updateFrom(null, rchive);
+      }
+      catch (IOException e) {
+        log.error("Problem updating archive from temporary file! - backup in '"
+            +backupFile().getAbsolutePath()+"'",e);
+      }
+    } else {
+      // don't need to do anything.
+    }
+  }
+  /**
+   * called by app to get name of backup if it was made.
+   * @return null or a valid file object
+   */
+  public File backupFile() {
+    
+    if (!virginArchive) {
+      makeBackup();
+      return ((original!=null) ? originalBackup : null);
+    }
+    return null;
+  }
+  /**
+   * 
+   * @return JarEntry name for the vamsas XML stream in this archive
+   */
+  protected String getDocumentJarEntry() {
+    if (vamsasdocument)
+      return VamsasArchiveReader.VAMSASDOC;
+    return VamsasArchiveReader.VAMSASXML;
+  }
+  
+  /**
+   * @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;
+  }
+  /**
+   * 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.putNextEntry(je);
+    return true;
+  }
+  
+  /**
+   * 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.
+   * @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?");
+    }
+    
+    rchive = new SessionFile(archive);
+    rchive.lockFile();
+    newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
+    entries = new Hashtable();
+  }
+  /**
+   * 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;
+  }
+  /**
+   * 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)
+      openArchive();
+    if (addValidEntry(appdataReference)) {
+      return new AppDataOutputStream(newarchive);
+    }
+    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.close();
+        
+      } catch (Exception e) {};
+      if (!virginArchive) {
+        // then there is something to recover.
+        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);
+          
+          try {
+            rchive.updateFrom(null, bckup); // recover from backup file.
+            bckup.unlockFile();
+            bckup=null;
+            originalBackup.delete();
+            originalBackup=null;
+          }
+          catch (Exception e) {
+            log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
+            return false;
+          }
+        }
+      }
+    } else {
+      log.info("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;
+      }
+    }
+    newarchive=null;
+    original=null;
+    entries=null;
+  }
+  
+  private final int _TRANSFER_BUFFER=4096*4;
+  /**
+   * 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);
+    }
+  }
+  
+  /**
+   * 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);
+  }
+  /**
+   * 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 {
+    // TODO: Specify valid AppDataReference form in all VamsasArchive handlers
+    if (AppDataReference==null)
+      throw new IOException("null AppDataReference!");
+    if (original==null || !original.exists()) {
+      log.warn("No backup archive exists.");
+      return false;
+    }
+    if (entries.containsKey(NewAppDataReference)) {
+      log.warn("Attempt to write '"+NewAppDataReference+"' twice! - IGNORED");
+      return true;
+    }
+    
+    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;
+  }
+  /**
+   * 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.closeEntry();
+      if (!isDocumentWritten())
+        log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
+      newarchive.close();
+      updateOriginal();
+      closeAndReset();
+    } else {
+      log.warn("Attempt to close archive that has not been opened for writing.");
+    }
+  }
+  /**
+   * 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 object[] getOriginalRoots() throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException  {
+    return VamsasArchive.getOriginalRoots(this);
+  }
+  /**
+   * Access original document if it exists, and get VAMSAS root objects.
+   * @return vector of vamsas roots from original document
+   * @throws IOException
+   */
+  public static object[] 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;
   }
+  protected VorbaIdFactory vorba = null;
   
+  /**
+   * @return Returns the current VorbaIdFactory for the archive.
+   */
+  public VorbaIdFactory getVorba() {
+    return vorba;
+  }
+
+  /**
+   * @param vorba the VorbaIdFactory to use for accessing vamsas objects.
+   */
+  public void setVorba(VorbaIdFactory vorba) {
+    this.vorba = vorba;
+  }
+
+  /**
+   * 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
+   */
+  public VamsasDocument getVamsasDocument() throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    VamsasDocument doc = getOriginalVamsasDocument(this, getVorba());
+    if (doc!=null)
+      return doc;
+    // Create a new document and return it
+    doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS()}, 
+        ProvenanceStuff.newProvenance("org.vamsas.simpleclient.VamsasArchive", "Created new empty document")
+        , VersionEntries.latestVersion());
+    return doc;
+  }
+  /**
+   * 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);
+  }
+  private VorbaIdFactory makeDefaultFactory(VorbaIdFactory vorba) {
+    if (vorba==null) {
+      vorba = getVorba();
+      if (vorba==null) {
+        vorba = IdFactory.getDummyFactory("simpleclient.VamsasArchive");
+        setVorba(vorba); // save for later use
+      }
+    }
+    return vorba;
+  }
+  /**
+   * 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) {
+      // check the factory
+      vorba = ths.makeDefaultFactory(vorba);
+      if (oReader.isValid()) {
+        InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
+        Object unmarsh[] = VorbaXmlBinder.getVamsasObjects(vdoc, vorba, new VamsasDocument());
+        if (unmarsh==null)
+          log.fatal("Couldn't unmarshall document!");
+        
+        object vobjs = (object) 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
+        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(
+                    "org.vamsas.simpleclient.VamsasArchive", // TODO: VAMSAS: decide on 'system' operations provenance form
+                    "Vamsas Document constructed from vamsas.xml in <file>" 
+                    // TODO: VAMSAS: decide on machine readable info embedding in provenance should be done
+                    +ths.original+"</file>"), VersionEntries.ALPHA_VERSION);
+            root[0]=null;
+            root=null;
+            return doc;
+          }
+        }
+      }
+    }
+    // otherwise - there was no valid original document to read.
+    return null;    
+  }
+  public void putVamsasDocument(VamsasDocument doc) throws IOException, 
+  org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+    VorbaIdFactory vorba = makeDefaultFactory(getVorba());
+    VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);
+  }
 }