applied LGPLv3 and source code formatting.
[vamsas.git] / src / uk / ac / vamsas / client / simpleclient / VamsasArchive.java
1 /*\r
2  * This file is part of the Vamsas Client version 0.1. \r
3  * Copyright 2009 by Jim Procter, Iain Milne, Pierre Marguerite, \r
4  *  Andrew Waterhouse and Dominik Lindner.\r
5  * \r
6  * Earlier versions have also been incorporated into Jalview version 2.4 \r
7  * since 2008, and TOPALi version 2 since 2007.\r
8  * \r
9  * The Vamsas Client is free software: you can redistribute it and/or modify\r
10  * it under the terms of the GNU Lesser General Public License as published by\r
11  * the Free Software Foundation, either version 3 of the License, or\r
12  * (at your option) any later version.\r
13  *  \r
14  * The Vamsas Client is distributed in the hope that it will be useful,\r
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
17  * GNU Lesser General Public License for more details.\r
18  * \r
19  * You should have received a copy of the GNU Lesser General Public License\r
20  * along with the Vamsas Client.  If not, see <http://www.gnu.org/licenses/>.\r
21  */\r
22 package uk.ac.vamsas.client.simpleclient;\r
23 \r
24 import java.io.BufferedInputStream;\r
25 import java.io.BufferedOutputStream;\r
26 import java.io.DataOutputStream;\r
27 import java.io.File;\r
28 import java.io.IOException;\r
29 import java.io.InputStream;\r
30 import java.io.InputStreamReader;\r
31 import java.io.OutputStream;\r
32 import java.io.OutputStreamWriter;\r
33 import java.io.PrintWriter;\r
34 import java.util.Hashtable;\r
35 import java.util.Iterator;\r
36 import java.util.Vector;\r
37 import java.util.jar.JarEntry;\r
38 import java.util.jar.JarOutputStream;\r
39 import java.util.jar.Manifest;\r
40 \r
41 import org.apache.commons.logging.Log;\r
42 import org.apache.commons.logging.LogFactory;\r
43 \r
44 import uk.ac.vamsas.client.AppDataOutputStream;\r
45 import uk.ac.vamsas.client.ClientHandle;\r
46 import uk.ac.vamsas.client.IVorbaIdFactory;\r
47 import uk.ac.vamsas.client.SessionHandle;\r
48 import uk.ac.vamsas.client.UserHandle;\r
49 import uk.ac.vamsas.client.Vobject;\r
50 import uk.ac.vamsas.client.VorbaIdFactory;\r
51 import uk.ac.vamsas.client.VorbaXmlBinder;\r
52 import uk.ac.vamsas.objects.core.ApplicationData;\r
53 import uk.ac.vamsas.objects.core.VAMSAS;\r
54 import uk.ac.vamsas.objects.core.VamsasDocument;\r
55 import uk.ac.vamsas.objects.utils.AppDataReference;\r
56 import uk.ac.vamsas.objects.utils.DocumentStuff;\r
57 import uk.ac.vamsas.objects.utils.ProvenanceStuff;\r
58 import uk.ac.vamsas.objects.utils.document.VersionEntries;\r
59 \r
60 /**\r
61  * Class for high-level io and Jar manipulation involved in creating or updating\r
62  * a vamsas archive (with backups). Writes to a temporary file and then swaps\r
63  * new file for backup. uses the sessionFile locking mechanism for safe I/O\r
64  * \r
65  * @author jimp\r
66  * \r
67  */\r
68 public class VamsasArchive {\r
69   private static Log log = LogFactory.getLog(VamsasArchive.class);\r
70 \r
71   /**\r
72    * Access original document if it exists, and get VAMSAS root objects.\r
73    * \r
74    * @return vector of vamsas roots from original document\r
75    * @throws IOException\r
76    */\r
77   public static Vobject[] getOriginalRoots(VamsasArchive ths)\r
78       throws IOException, org.exolab.castor.xml.MarshalException,\r
79       org.exolab.castor.xml.ValidationException {\r
80     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();\r
81     if (oReader != null) {\r
82 \r
83       if (oReader.isValid()) {\r
84         InputStreamReader vdoc = new InputStreamReader(oReader\r
85             .getVamsasDocumentStream());\r
86         VamsasDocument doc = VamsasDocument.unmarshal(vdoc);\r
87         if (doc != null)\r
88           return doc.getVAMSAS();\r
89         // TODO ensure embedded appDatas are garbage collected to save memory\r
90       } else {\r
91         InputStream vxmlis = oReader.getVamsasXmlStream();\r
92         if (vxmlis != null) { // Might be an old vamsas file.\r
93           BufferedInputStream ixml = new BufferedInputStream(oReader\r
94               .getVamsasXmlStream());\r
95           InputStreamReader vxml = new InputStreamReader(ixml);\r
96           VAMSAS root[] = new VAMSAS[1];\r
97           root[0] = VAMSAS.unmarshal(vxml);\r
98           if (root[0] != null)\r
99             return root;\r
100         }\r
101       }\r
102     }\r
103     return null;\r
104   }\r
105 \r
106   /**\r
107    * Access the original vamsas document for a VamsasArchive class, and return\r
108    * it. Users of the VamsasArchive class should use the getVamsasDocument\r
109    * method to retrieve the current document - only use this one if you want the\r
110    * 'backup' version. TODO: catch OutOfMemoryError - they are likely to occur\r
111    * here. NOTE: vamsas.xml datastreams are constructed as 'ALPHA_VERSION'\r
112    * vamsas documents.\r
113    * \r
114    * @param ths\r
115    * @return null if no document exists.\r
116    * @throws IOException\r
117    * @throws org.exolab.castor.xml.MarshalException\r
118    * @throws org.exolab.castor.xml.ValidationException\r
119    */\r
120   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths)\r
121       throws IOException, org.exolab.castor.xml.MarshalException,\r
122       org.exolab.castor.xml.ValidationException {\r
123     return VamsasArchive.getOriginalVamsasDocument(ths, null);\r
124   }\r
125 \r
126   /**\r
127    * Uses VorbaXmlBinder to retrieve the VamsasDocument from the original\r
128    * archive referred to by ths\r
129    * \r
130    * @param ths\r
131    * @param vorba\r
132    * @return\r
133    * @throws IOException\r
134    * @throws org.exolab.castor.xml.MarshalException\r
135    * @throws org.exolab.castor.xml.ValidationException\r
136    */\r
137   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths,\r
138       VorbaIdFactory vorba) throws IOException,\r
139       org.exolab.castor.xml.MarshalException,\r
140       org.exolab.castor.xml.ValidationException {\r
141     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();\r
142     if (oReader != null) {\r
143       ths.setVorba(vorba);\r
144       return ths.vorba.getVamsasDocument(oReader);\r
145     }\r
146     // otherwise - there was no valid original document to read.\r
147     return null;\r
148   }\r
149 \r
150   /**\r
151    * destination of new archive data (tempfile if virginarchive=true, original\r
152    * archive location otherwise)\r
153    */\r
154   java.io.File archive = null;\r
155 \r
156   /**\r
157    * locked IO handler for new archive file\r
158    */\r
159   SessionFile rchive = null;\r
160 \r
161   /**\r
162    * original archive file to be updated (or null if virgin) where new data will\r
163    * finally reside\r
164    */\r
165   java.io.File original = null;\r
166 \r
167   /**\r
168    * original archive IO handler\r
169    */\r
170   SessionFile odoclock = null;\r
171 \r
172   Lock destinationLock = null;\r
173 \r
174   /**\r
175    * Original archive reader class\r
176    */\r
177   VamsasArchiveReader odoc = null;\r
178 \r
179   /**\r
180    * true if a real vamsas document is being written.\r
181    */\r
182   boolean vamsasdocument = true;\r
183 \r
184   /**\r
185    * Output stream for archived data\r
186    */\r
187   org.apache.tools.zip.ZipOutputStream newarchive = null;\r
188 \r
189   /**\r
190    * JarEntries written to archive\r
191    */\r
192   Hashtable entries = null;\r
193 \r
194   /**\r
195    * true if we aren't just updating an archive\r
196    */\r
197   private boolean virginArchive = false;\r
198 \r
199   /**\r
200    * name of backup of existing archive that has been updated/overwritten. only\r
201    * one backup will be made - and this is it.\r
202    */\r
203   File originalBackup = null;\r
204 \r
205   boolean donotdeletebackup = false;\r
206 \r
207   private final int _TRANSFER_BUFFER = 4096 * 4;\r
208 \r
209   protected SimpleDocument vorba = null;\r
210 \r
211   /**\r
212    * LATER: ? CUT'n'Paste error ? Access and return current vamsas Document, if\r
213    * it exists, or create a new one (without affecting VamsasArchive object\r
214    * state - so is NOT THREAD SAFE) _TODO: possibly modify internal state to\r
215    * lock low-level files (like the IClientDocument interface instance\r
216    * constructer would do)\r
217    * \r
218    * @see org.vamsas.simpleclient.VamsasArchive.getOriginalVamsasDocument for\r
219    *      additional caveats\r
220    * \r
221    * @return\r
222    * @throws IOException\r
223    * @throws org.exolab.castor.xml.MarshalException\r
224    * @throws org.exolab.castor.xml.ValidationException\r
225    *           ????? where does this live JBPNote ?\r
226    */\r
227   private VamsasDocument _doc = null;\r
228 \r
229   /**\r
230    * Create a new vamsas archive File locks are made immediately to avoid\r
231    * contention\r
232    * \r
233    * @param archive\r
234    *          - file spec for new vamsas archive\r
235    * @param vamsasdocument\r
236    *          true if archive is to be a fully fledged vamsas document archive\r
237    * @throws IOException\r
238    *           if call to accessOriginal failed for updates, or openArchive\r
239    *           failed.\r
240    */\r
241   public VamsasArchive(File archive, boolean vamsasdocument) throws IOException {\r
242     this(archive, false, vamsasdocument, null);\r
243   }\r
244 \r
245   public VamsasArchive(File archive, boolean vamsasdocument, boolean overwrite)\r
246       throws IOException {\r
247     this(archive, overwrite, vamsasdocument, null);\r
248   }\r
249 \r
250   /**\r
251    * Constructor for accessing Files under file-lock management (ie a session\r
252    * file)\r
253    * \r
254    * @param archive\r
255    * @param vamsasdocument\r
256    * @param overwrite\r
257    * @throws IOException\r
258    */\r
259   public VamsasArchive(VamsasFile archive, boolean vamsasdocument,\r
260       boolean overwrite) throws IOException {\r
261     this(archive.sessionFile, overwrite, vamsasdocument, archive);\r
262     // log.debug("using non-functional lock-IO stream jar access constructor");\r
263   }\r
264 \r
265   /**\r
266    * read and write to archive - will not overwrite original contents, and will\r
267    * always write an up to date vamsas document structure.\r
268    * \r
269    * @param archive\r
270    * @throws IOException\r
271    */\r
272   public VamsasArchive(VamsasFile archive) throws IOException {\r
273     this(archive, true, false);\r
274   }\r
275 \r
276   /**\r
277    * \r
278    * @param archive\r
279    *          file to write\r
280    * @param overwrite\r
281    *          true if original contents should be deleted\r
282    * @param vamsasdocument\r
283    *          true if a proper VamsasDocument archive is to be written.\r
284    * @param extantLock\r
285    *          SessionFile object holding a lock for the <object>archive</object>\r
286    * @throws IOException\r
287    */\r
288   public VamsasArchive(File archive, boolean overwrite, boolean vamsasdocument,\r
289       SessionFile extantLock) throws IOException {\r
290     super();\r
291     if (archive == null\r
292         || (archive != null && !(archive.getAbsoluteFile().getParentFile()\r
293             .canWrite() && (!archive.exists() || archive.canWrite())))) {\r
294       log\r
295           .fatal("Expect Badness! -- Invalid parameters for VamsasArchive constructor:"\r
296               + ((archive != null) ? "File cannot be overwritten."\r
297                   : "Null Object not valid constructor parameter"));\r
298       return;\r
299     }\r
300 \r
301     this.vamsasdocument = vamsasdocument;\r
302     if (archive.exists() && !overwrite) {\r
303       this.original = archive;\r
304       if (extantLock != null) {\r
305         this.odoclock = extantLock;\r
306         if (odoclock.fileLock == null || !odoclock.fileLock.isLocked())\r
307           odoclock.lockFile();\r
308       } else {\r
309         this.odoclock = new SessionFile(archive);\r
310       }\r
311       odoclock.lockFile(); // lock the file *immediatly*\r
312       this.archive = null; // archive will be a temp file when the open method\r
313                            // is called\r
314       virginArchive = false;\r
315       try {\r
316         this.accessOriginal();\r
317       } catch (IOException e) {\r
318         throw new IOException("Lock failed for existing archive" + archive);\r
319       }\r
320     } else {\r
321       this.original = null;\r
322       this.archive = archive; // archive is written in place.\r
323       if (extantLock != null)\r
324         rchive = extantLock;\r
325       else\r
326         rchive = new SessionFile(archive);\r
327       rchive.lockFile();\r
328       if (rchive.fileLock == null || !rchive.fileLock.isLocked())\r
329         throw new IOException("Lock failed for new archive" + archive);\r
330       rchive.fileLock.getRaFile().setLength(0); // empty the archive.\r
331       virginArchive = true;\r
332     }\r
333     this.openArchive(); // open archive\r
334   }\r
335 \r
336   /**\r
337    * open original archive file for exclusive (locked) reading.\r
338    * \r
339    * @throws IOException\r
340    */\r
341   private void accessOriginal() throws IOException {\r
342     if (original != null && original.exists()) {\r
343       if (odoclock == null)\r
344         odoclock = new SessionFile(original);\r
345       odoclock.lockFile();\r
346       if (odoc == null)\r
347         odoc = new VamsasArchiveReader(original);\r
348       // this constructor is not implemented yet odoc = new\r
349       // VamsasArchiveReader(odoclock.fileLock);\r
350     }\r
351   }\r
352 \r
353   /**\r
354    * Add unique entry strings to internal JarEntries list.\r
355    * \r
356    * @param entry\r
357    * @return true if entry was unique and was added.\r
358    */\r
359   private boolean addEntry(String entry) {\r
360     if (entries == null)\r
361       entries = new Hashtable();\r
362     if (log.isDebugEnabled()) {\r
363       log.debug("validating '" + entry + "' in hash for " + this);\r
364     }\r
365     if (entries.containsKey(entry))\r
366       return false;\r
367     entries.put(entry, new Integer(entries.size()));\r
368     return true;\r
369   }\r
370 \r
371   /**\r
372    * adds named entry to newarchive or returns false.\r
373    * \r
374    * @param entry\r
375    * @return true if entry was unique and could be added\r
376    * @throws IOException\r
377    *           if entry name was invalid or a new entry could not be made on\r
378    *           newarchive\r
379    */\r
380   private boolean addValidEntry(String entry) throws IOException {\r
381     org.apache.tools.zip.ZipEntry je = new org.apache.tools.zip.ZipEntry(entry);\r
382     // je.setExsetExtra(null);\r
383     if (!addEntry(entry))\r
384       return false;\r
385     newarchive.flush();\r
386     newarchive.putNextEntry(je);\r
387     return true;\r
388   }\r
389 \r
390   /**\r
391    * called by app to get name of backup if it was made. If this is called, the\r
392    * caller app *must* delete the backup themselves.\r
393    * \r
394    * @return null or a valid file object\r
395    */\r
396   public File backupFile() {\r
397 \r
398     if (!virginArchive) {\r
399       makeBackup();\r
400       donotdeletebackup = true; // external reference has been made.\r
401       return ((original != null) ? originalBackup : null);\r
402     }\r
403     return null;\r
404   }\r
405 \r
406   /**\r
407    * Stops any current write to archive, and reverts to the backup if it exists.\r
408    * All existing locks on the original will be released. All backup files are\r
409    * removed.\r
410    */\r
411   public boolean cancelArchive() {\r
412     if (newarchive != null) {\r
413       try {\r
414         newarchive.closeEntry();\r
415         newarchive.putNextEntry(new org.apache.tools.zip.ZipEntry("deleted"));\r
416         newarchive.closeEntry();\r
417         newarchive.close();\r
418 \r
419       } catch (Exception e) {\r
420         log.debug("Whilst closing newarchive", e);\r
421       }\r
422       ;\r
423       if (!virginArchive) {\r
424         // then there is something to recover.\r
425         try {\r
426           recoverBackup();\r
427         } catch (Exception e) {\r
428           log.warn("Problems when trying to cancel Archive "\r
429               + archive.getAbsolutePath(), e);\r
430           return false;\r
431         }\r
432       }\r
433 \r
434     } else {\r
435       log.warn("Client Error: cancelArchive called before archive("\r
436           + original.getAbsolutePath() + ") has been opened!");\r
437     }\r
438     closeAndReset(); // tidy up and release locks.\r
439     return true;\r
440   }\r
441 \r
442   /**\r
443    * only do this if you want to destroy the current file output stream\r
444    * \r
445    */\r
446   private void closeAndReset() {\r
447     if (rchive != null) {\r
448       rchive.unlockFile();\r
449       rchive = null;\r
450     }\r
451     if (original != null) {\r
452       if (odoc != null) {\r
453         odoc.close();\r
454         odoc = null;\r
455       }\r
456       if (archive != null)\r
457         archive.delete();\r
458       if (odoclock != null) {\r
459         odoclock.unlockFile();\r
460         odoclock = null;\r
461       }\r
462     }\r
463     removeBackup();\r
464     newarchive = null;\r
465     original = null;\r
466     entries = null;\r
467   }\r
468 \r
469   /**\r
470    * Tidies up and closes archive, removing any backups that were created. NOTE:\r
471    * It is up to the caller to delete the original archive backup obtained from\r
472    * backupFile() TODO: ensure all extant AppDataReference jar entries are\r
473    * transferred to new Jar TODO: provide convenient mechanism for generating\r
474    * new unique AppDataReferences and adding them to the document\r
475    */\r
476   public void closeArchive() throws IOException {\r
477     if (newarchive != null) {\r
478       newarchive.flush();\r
479       newarchive.closeEntry();\r
480       if (!isDocumentWritten())\r
481         log.warn("Premature closure of archive '" + archive.getAbsolutePath()\r
482             + "': No document has been written.");\r
483       newarchive.finish();// close(); // use newarchive.finish(); for a stream\r
484                           // IO\r
485       newarchive.flush();\r
486       //\r
487       updateOriginal();\r
488       closeAndReset();\r
489     } else {\r
490       log\r
491           .warn("Attempt to close archive that has not been opened for writing.");\r
492     }\r
493   }\r
494 \r
495   /**\r
496    * Opens and returns the applicationData output stream for the\r
497    * appdataReference string.\r
498    * \r
499    * @param appdataReference\r
500    * @return Output stream to write to\r
501    * @throws IOException\r
502    */\r
503   public AppDataOutputStream getAppDataStream(String appdataReference)\r
504       throws IOException {\r
505     if (newarchive == null)\r
506       throw new IOException("Attempt to write to closed VamsasArchive object.");\r
507     if (addValidEntry(appdataReference)) {\r
508       return new AppDataOutputStream(newarchive);\r
509     }\r
510     return null;\r
511   }\r
512 \r
513   /**\r
514    * \r
515    * @return JarEntry name for the vamsas XML stream in this archive\r
516    */\r
517   protected String getDocumentJarEntry() {\r
518     if (vamsasdocument)\r
519       return VamsasArchiveReader.VAMSASDOC;\r
520     return VamsasArchiveReader.VAMSASXML;\r
521   }\r
522 \r
523   /**\r
524    * Safely initializes the VAMSAS XML document Jar Entry.\r
525    * \r
526    * @return Writer to pass to the marshalling function.\r
527    * @throws IOException\r
528    *           if a document entry has already been written.\r
529    */\r
530   public PrintWriter getDocumentOutputStream() throws IOException {\r
531     if (newarchive == null)\r
532       openArchive();\r
533     if (!isDocumentWritten()) {\r
534       try {\r
535         if (addValidEntry(getDocumentJarEntry()))\r
536           return new PrintWriter(new java.io.OutputStreamWriter(newarchive,\r
537               "UTF-8"));\r
538       } catch (Exception e) {\r
539         log.warn("Problems opening XML document JarEntry stream", e);\r
540       }\r
541     } else {\r
542       throw new IOException("Vamsas Document output stream is already written.");\r
543     }\r
544     return null;\r
545   }\r
546 \r
547   /**\r
548    * Access original archive if it exists, pass the reader to the client Note:\r
549    * this is NOT thread safe and a call to closeArchive() will by necessity\r
550    * close and invalidate the VamsasArchiveReader object.\r
551    * \r
552    * @return null if no original archive exists.\r
553    */\r
554   public VamsasArchiveReader getOriginalArchiveReader() throws IOException {\r
555     if (!virginArchive) {\r
556       accessOriginal();\r
557       return odoc;\r
558     }\r
559     return null;\r
560   }\r
561 \r
562   /**\r
563    * returns original document's root vamsas elements.\r
564    * \r
565    * @return\r
566    * @throws IOException\r
567    * @throws org.exolab.castor.xml.MarshalException\r
568    * @throws org.exolab.castor.xml.ValidationException\r
569    */\r
570   public Vobject[] getOriginalRoots() throws IOException,\r
571       org.exolab.castor.xml.MarshalException,\r
572       org.exolab.castor.xml.ValidationException {\r
573     return VamsasArchive.getOriginalRoots(this);\r
574   }\r
575 \r
576   /**\r
577    * @return original document or a new empty document (with default provenance)\r
578    * @throws IOException\r
579    * @throws org.exolab.castor.xml.MarshalException\r
580    * @throws org.exolab.castor.xml.ValidationException\r
581    */\r
582   public VamsasDocument getVamsasDocument() throws IOException,\r
583       org.exolab.castor.xml.MarshalException,\r
584       org.exolab.castor.xml.ValidationException {\r
585     return getVamsasDocument("org.vamsas.simpleclient.VamsasArchive",\r
586         "Created new empty document", null);\r
587   }\r
588 \r
589   /**\r
590    * Return the original document or a new empty document with initial\r
591    * provenance entry.\r
592    * \r
593    * @param provenance_user\r
594    *          (null sets user to be the class name)\r
595    * @param provenance_action\r
596    *          (null sets action to be 'created new document')\r
597    * @param version\r
598    *          (null means use latest version)\r
599    * @return (original document or a new vamsas document with supplied\r
600    *         provenance and version info)\r
601    * @throws IOException\r
602    * @throws org.exolab.castor.xml.MarshalException\r
603    * @throws org.exolab.castor.xml.ValidationException\r
604    */\r
605   public VamsasDocument getVamsasDocument(String provenance_user,\r
606       String provenance_action, String version) throws IOException,\r
607       org.exolab.castor.xml.MarshalException,\r
608       org.exolab.castor.xml.ValidationException {\r
609     if (_doc != null)\r
610       return _doc;\r
611     _doc = getOriginalVamsasDocument(this, getVorba());\r
612     if (_doc != null)\r
613       return _doc;\r
614     // validate parameters\r
615     if (provenance_user == null)\r
616       provenance_user = "org.vamsas.simpleclient.VamsasArchive";\r
617     if (provenance_action == null)\r
618       provenance_action = "Created new empty document";\r
619     if (version == null)\r
620       version = VersionEntries.latestVersion();\r
621     // Create a new document and return it\r
622     _doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS() },\r
623         ProvenanceStuff.newProvenance(provenance_user, provenance_action),\r
624         version);\r
625     return _doc;\r
626   }\r
627 \r
628   /**\r
629    * @return Returns the current VorbaIdFactory for the archive.\r
630    */\r
631   public VorbaIdFactory getVorba() {\r
632     if (vorba == null)\r
633       vorba = new SimpleDocument("simpleclient.VamsasArchive");\r
634     return vorba.getVorba();\r
635   }\r
636 \r
637   /**\r
638    * @return true if Vamsas Document has been written to archive\r
639    */\r
640   protected boolean isDocumentWritten() {\r
641     if (newarchive == null)\r
642       log.warn("isDocumentWritten() called for unopened archive.");\r
643     if (entries != null) {\r
644       if (entries.containsKey(getDocumentJarEntry()))\r
645         return true;\r
646     }\r
647     return false;\r
648   }\r
649 \r
650   private void makeBackup() {\r
651     if (!virginArchive) {\r
652       if (originalBackup == null && original != null && original.exists()) {\r
653         try {\r
654           accessOriginal();\r
655           originalBackup = odoclock.backupSessionFile(null, original.getName(),\r
656               ".bak", original.getParentFile());\r
657         } catch (IOException e) {\r
658           log.warn("Problem whilst making a backup of original archive.", e);\r
659         }\r
660       }\r
661     }\r
662   }\r
663 \r
664   /**\r
665    * opens the new archive ready for writing. If the new archive is replacing an\r
666    * existing one, then the existing archive will be locked, and the new archive\r
667    * written to a temporary file. The new archive will be put in place once\r
668    * close() is called.\r
669    * \r
670    * @param doclock\r
671    *          LATER - pass existing lock on document, if it exists.... no need\r
672    *          yet?\r
673    * @throws IOException\r
674    */\r
675   private void openArchive() throws IOException {\r
676 \r
677     if (newarchive != null) {\r
678       log.warn("openArchive() called multiple times.");\r
679       throw new IOException("Vamsas Archive '" + archive.getAbsolutePath()\r
680           + "' is already open.");\r
681     }\r
682     if (archive == null && (virginArchive || original == null)) {\r
683       log.warn("openArchive called on uninitialised VamsasArchive object.");\r
684       throw new IOException(\r
685           "Badly initialised VamsasArchive object - no archive file specified.");\r
686     }\r
687     if (!virginArchive) {\r
688       // lock the original\r
689       accessOriginal();\r
690       // make a temporary file to write to\r
691       archive = File.createTempFile(original.getName(), ".new", original\r
692           .getParentFile());\r
693     } else {\r
694       if (archive.exists())\r
695         log\r
696             .warn("New archive file name already in use! Possible lock failure imminent?");\r
697     }\r
698 \r
699     if (rchive == null)\r
700       rchive = new SessionFile(archive);\r
701     if (!rchive.lockFile())\r
702       throw new IOException("Failed to get lock on file " + archive);\r
703     // LATER: locked IO stream based access.\r
704     // Manifest newmanifest = new Manifest();\r
705     newarchive = new org.apache.tools.zip.ZipOutputStream(rchive.fileLock\r
706         .getBufferedOutputStream(true));// , newmanifest);\r
707     // newarchive = new JarOutputStream(new BufferedOutputStream(new\r
708     // java.io.FileOutputStream(archive)));\r
709     entries = new Hashtable();\r
710   }\r
711 \r
712   public void putVamsasDocument(VamsasDocument doc) throws IOException,\r
713       org.exolab.castor.xml.MarshalException,\r
714       org.exolab.castor.xml.ValidationException {\r
715     putVamsasDocument(doc, getVorba());\r
716   }\r
717 \r
718   /**\r
719    * \r
720    * @param doc\r
721    * @param vorba\r
722    * @return (vorbaId string, Vobjhash) pairs for last hash of each object in\r
723    *         document\r
724    * @throws IOException\r
725    * @throws org.exolab.castor.xml.MarshalException\r
726    * @throws org.exolab.castor.xml.ValidationException\r
727    */\r
728   public void putVamsasDocument(VamsasDocument doc, VorbaIdFactory vorba)\r
729       throws IOException, org.exolab.castor.xml.MarshalException,\r
730       org.exolab.castor.xml.ValidationException {\r
731     if (vamsasdocument)\r
732       doc.setVersion(VersionEntries.latestVersion()); // LATER: ensure this does\r
733                                                       // the correct thing.\r
734     VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);\r
735   }\r
736 \r
737   /**\r
738    * recovers the original file's contents from the (temporary) backup.\r
739    * \r
740    * @throws Exception\r
741    *           if any SessionFile or file removal operations fail.\r
742    */\r
743   private void recoverBackup() throws Exception {\r
744     if (originalBackup != null) {\r
745       // backup has been made.\r
746       // revert from backup and delete it (changing backup filename)\r
747       if (rchive == null) {\r
748         rchive = new SessionFile(original);\r
749       }\r
750       SessionFile bckup = new SessionFile(originalBackup);\r
751 \r
752       rchive.updateFrom(null, bckup); // recover from backup file.\r
753       bckup.unlockFile();\r
754       bckup = null;\r
755       removeBackup();\r
756     }\r
757   }\r
758 \r
759   /**\r
760    * forget about any backup that was made - removing it first if it was only\r
761    * temporary.\r
762    */\r
763   private void removeBackup() {\r
764     if (originalBackup != null) {\r
765       log.debug("Removing backup in " + originalBackup.getAbsolutePath());\r
766       if (!donotdeletebackup)\r
767         if (!originalBackup.delete())\r
768           log.info("VamsasArchive couldn't remove temporary backup "\r
769               + originalBackup.getAbsolutePath());\r
770       originalBackup = null;\r
771     }\r
772   }\r
773 \r
774   /**\r
775    * @param vorba\r
776    *          the VorbaIdFactory to use for accessing vamsas objects.\r
777    */\r
778   public void setVorba(VorbaIdFactory Vorba) {\r
779     if (Vorba != null) {\r
780       if (vorba == null)\r
781         vorba = new SimpleDocument(Vorba);\r
782       else\r
783         vorba.setVorba(Vorba);\r
784     } else\r
785       getVorba();\r
786   }\r
787 \r
788   /**\r
789    * Convenience method to copy over the referred entry from the backup to the\r
790    * new version. Warning messages are raised if no backup exists or the entry\r
791    * doesn't exist in the backed-up original. Duplicate writes return true - but\r
792    * a warning message will also be raised.\r
793    * \r
794    * @param AppDataReference\r
795    * @return true if AppDataReference now exists in the new document\r
796    * @throws IOException\r
797    */\r
798   public boolean transferAppDataEntry(String AppDataReference)\r
799       throws IOException {\r
800     return transferAppDataEntry(AppDataReference, AppDataReference);\r
801   }\r
802 \r
803   /**\r
804    * Validates the AppDataReference: not null and not already written to\r
805    * archive.\r
806    * \r
807    * @param AppDataReference\r
808    * @return true if valid. false if not\r
809    * @throws IOException\r
810    *           for really broken references!\r
811    */\r
812   protected boolean _validNewAppDataReference(String newAppDataReference)\r
813       throws IOException {\r
814     // LATER: Specify valid AppDataReference form in all VamsasArchive handlers\r
815     if (newAppDataReference == null)\r
816       throw new IOException("null newAppDataReference!");\r
817     if (entries.containsKey(newAppDataReference)) {\r
818       log.warn("Attempt to write '" + newAppDataReference\r
819           + "' twice! - IGNORED");\r
820       // LATER: fix me? warning message should raise an exception here.\r
821       return false;\r
822     }\r
823     return true;\r
824   }\r
825 \r
826   /**\r
827    * Transfers an AppDataReference from old to new vamsas archive, with a name\r
828    * change.\r
829    * \r
830    * @see transferAppDataEntry(String AppDataReference)\r
831    * @param AppDataReference\r
832    * @param NewAppDataReference\r
833    *          - AppDataReference in new Archive\r
834    * @return\r
835    * @throws IOException\r
836    */\r
837   public boolean transferAppDataEntry(String AppDataReference,\r
838       String NewAppDataReference) throws IOException {\r
839     if (original == null || !original.exists()) {\r
840       log.warn("No backup archive exists.");\r
841       return false;\r
842     }\r
843     if (AppDataReference == null)\r
844       throw new IOException("null AppDataReference!");\r
845 \r
846     if (!_validNewAppDataReference(NewAppDataReference))\r
847       return false;\r
848 \r
849     accessOriginal();\r
850 \r
851     java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);\r
852 \r
853     if (adstream == null) {\r
854       log.warn("AppDataReference '" + AppDataReference\r
855           + "' doesn't exist in backup archive.");\r
856       return false;\r
857     }\r
858 \r
859     java.io.OutputStream adout = getAppDataStream(NewAppDataReference);\r
860     // copy over the bytes\r
861     int written = -1;\r
862     long count = 0;\r
863     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a\r
864                                                 // sensible buffer\r
865     do {\r
866       if ((written = adstream.read(buffer)) > -1) {\r
867         adout.write(buffer, 0, written);\r
868         log.debug("Transferring " + written + ".");\r
869         count += written;\r
870       }\r
871     } while (written > -1);\r
872     log.debug("Sucessfully transferred AppData for '" + AppDataReference\r
873         + "' as '" + NewAppDataReference + "' (" + count + " bytes)");\r
874     return true;\r
875   }\r
876 \r
877   /**\r
878    * write data from a stream into an appData reference.\r
879    * \r
880    * @param AppDataReference\r
881    *          - New AppDataReference not already written to archive\r
882    * @param adstream\r
883    *          Source of data for appData reference - read until .read(buffer)\r
884    *          returns -1\r
885    * @return true on success.\r
886    * @throws IOException\r
887    *           for file IO or invalid AppDataReference string\r
888    */\r
889   public boolean writeAppdataFromStream(String AppDataReference,\r
890       java.io.InputStream adstream) throws IOException {\r
891     if (!_validNewAppDataReference(AppDataReference)) {\r
892       log.warn("Invalid AppDataReference passed to writeAppdataFromStream");\r
893       throw new IOException(\r
894           "Invalid AppDataReference! (null, or maybe non-unique)!");\r
895     }\r
896 \r
897     if (AppDataReference == null) {\r
898       log.warn("null appdata passed.");\r
899       throw new IOException("Null AppDataReference");\r
900     }\r
901 \r
902     java.io.OutputStream adout = getAppDataStream(AppDataReference);\r
903     // copy over the bytes\r
904     int written = -1;\r
905     long count = 0;\r
906     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a\r
907                                                 // sensible buffer\r
908     do {\r
909       if ((written = adstream.read(buffer)) > -1) {\r
910         adout.write(buffer, 0, written);\r
911         log.debug("Transferring " + written + ".");\r
912         count += written;\r
913       }\r
914     } while (written > -1);\r
915     return true;\r
916   }\r
917 \r
918   /**\r
919    * transfers any AppDataReferences existing in the old document that haven't\r
920    * already been transferred to the new one LATER: do the same for transfers\r
921    * requiring a namechange - more document dependent.\r
922    * \r
923    * @return true if data was transferred.\r
924    */\r
925   public boolean transferRemainingAppDatas() throws IOException {\r
926     boolean transfered = false;\r
927     if (original == null || !original.exists()) {\r
928       log.warn("No backup archive exists.");\r
929       return false;\r
930     }\r
931     accessOriginal();\r
932 \r
933     if (getVorba() != null) {\r
934       Vector originalRefs = null;\r
935       try {\r
936         originalRefs = vorba.getReferencedEntries(getVamsasDocument(),\r
937             getOriginalArchiveReader());\r
938       } catch (Exception e) {\r
939         log.warn("Problems accessing original document entries!", e);\r
940       }\r
941       if (originalRefs != null) {\r
942         Iterator ref = originalRefs.iterator();\r
943         while (ref.hasNext()) {\r
944           String oldentry = (String) ref.next();\r
945           if (oldentry != null && !entries.containsKey(oldentry)) {\r
946             log.debug("Transferring remaining entry '" + oldentry + "'");\r
947             transfered |= transferAppDataEntry(oldentry);\r
948           }\r
949         }\r
950       }\r
951     }\r
952     return transfered;\r
953   }\r
954 \r
955   /**\r
956    * called after archive is written to put file in its final place\r
957    */\r
958   private void updateOriginal() {\r
959     if (!virginArchive) {\r
960       // make sure original document really is backed up and then overwrite it.\r
961       if (odoc != null) {\r
962         // try to shut the odoc reader.\r
963         odoc.close();\r
964         odoc = null;\r
965       }\r
966       // Make a backup if it isn't done already\r
967       makeBackup();\r
968       try {\r
969         // copy new Archive data that was writen to a temporary file\r
970         odoclock.updateFrom(null, rchive);\r
971       } catch (IOException e) {\r
972         // LATER: decide if leaving nastily named backup files around is\r
973         // necessary.\r
974         File backupFile = backupFile();\r
975         if (backupFile != null)\r
976           log.error(\r
977               "Problem updating archive from temporary file! - backup left in '"\r
978                   + backupFile().getAbsolutePath() + "'", e);\r
979         else\r
980           log\r
981               .error(\r
982                   "Problems updating, and failed to even make a backup file. Ooops!",\r
983                   e);\r
984       }\r
985       // Tidy up if necessary.\r
986       removeBackup();\r
987     } else {\r
988 \r
989     }\r
990   }\r
991 }\r