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.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.object; import org.vamsas.objects.core.ApplicationData; import org.vamsas.objects.core.VAMSAS; import org.vamsas.objects.core.VamsasDocument; /** * Class for creating 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); /** * destination of new archive data */ java.io.File archive=null; /** * locked IO handler for new archive file */ SessionFile rchive=null; /** * original archive file that is to be updated */ 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 * nb. No file locks are made until open() is called. * @param archive - file spec for new vamsas archive * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive */ public VamsasArchive(File archive, boolean vamsasdocument) { super(); 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; } else { this.original = null; this.archive = archive; // archive is written in place. virginArchive = true; } } /** * name of backup of existing archive that has been updated/overwritten. */ 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 (original!=null) { if (!virginArchive) { if (odoc!=null) { odoc.close(); odoc=null; } if (!archive.getAbsolutePath().equals(original)) { if (originalBackup==null) makeBackup(); try { odoclock.updateFrom(null, rchive); } catch (IOException e) { log.error("Problem updating archive from temporary file!",e); } } else { log.warn("archive and original are the same file! ("+archive.getAbsolutePath()+")"); } } // else virginArchive are put in correct place from the beginning } } /** * 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; } 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) { if (original==null) { log.warn("openArchive called on uninitialised VamsasArchive object."); throw new IOException("Badly initialised VamsasArchive object - no archive file specified."); } // lock the original accessOriginal(); // make a temporary file to write to archive = File.createTempFile(original.getName(), "new",original.getParentFile()); } 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. */ 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(); } catch (Exception e) { log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e); return false; } } // original is untouched // just delete temp files } } else { log.info("cancelArchive called before archive("+original.getAbsolutePath()+") has been opened!"); } closeAndReset(); return true; } /** * only do this if you want to destroy the current file output stream * */ private void closeAndReset() { rchive.unlockFile(); rchive = null; if (original!=null) { if (odoc!=null) { odoc.close(); odoc=null; } archive.delete(); if (odoclock!=null) { odoclock.unlockFile(); odoclock = null; } } newarchive=null; original=null; entries=null; } private final int _TRANSFER_BUFFER=4096*4; /** * open backup 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() */ 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 } 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; } }