finished the Appdata/ClientDocument update mechanism. Untested!
[vamsas.git] / src / org / vamsas / client / simpleclient / SimpleClientAppdata.java
index 9f6b27f..a2d8912 100644 (file)
@@ -3,9 +3,21 @@
  */
 package org.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;
@@ -19,10 +31,35 @@ 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
@@ -47,86 +84,355 @@ public class SimpleClientAppdata implements IClientAppdata {
   }
   /**
    * 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 = null;
+  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());
-    ApplicationData appsglobal=null;
-    User usersdata = null;
+    accessedDocument = true;
     if (apldataset!=null) {
       if (apldataset.size()>0) {
         AppData clientdat = (AppData) apldataset.get(0);
         if (clientdat instanceof ApplicationData) {
-          appsglobal = (ApplicationData) clientdat;
+          appsGlobal = (ApplicationData) clientdat;
           if (apldataset.size()>1) {
             clientdat = (AppData) apldataset.get(1);
-            if (clientdat instanceof User)
-              usersdata = (User) clientdat;
+            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.
       }
     }
   }
-
-  /* (non-Javadoc)
-   * @see org.vamsas.client.IClientAppdata#getClientAppdata()
+  /**
+   * LATER: generalize this for different low-level session implementations (it may not always be a Jar)
+   * @param appdata
+   * @param docreader
+   * @return
    */
-  public byte[] getClientAppdata() {
-    // TODO Auto-generated method stub
+  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;
 
-  /* (non-Javadoc)
-   * @see org.vamsas.client.IClientAppdata#getClientInputStream()
+  /**
+   * Resolve AppData object to a byte array.
+   * @param appdata
+   * @param archiveReader
+   * @return null or the application data as a byte array
    */
-  public DataInput getClientInputStream() {
-    // TODO Auto-generated method stub
+  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;
   }
 
-  /* (non-Javadoc)
-   * @see org.vamsas.client.IClientAppdata#getClientOutputStream()
+  /**
+   * internal method for getting ByteArray from AppData object
+   * @param clientOrUser - true for returning userData, otherwise return Client AppData.
+   * @return null or byte array
    */
-  public DataOutput getClientOutputStream() {
-    // TODO Auto-generated method stub
+  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() {
-    // TODO Auto-generated method stub
-    return null;
+    return _getappdataByteArray(true);
   }
 
   /* (non-Javadoc)
    * @see org.vamsas.client.IClientAppdata#getUserInputStream()
    */
   public DataInput getUserInputStream() {
-    // TODO Auto-generated method stub
+    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.rafile.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(
+            new BufferedOutputStream(new java.io.FileOutputStream(apdfile.sessionFile)));
+      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(new BufferedInputStream(new FileInputStream(apdjar.sessionFile)));
+    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() {
-    // TODO Auto-generated method stub
-    return null;
+    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() {
-    // TODO Auto-generated method stub
+    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;
   }
 
@@ -134,30 +440,104 @@ public class SimpleClientAppdata implements IClientAppdata {
    * @see org.vamsas.client.IClientAppdata#hasUserAppdata()
    */
   public boolean hasUserAppdata() {
-    // TODO Auto-generated method stub
+    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) {
-    // TODO Auto-generated method stub
-    
+    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) {
-    // TODO Auto-generated method stub
-    
+    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 {
-    // TODO Auto-generated method stub
+    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();
   }