1 package org.vamsas.client.simpleclient;
3 import java.io.BufferedInputStream;
4 import java.io.BufferedOutputStream;
5 import java.io.DataOutputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.InputStreamReader;
10 import java.io.OutputStream;
11 import java.io.OutputStreamWriter;
12 import java.io.PrintWriter;
13 import java.util.Hashtable;
14 import java.util.Iterator;
15 import java.util.Vector;
16 import java.util.jar.JarEntry;
17 import java.util.jar.JarOutputStream;
18 import java.util.jar.Manifest;
20 import org.apache.commons.logging.Log;
21 import org.apache.commons.logging.LogFactory;
22 import org.vamsas.client.ClientHandle;
23 import org.vamsas.client.IVorbaIdFactory;
24 import org.vamsas.client.SessionHandle;
25 import org.vamsas.client.UserHandle;
26 import org.vamsas.client.VorbaIdFactory;
27 import org.vamsas.client.VorbaXmlBinder;
28 import org.vamsas.client.Vobject;
29 import org.vamsas.objects.core.ApplicationData;
30 import org.vamsas.objects.core.VAMSAS;
31 import org.vamsas.objects.core.VamsasDocument;
32 import org.vamsas.objects.utils.AppDataReference;
33 import org.vamsas.objects.utils.DocumentStuff;
34 import org.vamsas.objects.utils.ProvenanceStuff;
35 import org.vamsas.objects.utils.document.VersionEntries;
38 * Class for high-level io and Jar manipulation involved in creating
39 * or updating a vamsas archive (with backups).
40 * Writes to a temporary file and then swaps new file for backup.
41 * uses the sessionFile locking mechanism for safe I/O
45 public class VamsasArchive {
46 private static Log log = LogFactory.getLog(VamsasArchive.class);
48 * Access original document if it exists, and get VAMSAS root objects.
49 * @return vector of vamsas roots from original document
52 public static Vobject[] getOriginalRoots(VamsasArchive ths) throws IOException,
53 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
54 VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
57 if (oReader.isValid()) {
58 InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
59 VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
61 return doc.getVAMSAS();
62 // TODO ensure embedded appDatas are garbage collected to save memory
64 InputStream vxmlis = oReader.getVamsasXmlStream();
65 if (vxmlis!=null) { // Might be an old vamsas file.
66 BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
67 InputStreamReader vxml = new InputStreamReader(ixml);
68 VAMSAS root[] = new VAMSAS[1];
69 root[0] = VAMSAS.unmarshal(vxml);
78 * Access the original vamsas document for a VamsasArchive class, and return it.
79 * Users of the VamsasArchive class should use the getVamsasDocument method to retrieve
80 * the current document - only use this one if you want the 'backup' version.
81 * TODO: catch OutOfMemoryError - they are likely to occur here.
82 * NOTE: vamsas.xml datastreams are constructed as 'ALPHA_VERSION' vamsas documents.
84 * @return null if no document exists.
86 * @throws org.exolab.castor.xml.MarshalException
87 * @throws org.exolab.castor.xml.ValidationException
89 public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths) throws IOException,
90 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
91 return VamsasArchive.getOriginalVamsasDocument(ths, null);
94 * Uses VorbaXmlBinder to retrieve the VamsasDocument from the original archive referred to by ths
99 * @throws org.exolab.castor.xml.MarshalException
100 * @throws org.exolab.castor.xml.ValidationException
102 public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths, VorbaIdFactory vorba) throws IOException,
103 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
104 VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
107 return ths.vorba.getVamsasDocument(oReader);
109 // otherwise - there was no valid original document to read.
113 * destination of new archive data (tempfile if virginarchive=true, original archive location otherwise)
115 java.io.File archive=null;
117 * locked IO handler for new archive file
119 SessionFile rchive=null;
121 * original archive file to be updated (or null if virgin) where new data will finally reside
123 java.io.File original=null;
125 * original archive IO handler
127 SessionFile odoclock = null;
128 Lock destinationLock = null;
130 * Original archive reader class
132 VamsasArchiveReader odoc = null;
134 * true if a real vamsas document is being written.
136 boolean vamsasdocument=true;
138 * Output stream for archived data
140 JarOutputStream newarchive=null;
142 * JarEntries written to archive
144 Hashtable entries = null;
147 * true if we aren't just updating an archive
149 private boolean virginArchive=false;
152 * name of backup of existing archive that has been updated/overwritten.
153 * only one backup will be made - and this is it.
155 File originalBackup = null;
157 boolean donotdeletebackup=false;
158 private final int _TRANSFER_BUFFER=4096*4;
159 protected SimpleDocument vorba = null;
161 * LATER: ? CUT'n'Paste error ?
162 * Access and return current vamsas Document, if it exists, or create a new one
163 * (without affecting VamsasArchive object state - so is NOT THREAD SAFE)
164 * _TODO: possibly modify internal state to lock low-level files
165 * (like the IClientDocument interface instance constructer would do)
166 * @see org.vamsas.simpleclient.VamsasArchive.getOriginalVamsasDocument for additional caveats
169 * @throws IOException
170 * @throws org.exolab.castor.xml.MarshalException
171 * @throws org.exolab.castor.xml.ValidationException
172 * ????? where does this live JBPNote ?
174 private VamsasDocument _doc=null;
177 * Create a new vamsas archive
178 * File locks are made immediately to avoid contention
180 * @param archive - file spec for new vamsas archive
181 * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive
182 * @throws IOException if call to accessOriginal failed for updates, or openArchive failed.
184 public VamsasArchive(File archive, boolean vamsasdocument) throws IOException {
185 this(archive, false, vamsasdocument, null);
187 public VamsasArchive(File archive, boolean vamsasdocument, boolean overwrite) throws IOException {
188 this(archive, overwrite, vamsasdocument, null);
191 * Constructor for accessing Files under file-lock management (ie a session file)
193 * @param vamsasdocument
195 * @throws IOException
197 public VamsasArchive(VamsasFile archive, boolean vamsasdocument, boolean overwrite) throws IOException {
198 this(archive.sessionFile, overwrite, vamsasdocument, archive);
202 * @param archive file to write
203 * @param overwrite true if original contents should be deleted
204 * @param vamsasdocument true if a proper VamsasDocument archive is to be written.
205 * @param extantLock SessionFile object holding a lock for the <object>archive</object>
206 * @throws IOException
208 public VamsasArchive(File archive, boolean overwrite, boolean vamsasdocument, SessionFile extantLock) throws IOException {
210 if (archive==null || (archive!=null && !(archive.getParentFile().canWrite() && (!archive.exists() || archive.canWrite())))) {
211 log.fatal("Expect Badness! -- Invalid parameters for VamsasArchive constructor:"+((archive!=null)
212 ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
216 this.vamsasdocument = vamsasdocument;
217 if (archive.exists() && !overwrite) {
218 this.original = archive;
219 if (extantLock!=null) {
220 this.odoclock = extantLock;
221 if (odoclock.fileLock==null || !odoclock.fileLock.isLocked())
224 this.odoclock = new SessionFile(archive);
226 odoclock.lockFile(); // lock the file *immediatly*
227 this.archive = null; // archive will be a temp file when the open method is called
230 this.accessOriginal();
231 } catch (IOException e) {
232 throw new IOException("Lock failed for existing archive"+archive);
235 this.original = null;
236 this.archive = archive; // archive is written in place.
237 if (extantLock!=null)
240 rchive = new SessionFile(archive);
242 if (rchive.fileLock==null || rchive.fileLock.rafile==null || !rchive.fileLock.isLocked())
243 throw new IOException("Lock failed for new archive"+archive);
244 rchive.fileLock.rafile.setLength(0); // empty the archive.
245 virginArchive = true;
247 this.openArchive(); // open archive
250 * open original archive file for exclusive (locked) reading.
251 * @throws IOException
253 private void accessOriginal() throws IOException {
254 if (original!=null && original.exists()) {
256 odoclock = new SessionFile(original);
259 odoc = new VamsasArchiveReader(odoclock.fileLock);
264 * Add unique entry strings to internal JarEntries list.
266 * @return true if entry was unique and was added.
268 private boolean addEntry(String entry) {
270 entries=new Hashtable();
271 if (entries.containsKey(entry))
273 entries.put(entry, new Integer(entries.size()));
277 * adds named entry to newarchive or returns false.
279 * @return true if entry was unique and could be added
280 * @throws IOException if entry name was invalid or a new entry could not be made on newarchive
282 private boolean addValidEntry(String entry) throws IOException {
283 JarEntry je = new JarEntry(entry);
284 if (!addEntry(entry))
287 newarchive.putNextEntry(je);
291 * called by app to get name of backup if it was made.
292 * If this is called, the caller app *must* delete the backup themselves.
293 * @return null or a valid file object
295 public File backupFile() {
297 if (!virginArchive) {
299 donotdeletebackup=false; // external reference has been made.
300 return ((original!=null) ? originalBackup : null);
306 * Stops any current write to archive, and reverts to the backup if it exists.
307 * All existing locks on the original will be released. All backup files are removed.
309 public boolean cancelArchive() {
310 if (newarchive!=null) {
312 newarchive.closeEntry();
313 newarchive.putNextEntry(new JarEntry("deleted"));
314 newarchive.closeEntry();
317 } catch (Exception e) {
318 log.debug("Whilst closing newarchive",e);
320 if (!virginArchive) {
321 // then there is something to recover.
325 catch (Exception e) {
326 log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
332 log.warn("Client Error: cancelArchive called before archive("+original.getAbsolutePath()+") has been opened!");
334 closeAndReset(); // tidy up and release locks.
339 * only do this if you want to destroy the current file output stream
342 private void closeAndReset() {
347 if (original!=null) {
354 if (odoclock!=null) {
355 odoclock.unlockFile();
365 * Tidies up and closes archive, removing any backups that were created.
366 * NOTE: It is up to the caller to delete the original archive backup obtained from backupFile()
367 * TODO: ensure all extant AppDataReference jar entries are transferred to new Jar
368 * TODO: provide convenient mechanism for generating new unique AppDataReferences and adding them to the document
370 public void closeArchive() throws IOException {
371 if (newarchive!=null) {
373 newarchive.closeEntry();
374 if (!isDocumentWritten())
375 log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
380 log.warn("Attempt to close archive that has not been opened for writing.");
384 * Opens and returns the applicationData output stream for the appdataReference string.
385 * @param appdataReference
386 * @return Output stream to write to
387 * @throws IOException
389 public AppDataOutputStream getAppDataStream(String appdataReference) throws IOException {
390 if (newarchive==null)
391 throw new IOException("Attempt to write to closed VamsasArchive object.");
392 if (addValidEntry(appdataReference)) {
393 return new AppDataOutputStream(newarchive);
400 * @return JarEntry name for the vamsas XML stream in this archive
402 protected String getDocumentJarEntry() {
404 return VamsasArchiveReader.VAMSASDOC;
405 return VamsasArchiveReader.VAMSASXML;
408 * Safely initializes the VAMSAS XML document Jar Entry.
409 * @return Writer to pass to the marshalling function.
410 * @throws IOException if a document entry has already been written.
412 public PrintWriter getDocumentOutputStream() throws IOException {
413 if (newarchive==null)
415 if (!isDocumentWritten()) {
417 if (addValidEntry(getDocumentJarEntry()))
418 return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
419 } catch (Exception e) {
420 log.warn("Problems opening XML document JarEntry stream",e);
423 throw new IOException("Vamsas Document output stream is already written.");
429 * Access original archive if it exists, pass the reader to the client
430 * Note: this is NOT thread safe and a call to closeArchive() will by necessity
431 * close and invalidate the VamsasArchiveReader object.
432 * @return null if no original archive exists.
434 public VamsasArchiveReader getOriginalArchiveReader() throws IOException {
435 if (!virginArchive) {
442 * returns original document's root vamsas elements.
444 * @throws IOException
445 * @throws org.exolab.castor.xml.MarshalException
446 * @throws org.exolab.castor.xml.ValidationException
448 public Vobject[] getOriginalRoots() throws IOException,
449 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
450 return VamsasArchive.getOriginalRoots(this);
453 * @return original document or a new empty document (with default provenance)
454 * @throws IOException
455 * @throws org.exolab.castor.xml.MarshalException
456 * @throws org.exolab.castor.xml.ValidationException
458 public VamsasDocument getVamsasDocument() throws IOException,
459 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
460 return getVamsasDocument("org.vamsas.simpleclient.VamsasArchive", "Created new empty document", null);
463 * Return the original document or a new empty document with initial provenance entry.
464 * @param provenance_user (null sets user to be the class name)
465 * @param provenance_action (null sets action to be 'created new document')
466 * @param version (null means use latest version)
467 * @return (original document or a new vamsas document with supplied provenance and version info)
468 * @throws IOException
469 * @throws org.exolab.castor.xml.MarshalException
470 * @throws org.exolab.castor.xml.ValidationException
472 public VamsasDocument getVamsasDocument(String provenance_user, String provenance_action, String version) throws IOException,
473 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
476 _doc = getOriginalVamsasDocument(this, getVorba());
479 // validate parameters
480 if (provenance_user==null)
481 provenance_user = "org.vamsas.simpleclient.VamsasArchive";
482 if (provenance_action == null)
483 provenance_action="Created new empty document";
485 version = VersionEntries.latestVersion();
486 // Create a new document and return it
487 _doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS()},
488 ProvenanceStuff.newProvenance(provenance_user, provenance_action), version);
492 * @return Returns the current VorbaIdFactory for the archive.
494 public VorbaIdFactory getVorba() {
496 vorba = new SimpleDocument("simpleclient.VamsasArchive");
497 return vorba.getVorba();
500 * @return true if Vamsas Document has been written to archive
502 protected boolean isDocumentWritten() {
503 if (newarchive==null)
504 log.warn("isDocumentWritten() called for unopened archive.");
506 if (entries.containsKey(getDocumentJarEntry()))
511 private void makeBackup() {
512 if (!virginArchive) {
513 if (originalBackup==null && original!=null && original.exists()) {
516 originalBackup = odoclock.backupSessionFile(null, original.getName(), ".bak", original.getParentFile());
518 catch (IOException e) {
519 log.warn("Problem whilst making a backup of original archive.",e);
525 * opens the new archive ready for writing. If the new archive is replacing an existing one,
526 * then the existing archive will be locked, and the new archive written to a temporary file.
527 * The new archive will be put in place once close() is called.
528 * @param doclock LATER - pass existing lock on document, if it exists.... no need yet?
529 * @throws IOException
531 private void openArchive() throws IOException {
533 if (newarchive!=null) {
534 log.warn("openArchive() called multiple times.");
535 throw new IOException("Vamsas Archive '"+archive.getAbsolutePath()+"' is already open.");
537 if (archive==null && (virginArchive || original==null)) {
538 log.warn("openArchive called on uninitialised VamsasArchive object.");
539 throw new IOException("Badly initialised VamsasArchive object - no archive file specified.");
541 if (!virginArchive) {
544 // make a temporary file to write to
545 archive = File.createTempFile(original.getName(), ".new",original.getParentFile());
547 if (archive.exists())
548 log.warn("New archive file name already in use! Possible lock failure imminent?");
552 rchive = new SessionFile(archive);
553 if (!rchive.lockFile())
554 throw new IOException("Failed to get lock on file "+archive);
555 Manifest newmanifest = new Manifest();
556 newarchive = new JarOutputStream(rchive.fileLock.getBufferedOutputStream(true), newmanifest);
557 entries = new Hashtable();
559 public void putVamsasDocument(VamsasDocument doc) throws IOException,
560 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
561 putVamsasDocument(doc, getVorba());
564 public void putVamsasDocument(VamsasDocument doc, VorbaIdFactory vorba) throws IOException,
565 org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
567 doc.setVersion(VersionEntries.latestVersion()); // LATER: ensure this does the correct thing.
568 VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);
572 * recovers the original file's contents from the (temporary) backup.
573 * @throws Exception if any SessionFile or file removal operations fail.
575 private void recoverBackup() throws Exception {
576 if (originalBackup!=null) {
577 // backup has been made.
578 // revert from backup and delete it (changing backup filename)
580 rchive = new SessionFile(original);
582 SessionFile bckup = new SessionFile(originalBackup);
584 rchive.updateFrom(null, bckup); // recover from backup file.
592 * forget about any backup that was made - removing it first if it was only temporary.
594 private void removeBackup() {
595 if (originalBackup!=null) {
596 log.debug("Removing backup in "+originalBackup.getAbsolutePath());
597 if (!donotdeletebackup)
598 if (!originalBackup.delete())
599 log.info("VamsasArchive couldn't remove temporary backup "+originalBackup.getAbsolutePath());
604 * @param vorba the VorbaIdFactory to use for accessing vamsas objects.
606 public void setVorba(VorbaIdFactory Vorba) {
609 vorba = new SimpleDocument(Vorba);
611 vorba.setVorba(Vorba);
616 * Convenience method to copy over the referred entry from the backup to the new version.
617 * Warning messages are raised if no backup exists or the
618 * entry doesn't exist in the backed-up original.
619 * Duplicate writes return true - but a warning message will also be raised.
620 * @param AppDataReference
621 * @return true if AppDataReference now exists in the new document
622 * @throws IOException
624 public boolean transferAppDataEntry(String AppDataReference) throws IOException {
625 return transferAppDataEntry(AppDataReference, AppDataReference);
628 * Validates the AppDataReference: not null and not already written to archive.
629 * @param AppDataReference
630 * @return true if valid. false if not
631 * @throws IOException for really broken references!
633 protected boolean _validNewAppDataReference(String newAppDataReference) throws IOException {
634 // LATER: Specify valid AppDataReference form in all VamsasArchive handlers
635 if (newAppDataReference==null)
636 throw new IOException("null newAppDataReference!");
637 if (entries.containsKey(newAppDataReference)) {
638 log.warn("Attempt to write '"+newAppDataReference+"' twice! - IGNORED");
639 // LATER: fix me? warning message should raise an exception here.
645 * Transfers an AppDataReference from old to new vamsas archive, with a name change.
646 * @see transferAppDataEntry(String AppDataReference)
647 * @param AppDataReference
648 * @param NewAppDataReference - AppDataReference in new Archive
650 * @throws IOException
652 public boolean transferAppDataEntry(String AppDataReference, String NewAppDataReference) throws IOException {
653 if (original==null || !original.exists()) {
654 log.warn("No backup archive exists.");
657 if (AppDataReference==null)
658 throw new IOException("null AppDataReference!");
660 if (!_validNewAppDataReference(NewAppDataReference))
665 java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
667 if (adstream==null) {
668 log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
672 java.io.OutputStream adout = getAppDataStream(NewAppDataReference);
673 // copy over the bytes
676 byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
678 if ((written = adstream.read(buffer))>-1) {
679 adout.write(buffer, 0, written);
680 log.debug("Transferring "+written+".");
683 } while (written>-1);
684 log.debug("Sucessfully transferred AppData for '"
685 +AppDataReference+"' as '"+NewAppDataReference+"' ("+count+" bytes)");
689 * write data from a stream into an appData reference.
690 * @param AppDataReference - New AppDataReference not already written to archive
691 * @param adstream Source of data for appData reference - read until .read(buffer) returns -1
692 * @return true on success.
693 * @throws IOException for file IO or invalid AppDataReference string
695 public boolean writeAppdataFromStream(String AppDataReference, java.io.InputStream adstream) throws IOException {
696 if (!_validNewAppDataReference(AppDataReference)) {
697 log.warn("Invalid AppDataReference passed to writeAppdataFromStream");
698 throw new IOException("Invalid AppDataReference! (null, or maybe non-unique)!");
701 if (AppDataReference==null) {
702 log.warn("null appdata passed.");
703 throw new IOException("Null AppDataReference");
706 java.io.OutputStream adout = getAppDataStream(AppDataReference);
707 // copy over the bytes
710 byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
712 if ((written = adstream.read(buffer))>-1) {
713 adout.write(buffer, 0, written);
714 log.debug("Transferring "+written+".");
717 } while (written>-1);
721 * transfers any AppDataReferences existing in the old document
722 * that haven't already been transferred to the new one
723 * LATER: do the same for transfers requiring a namechange - more document dependent.
724 * @return true if data was transferred.
726 public boolean transferRemainingAppDatas() throws IOException {
727 boolean transfered=false;
728 if (original==null || !original.exists()) {
729 log.warn("No backup archive exists.");
734 if (getVorba()!=null) {
735 Vector originalRefs=null;
737 originalRefs = vorba.getReferencedEntries(getVamsasDocument(), getOriginalArchiveReader());
738 } catch (Exception e) {
739 log.warn("Problems accessing original document entries!",e);
741 if (originalRefs!=null) {
742 Iterator ref = originalRefs.iterator();
743 while (ref.hasNext()) {
744 String oldentry = (String) ref.next();
745 if (oldentry!=null && !entries.containsKey(oldentry)) {
746 log.debug("Transferring remaining entry '"+oldentry+"'");
747 transfered |= transferAppDataEntry(oldentry);
755 * called after archive is written to put file in its final place
757 private void updateOriginal() {
758 if (!virginArchive) {
759 // make sure original document really is backed up and then overwrite it.
761 // try to shut the odoc reader.
765 // Make a backup if it isn't done already
768 // copy new Archive data that was writen to a temporary file
769 odoclock.updateFrom(null, rchive);
771 catch (IOException e) {
772 // LATER: decide if leaving nastily named backup files around is necessary.
773 log.error("Problem updating archive from temporary file! - backup left in '"
774 +backupFile().getAbsolutePath()+"'",e);
776 // Tidy up if necessary.