/** * */ 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.objects.utils.AppDataReference; import uk.ac.vamsas.client.IClientAppdata; import uk.ac.vamsas.objects.core.AppData; import uk.ac.vamsas.objects.core.ApplicationData; import uk.ac.vamsas.objects.core.User; /** * @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(uk.ac.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 uk.ac.vamsas.client.IClientAppdata#getClientAppdata() */ public byte[] getClientAppdata() { return _getappdataByteArray(false); } /* (non-Javadoc) * @see uk.ac.vamsas.client.IClientAppdata#getClientInputStream() */ public DataInput getClientInputStream() { return _getappdataInputStream(false); } /* (non-Javadoc) * @see uk.ac.vamsas.client.IClientAppdata#getUserAppdata() */ public byte[] getUserAppdata() { return _getappdataByteArray(true); } /* (non-Javadoc) * @see uk.ac.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 uk.ac.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 uk.ac.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 uk.ac.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 uk.ac.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 uk.ac.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 uk.ac.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(); } }