/* * This file is part of the Vamsas Client version 0.1. * Copyright 2009 by Jim Procter, Iain Milne, Pierre Marguerite, * Andrew Waterhouse and Dominik Lindner. * * Earlier versions have also been incorporated into Jalview version 2.4 * since 2008, and TOPALi version 2 since 2007. * * The Vamsas Client is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The Vamsas Client is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the Vamsas Client. If not, see . */ 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 uk.ac.vamsas.client.AppDataInputStream; import uk.ac.vamsas.client.AppDataOutputStream; 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; import uk.ac.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; } /** * ensures that the appData information for this client instance has been * extracted from the vamsas document provided by clientdoc. */ private void extractAppData() { if (!accessedDocument) _extractAppData(clientdoc.getVamsasDocument()); } /** * 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 InputStream 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) { return 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 InputStream entry = getAppDataStream(appdata, docreader); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try { byte buff[] = new byte[_TRANSFER_BUFFER]; int olen = 0; while (entry != null && entry.available() > 0) { int len = entry.read(buff, 0, _TRANSFER_BUFFER); if (len > -1) { 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 AppDataInputStream 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 AppDataInputStream(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(); 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 AppDataInputStream _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(); 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 AppDataInputStream 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 AppDataInputStream getUserInputStream() { return _getappdataInputStream(true); } /** * methods for writing new AppData entries. */ private AppDataOutputStream _getAppdataOutputStream(boolean clientOrUser) { // Must access document to get any existing references extractAppData(); 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."); } log.debug("Writing appData_entry.dat as " + ref); 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 AppDataOutputStream 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 AppDataOutputStream 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(); // LATER - check validity of a DataReference before we return true // TODO: return true if usersData is null but we have already written a new // data stream 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(); // LATER - check validity of a DataReference before we return true // TODO: return true if usersData is null but we have already written a new // data stream if ((usersData != null) && (usersData.getDataReference() != null || usersData.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 != null && newAppData.sessionFile.exists()) || (newUserData != null && 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(); } }