/*
 * This file is part of the Vamsas Client version 0.2. 
 * Copyright 2010 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 <http://www.gnu.org/licenses/>.
 */
package uk.ac.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.Iterator;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import uk.ac.vamsas.client.AppDataOutputStream;
import uk.ac.vamsas.client.ClientHandle;
import uk.ac.vamsas.client.IVorbaIdFactory;
import uk.ac.vamsas.client.SessionHandle;
import uk.ac.vamsas.client.UserHandle;
import uk.ac.vamsas.client.Vobject;
import uk.ac.vamsas.client.VorbaIdFactory;
import uk.ac.vamsas.client.VorbaXmlBinder;
import uk.ac.vamsas.objects.core.ApplicationData;
import uk.ac.vamsas.objects.core.VAMSAS;
import uk.ac.vamsas.objects.core.VamsasDocument;
import uk.ac.vamsas.objects.utils.AppDataReference;
import uk.ac.vamsas.objects.utils.DocumentStuff;
import uk.ac.vamsas.objects.utils.ProvenanceStuff;
import uk.ac.vamsas.objects.utils.document.VersionEntries;

/**
 * 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 {
  private static Log log = LogFactory.getLog(VamsasArchive.class);

  /**
   * Access original document if it exists, and get VAMSAS root objects.
   * 
   * @return vector of vamsas roots from original document
   * @throws IOException
   */
  public static Vobject[] 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;
  }

  /**
   * 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);
  }

  /**
   * 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) {
      ths.setVorba(vorba);
      return ths.vorba.getVamsasDocument(oReader);
    }
    // otherwise - there was no valid original document to read.
    return null;
  }

  /**
   * 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;

  Lock destinationLock = 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
   */
  org.apache.tools.zip.ZipOutputStream newarchive = null;

  /**
   * JarEntries written to archive
   */
  Hashtable entries = null;

  /**
   * true if we aren't just updating an archive
   */
  private boolean virginArchive = false;

  /**
   * name of backup of existing archive that has been updated/overwritten. only
   * one backup will be made - and this is it.
   */
  File originalBackup = null;

  boolean donotdeletebackup = false;

  private final int _TRANSFER_BUFFER = 4096 * 4;

  protected SimpleDocument vorba = null;

  /**
   * LATER: ? CUT'n'Paste error ? 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
   *           ????? where does this live JBPNote ?
   */
  private VamsasDocument _doc = null;

  /**
   * 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) throws IOException {
    this(archive, false, vamsasdocument, null);
  }

  public VamsasArchive(File archive, boolean vamsasdocument, boolean overwrite)
      throws IOException {
    this(archive, overwrite, vamsasdocument, null);
  }

  /**
   * Constructor for accessing Files under file-lock management (ie a session
   * file)
   * 
   * @param archive
   * @param vamsasdocument
   * @param overwrite
   * @throws IOException
   */
  public VamsasArchive(VamsasFile archive, boolean vamsasdocument,
      boolean overwrite) throws IOException {
    this(archive.sessionFile, overwrite, vamsasdocument, archive);
    // log.debug("using non-functional lock-IO stream jar access constructor");
  }

  /**
   * read and write to archive - will not overwrite original contents, and will
   * always write an up to date vamsas document structure.
   * 
   * @param archive
   * @throws IOException
   */
  public VamsasArchive(VamsasFile archive) throws IOException {
    this(archive, true, false);
  }

  /**
   * 
   * @param archive
   *          file to write
   * @param overwrite
   *          true if original contents should be deleted
   * @param vamsasdocument
   *          true if a proper VamsasDocument archive is to be written.
   * @param extantLock
   *          SessionFile object holding a lock for the <object>archive</object>
   * @throws IOException
   */
  public VamsasArchive(File archive, boolean overwrite, boolean vamsasdocument,
      SessionFile extantLock) throws IOException {
    super();
    if (archive == null
        || (archive != null && !(archive.getAbsoluteFile().getParentFile()
            .canWrite() && (!archive.exists() || archive.canWrite())))) {
      log
          .fatal("Expect Badness! -- Invalid parameters for VamsasArchive constructor:"
              + ((archive != null) ? "File cannot be overwritten."
                  : "Null Object not valid constructor parameter"));
      return;
    }

    this.vamsasdocument = vamsasdocument;
    if (archive.exists() && !overwrite) {
      this.original = archive;
      if (extantLock != null) {
        this.odoclock = extantLock;
        if (odoclock.fileLock == null || !odoclock.fileLock.isLocked())
          odoclock.lockFile();
      } else {
        this.odoclock = new SessionFile(archive);
      }
      odoclock.lockFile(); // lock the file *immediatly*
      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.
      if (extantLock != null)
        rchive = extantLock;
      else
        rchive = new SessionFile(archive);
      rchive.lockFile();
      if (rchive.fileLock == null || !rchive.fileLock.isLocked())
        throw new IOException("Lock failed for new archive" + archive);
      rchive.fileLock.getRaFile().setLength(0); // empty the archive.
      virginArchive = true;
    }
    this.openArchive(); // open archive
  }

  /**
   * 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);
      // this constructor is not implemented yet odoc = new
      // VamsasArchiveReader(odoclock.fileLock);
    }
  }

  /**
   * 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 (log.isDebugEnabled()) {
      log.debug("validating '" + entry + "' in hash for " + this);
    }
    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 {
    org.apache.tools.zip.ZipEntry je = new org.apache.tools.zip.ZipEntry(entry);
    // je.setExsetExtra(null);
    if (!addEntry(entry))
      return false;
    newarchive.flush();
    newarchive.putNextEntry(je);
    return true;
  }

  /**
   * called by app to get name of backup if it was made. If this is called, the
   * caller app *must* delete the backup themselves.
   * 
   * @return null or a valid file object
   */
  public File backupFile() {

    if (!virginArchive) {
      makeBackup();
      donotdeletebackup = true; // external reference has been made.
      return ((original != null) ? originalBackup : null);
    }
    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.closeEntry();
        newarchive.putNextEntry(new org.apache.tools.zip.ZipEntry("deleted"));
        newarchive.closeEntry();
        newarchive.close();

      } catch (Exception e) {
        log.debug("Whilst closing newarchive", e);
      }
      ;
      if (!virginArchive) {
        // then there is something to recover.
        try {
          recoverBackup();
        } catch (Exception e) {
          log.warn("Problems when trying to cancel Archive "
              + archive.getAbsolutePath(), e);
          return false;
        }
      }

    } else {
      log.warn("Client Error: 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;
      }
    }
    removeBackup();
    newarchive = null;
    original = null;
    entries = null;
  }

  /**
   * 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.flush();
      newarchive.closeEntry();
      if (!isDocumentWritten())
        log.warn("Premature closure of archive '" + archive.getAbsolutePath()
            + "': No document has been written.");
      newarchive.finish();// close(); // use newarchive.finish(); for a stream
                          // IO
      newarchive.flush();
      //
      updateOriginal();
      closeAndReset();
    } else {
      log
          .warn("Attempt to close archive that has not been opened for writing.");
    }
  }

  /**
   * 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)
      throw new IOException("Attempt to write to closed VamsasArchive object.");
    if (addValidEntry(appdataReference)) {
      return new AppDataOutputStream(newarchive);
    }
    return null;
  }

  /**
   * 
   * @return JarEntry name for the vamsas XML stream in this archive
   */
  protected String getDocumentJarEntry() {
    if (vamsasdocument)
      return VamsasArchiveReader.VAMSASDOC;
    return VamsasArchiveReader.VAMSASXML;
  }

  /**
   * 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;
  }

  /**
   * 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 Vobject[] getOriginalRoots() throws IOException,
      org.exolab.castor.xml.MarshalException,
      org.exolab.castor.xml.ValidationException {
    return VamsasArchive.getOriginalRoots(this);
  }

  /**
   * @return original document or a new empty document (with default provenance)
   * @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 {
    return getVamsasDocument("org.vamsas.simpleclient.VamsasArchive",
        "Created new empty document", null);
  }

  /**
   * Return the original document or a new empty document with initial
   * provenance entry.
   * 
   * @param provenance_user
   *          (null sets user to be the class name)
   * @param provenance_action
   *          (null sets action to be 'created new document')
   * @param version
   *          (null means use latest version)
   * @return (original document or a new vamsas document with supplied
   *         provenance and version info)
   * @throws IOException
   * @throws org.exolab.castor.xml.MarshalException
   * @throws org.exolab.castor.xml.ValidationException
   */
  public VamsasDocument getVamsasDocument(String provenance_user,
      String provenance_action, String version) throws IOException,
      org.exolab.castor.xml.MarshalException,
      org.exolab.castor.xml.ValidationException {
    if (_doc != null)
      return _doc;
    _doc = getOriginalVamsasDocument(this, getVorba());
    if (_doc != null)
      return _doc;
    // validate parameters
    if (provenance_user == null)
      provenance_user = "org.vamsas.simpleclient.VamsasArchive";
    if (provenance_action == null)
      provenance_action = "Created new empty document";
    if (version == null)
      version = VersionEntries.latestVersion();
    // Create a new document and return it
    _doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS() },
        ProvenanceStuff.newProvenance(provenance_user, provenance_action),
        version);
    return _doc;
  }

  /**
   * @return Returns the current VorbaIdFactory for the archive.
   */
  public VorbaIdFactory getVorba() {
    if (vorba == null)
      vorba = new SimpleDocument("simpleclient.VamsasArchive");
    return vorba.getVorba();
  }

  /**
   * @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;
  }

  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);
        }
      }
    }
  }

  /**
   * 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.
   * 
   * @param doclock
   *          LATER - pass existing lock on document, if it exists.... no need
   *          yet?
   * @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?");
    }

    if (rchive == null)
      rchive = new SessionFile(archive);
    if (!rchive.lockFile())
      throw new IOException("Failed to get lock on file " + archive);
    // LATER: locked IO stream based access.
    // Manifest newmanifest = new Manifest();
    newarchive = new org.apache.tools.zip.ZipOutputStream(rchive.fileLock
        .getBufferedOutputStream(true));// , newmanifest);
    // newarchive = new JarOutputStream(new BufferedOutputStream(new
    // java.io.FileOutputStream(archive)));
    entries = new Hashtable();
  }

  public void putVamsasDocument(VamsasDocument doc) throws IOException,
      org.exolab.castor.xml.MarshalException,
      org.exolab.castor.xml.ValidationException {
    putVamsasDocument(doc, getVorba());
  }

  /**
   * 
   * @param doc
   * @param vorba
   * @return (vorbaId string, Vobjhash) pairs for last hash of each object in
   *         document
   * @throws IOException
   * @throws org.exolab.castor.xml.MarshalException
   * @throws org.exolab.castor.xml.ValidationException
   */
  public void putVamsasDocument(VamsasDocument doc, VorbaIdFactory vorba)
      throws IOException, org.exolab.castor.xml.MarshalException,
      org.exolab.castor.xml.ValidationException {
    if (vamsasdocument)
      doc.setVersion(VersionEntries.latestVersion()); // LATER: ensure this does
                                                      // the correct thing.
    VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);
  }

  /**
   * recovers the original file's contents from the (temporary) backup.
   * 
   * @throws Exception
   *           if any SessionFile or file removal operations fail.
   */
  private void recoverBackup() throws Exception {
    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);

      rchive.updateFrom(null, bckup); // recover from backup file.
      bckup.unlockFile();
      bckup = null;
      removeBackup();
    }
  }

  /**
   * forget about any backup that was made - removing it first if it was only
   * temporary.
   */
  private void removeBackup() {
    if (originalBackup != null) {
      log.debug("Removing backup in " + originalBackup.getAbsolutePath());
      if (!donotdeletebackup)
        if (!originalBackup.delete())
          log.info("VamsasArchive couldn't remove temporary backup "
              + originalBackup.getAbsolutePath());
      originalBackup = null;
    }
  }

  /**
   * @param vorba
   *          the VorbaIdFactory to use for accessing vamsas objects.
   */
  public void setVorba(VorbaIdFactory Vorba) {
    if (Vorba != null) {
      if (vorba == null)
        vorba = new SimpleDocument(Vorba);
      else
        vorba.setVorba(Vorba);
    } else
      getVorba();
  }

  /**
   * 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);
  }

  /**
   * Validates the AppDataReference: not null and not already written to
   * archive.
   * 
   * @param AppDataReference
   * @return true if valid. false if not
   * @throws IOException
   *           for really broken references!
   */
  protected boolean _validNewAppDataReference(String newAppDataReference)
      throws IOException {
    // LATER: Specify valid AppDataReference form in all VamsasArchive handlers
    if (newAppDataReference == null)
      throw new IOException("null newAppDataReference!");
    if (entries.containsKey(newAppDataReference)) {
      log.warn("Attempt to write '" + newAppDataReference
          + "' twice! - IGNORED");
      // LATER: fix me? warning message should raise an exception here.
      return false;
    }
    return true;
  }

  /**
   * 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 {
    if (original == null || !original.exists()) {
      log.warn("No backup archive exists.");
      return false;
    }
    if (AppDataReference == null)
      throw new IOException("null AppDataReference!");

    if (!_validNewAppDataReference(NewAppDataReference))
      return false;

    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;
  }

  /**
   * write data from a stream into an appData reference.
   * 
   * @param AppDataReference
   *          - New AppDataReference not already written to archive
   * @param adstream
   *          Source of data for appData reference - read until .read(buffer)
   *          returns -1
   * @return true on success.
   * @throws IOException
   *           for file IO or invalid AppDataReference string
   */
  public boolean writeAppdataFromStream(String AppDataReference,
      java.io.InputStream adstream) throws IOException {
    if (!_validNewAppDataReference(AppDataReference)) {
      log.warn("Invalid AppDataReference passed to writeAppdataFromStream");
      throw new IOException(
          "Invalid AppDataReference! (null, or maybe non-unique)!");
    }

    if (AppDataReference == null) {
      log.warn("null appdata passed.");
      throw new IOException("Null AppDataReference");
    }

    java.io.OutputStream adout = getAppDataStream(AppDataReference);
    // 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);
    return true;
  }

  /**
   * transfers any AppDataReferences existing in the old document that haven't
   * already been transferred to the new one LATER: do the same for transfers
   * requiring a namechange - more document dependent.
   * 
   * @return true if data was transferred.
   */
  public boolean transferRemainingAppDatas() throws IOException {
    boolean transfered = false;
    if (original == null || !original.exists()) {
      log.warn("No backup archive exists.");
      return false;
    }
    accessOriginal();

    if (getVorba() != null) {
      Vector originalRefs = null;
      try {
        originalRefs = vorba.getReferencedEntries(getVamsasDocument(),
            getOriginalArchiveReader());
      } catch (Exception e) {
        log.warn("Problems accessing original document entries!", e);
      }
      if (originalRefs != null) {
        Iterator ref = originalRefs.iterator();
        while (ref.hasNext()) {
          String oldentry = (String) ref.next();
          if (oldentry != null && !entries.containsKey(oldentry)) {
            log.debug("Transferring remaining entry '" + oldentry + "'");
            transfered |= transferAppDataEntry(oldentry);
          }
        }
      }
    }
    return transfered;
  }

  /**
   * called after archive is written to put file in its final place
   */
  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
      makeBackup();
      try {
        // copy new Archive data that was writen to a temporary file
        odoclock.updateFrom(null, rchive);
      } catch (IOException e) {
        // LATER: decide if leaving nastily named backup files around is
        // necessary.
        File backupFile = backupFile();
        if (backupFile != null)
          log.error(
              "Problem updating archive from temporary file! - backup left in '"
                  + backupFile().getAbsolutePath() + "'", e);
        else
          log
              .error(
                  "Problems updating, and failed to even make a backup file. Ooops!",
                  e);
      }
      // Tidy up if necessary.
      removeBackup();
    } else {

    }
  }
}
