debug as far as test marshalling - a vorbaIdFactory is now required.
[vamsas.git] / src / org / vamsas / client / simpleclient / VamsasArchive.java
1 package org.vamsas.client.simpleclient;
2
3 import java.io.BufferedInputStream;
4 import java.io.BufferedOutputStream;
5 import java.io.DataOutputStream;
6 import java.io.File;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.InputStreamReader;
10 import java.io.PrintWriter;
11 import java.util.Hashtable;
12 import java.util.jar.JarEntry;
13 import java.util.jar.JarOutputStream;
14
15 import org.apache.commons.logging.Log;
16 import org.apache.commons.logging.LogFactory;
17 import org.vamsas.client.object;
18 import org.vamsas.objects.core.ApplicationData;
19 import org.vamsas.objects.core.VAMSAS;
20 import org.vamsas.objects.core.VamsasDocument;
21 import org.vamsas.objects.utils.DocumentStuff;
22 import org.vamsas.objects.utils.ProvenanceStuff;
23 import org.vamsas.objects.utils.document.VersionEntries;
24
25 /**
26  * Class for high-level io and Jar manipulation involved in creating 
27  * or updating a vamsas archive (with backups).
28  * Writes to a temporary file and then swaps new file for backup.
29  * uses the sessionFile locking mechanism for safe I/O
30  * @author jimp
31  *
32  */
33 public class VamsasArchive {
34   private static Log log = LogFactory.getLog(VamsasArchive.class);
35   /**
36    * destination of new archive data (tempfile if virginarchive=true, original archive location otherwise)
37    */
38   java.io.File archive=null;
39   /**
40    * locked IO handler for new archive file
41    */
42   SessionFile rchive=null; 
43   /**
44    * original archive file to be updated (or null if virgin) where new data will finally reside
45    */
46   java.io.File original=null;
47   /**
48    * original archive IO handler
49    */
50   SessionFile odoclock = null;
51   /**
52    * Original archive reader class
53    */
54   VamsasArchiveReader odoc = null;
55   /**
56    * true if a real vamsas document is being written.
57    */
58   boolean vamsasdocument=true;
59   /**
60    * Output stream for archived data
61    */
62   JarOutputStream newarchive=null;
63   /**
64    * JarEntries written to archive
65    */
66   Hashtable entries = null;
67   /**
68    * true if we aren't just updating an archive
69    */
70   private boolean virginArchive=false;
71   /**
72    * Create a new vamsas archive
73    * File locks are made immediately to avoid contention
74    *  
75    * @param archive - file spec for new vamsas archive
76    * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive
77    * @throws IOException if call to accessOriginal failed for updates, or openArchive failed.
78    */
79   public VamsasArchive(File archive, boolean vamsasdocument) throws IOException {
80     super();
81     if (archive==null || (archive!=null && !archive.canWrite())) {
82       log.fatal("Invalid parameters for VamsasArchive constructor:"+((archive!=null) 
83           ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
84     }
85     this.vamsasdocument = vamsasdocument;
86     if (archive.exists()) {
87       this.original = archive;
88       this.archive = null;       // archive will be a temp file when the open method is called
89       virginArchive=false;
90       try {
91         this.accessOriginal();
92       } catch (IOException e)  {
93         throw new IOException("Lock failed for existing archive"+archive);
94       }
95     } else {
96       this.original = null;
97       this.archive = archive; // archive is written in place.
98       virginArchive = true;
99     }
100     this.openArchive();
101   }
102   /**
103    * name of backup of existing archive that has been updated/overwritten.
104    * onlu one backup will be made - and this is it.
105    */
106   File originalBackup = null;
107   
108   private void makeBackup() {
109     if (!virginArchive) {
110       if (originalBackup==null && original!=null && original.exists()) {
111         try {
112           accessOriginal();
113           originalBackup = odoclock.backupSessionFile(null, original.getName(), ".bak", original.getParentFile());
114         }
115         catch (IOException e) {
116           log.warn("Problem whilst making a backup of original archive.",e);
117         }
118       }
119     }
120   }
121
122   /**
123    * called after archive is written to put file in its final place
124    * TODO: FINISH ?? original should have sessionFile, and archive should also have sessionFile
125    */
126   private void updateOriginal() {
127     if (!virginArchive) {
128       // make sure original document really is backed up and then overwrite it.
129         if (odoc!=null) {
130           // try to shut the odoc reader.
131           odoc.close();
132           odoc = null;
133         }
134         // Make a backup if it isn't done already
135         if (originalBackup==null)
136           makeBackup();
137         try {
138           // copy new Archive data that was writen to a temporary file
139           odoclock.updateFrom(null, rchive);
140         }
141         catch (IOException e) {
142           log.error("Problem updating archive from temporary file! - backup in '"
143               +backupFile().getAbsolutePath()+"'",e);
144         }
145     } else {
146       // don't need to do anything.
147     }
148   }
149   /**
150    * called by app to get name of backup if it was made.
151    * @return null or a valid file object
152    */
153   public File backupFile() {
154     
155     if (!virginArchive) {
156       makeBackup();
157       return ((original!=null) ? originalBackup : null);
158     }
159     return null;
160   }
161   /**
162    * 
163    * @return JarEntry name for the vamsas XML stream in this archive
164    */
165   protected String getDocumentJarEntry() {
166     if (vamsasdocument)
167       return VamsasArchiveReader.VAMSASDOC;
168     return VamsasArchiveReader.VAMSASXML;
169   }
170   
171   /**
172    * @return true if Vamsas Document has been written to archive
173    */
174   protected boolean isDocumentWritten() {
175     if (newarchive==null)
176       log.warn("isDocumentWritten() called for unopened archive.");
177     if (entries!=null) {
178       if (entries.containsKey(getDocumentJarEntry()))
179         return true;
180     }
181     return false;
182   }
183   /**
184    * Add unique entry strings to internal JarEntries list.
185    * @param entry
186    * @return true if entry was unique and was added.
187    */
188   private boolean addEntry(String entry) {
189     if (entries!=null)
190       entries=new Hashtable();
191     if (entries.containsKey(entry))
192       return false;
193     entries.put(entry, new Integer(entries.size()));
194     return true;
195   }
196   /**
197    * adds named entry to newarchive or returns false.
198    * @param entry
199    * @return true if entry was unique and could be added
200    * @throws IOException if entry name was invalid or a new entry could not be made on newarchive
201    */
202   private boolean addValidEntry(String entry) throws IOException {
203     JarEntry je = new JarEntry(entry);
204     if (!addEntry(entry))
205       return false;
206     newarchive.putNextEntry(je);
207     return true;
208   }
209   
210   /**
211    * opens the new archive ready for writing. If the new archive is replacing an existing one, 
212    * then the existing archive will be locked, and the new archive written to a temporary file. 
213    * The new archive will be put in place once close() is called.
214    * @throws IOException
215    */
216   private void openArchive() throws IOException {
217     
218     if (newarchive!=null) {
219       log.warn("openArchive() called multiple times.");
220       throw new IOException("Vamsas Archive '"+archive.getAbsolutePath()+"' is already open.");
221     }
222     if (archive==null && (virginArchive || original==null)) {
223       log.warn("openArchive called on uninitialised VamsasArchive object.");
224       throw new IOException("Badly initialised VamsasArchive object - no archive file specified.");
225     }
226     if (!virginArchive) {
227       // lock the original
228       accessOriginal();
229       // make a temporary file to write to
230       archive = File.createTempFile(original.getName(), ".new",original.getParentFile());
231     } else {
232       if (archive.exists())
233         log.warn("New archive file name already in use! Possible lock failure imminent?");
234     }
235     
236     rchive = new SessionFile(archive);
237     rchive.lockFile();
238     newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
239     entries = new Hashtable();
240   }
241   /**
242    * Safely initializes the VAMSAS XML document Jar Entry. 
243    * @return Writer to pass to the marshalling function.
244    * @throws IOException if a document entry has already been written. 
245    */
246   public PrintWriter getDocumentOutputStream() throws IOException {
247     if (newarchive==null)
248       openArchive();
249     if (!isDocumentWritten()) {
250       try {
251         if (addValidEntry(getDocumentJarEntry())) 
252           return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
253       } catch (Exception e) {
254         log.warn("Problems opening XML document JarEntry stream",e);
255       }
256     } else {
257       throw new IOException("Vamsas Document output stream is already written.");
258     }
259     return null;
260   }
261   /**
262    * Opens and returns the applicationData output stream for the appdataReference string.
263    * @param appdataReference
264    * @return Output stream to write to
265    * @throws IOException
266    */
267   public AppDataOutputStream getAppDataStream(String appdataReference) throws IOException {
268     if (newarchive==null)
269       openArchive();
270     if (addValidEntry(appdataReference)) {
271       return new AppDataOutputStream(newarchive);
272     }
273     return null;
274   }
275   
276   /**
277    * Stops any current write to archive, and reverts to the backup if it exists.
278    * All existing locks on the original will be released. All backup files are removed.
279    */
280   public boolean cancelArchive() {
281     if (newarchive!=null) {
282       try { 
283         newarchive.close();
284         
285       } catch (Exception e) {};
286       if (!virginArchive) {
287         // then there is something to recover.
288         if (originalBackup!=null) {
289           // backup has been made.
290           // revert from backup and delete it (changing backup filename)
291           if (rchive==null) {
292             rchive = new SessionFile(original);
293           }
294           SessionFile bckup = new SessionFile(originalBackup);
295           
296           try {
297             rchive.updateFrom(null, bckup); // recover from backup file.
298             bckup.unlockFile();
299             bckup=null;
300             originalBackup.delete();
301             originalBackup=null;
302           }
303           catch (Exception e) {
304             log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
305             return false;
306           }
307         }
308       }
309     } else {
310       log.info("cancelArchive called before archive("+original.getAbsolutePath()+") has been opened!");
311     }
312     closeAndReset(); // tidy up and release locks.
313     return true;
314   }
315   
316   /**
317    * only do this if you want to destroy the current file output stream
318    *
319    */
320   private void closeAndReset() {
321     if (rchive!=null) {
322       rchive.unlockFile();
323       rchive = null;
324     }
325     if (original!=null) {
326       if (odoc!=null) {
327         odoc.close();
328         odoc=null;
329       }
330       if (archive!=null)
331         archive.delete();
332       if (odoclock!=null) {
333         odoclock.unlockFile();
334         odoclock = null;
335       }
336     }
337     newarchive=null;
338     original=null;
339     entries=null;
340   }
341   
342   private final int _TRANSFER_BUFFER=4096*4;
343   /**
344    * open original archive file for exclusive (locked) reading.
345    * @throws IOException
346    */
347   private void accessOriginal() throws IOException {
348     if (original!=null && original.exists()) {
349       if (odoclock==null) 
350         odoclock = new SessionFile(original);
351       odoclock.lockFile();
352       if (odoc == null) 
353         odoc = new VamsasArchiveReader(original);
354     }
355   }
356   
357   /**
358    * Convenience method to copy over the referred entry from the backup to the new version.
359    * Warning messages are raised if no backup exists or the 
360    * entry doesn't exist in the backed-up original.
361    * Duplicate writes return true - but a warning message will also be raised.
362    * @param AppDataReference
363    * @return true if AppDataReference now exists in the new document
364    * @throws IOException
365    */
366   public boolean transferAppDataEntry(String AppDataReference) throws IOException {
367     return transferAppDataEntry(AppDataReference, AppDataReference);
368   }
369   /**
370    * Transfers an AppDataReference from old to new vamsas archive, with a name change.
371    * @see transferAppDataEntry(String AppDataReference)
372    * @param AppDataReference
373    * @param NewAppDataReference - AppDataReference in new Archive
374    * @return
375    * @throws IOException
376    */
377   public boolean transferAppDataEntry(String AppDataReference, String NewAppDataReference) throws IOException {
378     // TODO: Specify valid AppDataReference form in all VamsasArchive handlers
379     if (AppDataReference==null)
380       throw new IOException("null AppDataReference!");
381     if (original==null || !original.exists()) {
382       log.warn("No backup archive exists.");
383       return false;
384     }
385     if (entries.containsKey(NewAppDataReference)) {
386       log.warn("Attempt to write '"+NewAppDataReference+"' twice! - IGNORED");
387       return true;
388     }
389     
390     accessOriginal();
391     
392     java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
393     
394     if (adstream==null) {
395       log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
396       return false;
397     }
398     
399     java.io.OutputStream adout = getAppDataStream(NewAppDataReference);
400     // copy over the bytes
401     int written=-1;
402     long count=0;
403     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
404     do {
405       if ((written = adstream.read(buffer))>-1) {
406         adout.write(buffer, 0, written);
407         log.debug("Transferring "+written+".");
408         count+=written;
409       }
410     } while (written>-1);
411     log.debug("Sucessfully transferred AppData for '"
412         +AppDataReference+"' as '"+NewAppDataReference+"' ("+count+" bytes)");
413     return true;
414   }
415   /**
416    * Tidies up and closes archive, removing any backups that were created.
417    * NOTE: It is up to the caller to delete the original archive backup obtained from backupFile()
418    * TODO: ensure all extant AppDataReference jar entries are transferred to new Jar
419    * TODO: provide convenient mechanism for generating new unique AppDataReferences and adding them to the document
420    */
421   public void closeArchive() throws IOException {
422     if (newarchive!=null) {
423       newarchive.closeEntry();
424       if (!isDocumentWritten())
425         log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
426       newarchive.close();
427       updateOriginal();
428       closeAndReset();
429     } else {
430       log.warn("Attempt to close archive that has not been opened for writing.");
431     }
432   }
433   /**
434    * Access original archive if it exists, pass the reader to the client
435    * Note: this is NOT thread safe and a call to closeArchive() will by necessity 
436    * close and invalidate the VamsasArchiveReader object.
437    * @return null if no original archive exists.
438    */
439   public VamsasArchiveReader getOriginalArchiveReader() throws IOException {
440     if (!virginArchive) {
441       accessOriginal();
442       return odoc;
443     }
444     return null;
445   }
446   /**
447    * returns original document's root vamsas elements.
448    * @return
449    * @throws IOException
450    * @throws org.exolab.castor.xml.MarshalException
451    * @throws org.exolab.castor.xml.ValidationException
452    */
453   public object[] getOriginalRoots() throws IOException, 
454   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException  {
455     return VamsasArchive.getOriginalRoots(this);
456   }
457   /**
458    * Access original document if it exists, and get VAMSAS root objects.
459    * @return vector of vamsas roots from original document
460    * @throws IOException
461    */
462   public static object[] getOriginalRoots(VamsasArchive ths) throws IOException, 
463   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
464     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
465     if (oReader!=null) {
466       
467       if (oReader.isValid()) {
468         InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
469         VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
470         if (doc!=null) 
471           return doc.getVAMSAS();
472         // TODO ensure embedded appDatas are garbage collected to save memory
473       } else {
474         InputStream vxmlis = oReader.getVamsasXmlStream();
475         if (vxmlis!=null) { // Might be an old vamsas file.
476           BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
477           InputStreamReader vxml = new InputStreamReader(ixml);
478           VAMSAS root[] = new VAMSAS[1];
479           root[0] = VAMSAS.unmarshal(vxml);
480           if (root[0]!=null)
481             return root;
482         }
483       }
484     }
485     return null;
486   }
487   /**
488    * Access and return current vamsas Document, if it exists, or create a new one 
489    * (without affecting VamsasArchive object state - so is NOT THREAD SAFE)
490    * TODO: possibly modify internal state to lock low-level files 
491    * (like the IClientDocument interface instance constructer would do) 
492    * @see org.vamsas.simpleclient.VamsasArchive.getOriginalVamsasDocument for additional caveats
493    * 
494    * @return
495    * @throws IOException
496    * @throws org.exolab.castor.xml.MarshalException
497    * @throws org.exolab.castor.xml.ValidationException
498    */
499   public VamsasDocument getVamsasDocument() throws IOException, 
500   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
501     VamsasDocument doc = getOriginalVamsasDocument(this);
502     if (doc!=null)
503       return doc;
504     // Create a new document and return it
505     doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS()}, 
506         ProvenanceStuff.newProvenance("org.vamsas.simpleclient.VamsasArchive", "Created new empty document")
507         , VersionEntries.latestVersion());
508     return doc;
509   }
510   /**
511    * Access the original vamsas document for a VamsasArchive class, and return it.
512    * Users of the VamsasArchive class should use the getVamsasDocument method to retrieve
513    * the current document - only use this one if you want the 'backup' version.
514    * TODO: catch OutOfMemoryError - they are likely to occur here.
515    * NOTE: vamsas.xml datastreams are constructed as 'ALPHA_VERSION' vamsas documents.
516    * @param ths
517    * @return null if no document exists.
518    * @throws IOException
519    * @throws org.exolab.castor.xml.MarshalException
520    * @throws org.exolab.castor.xml.ValidationException
521    */
522   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths) throws IOException, 
523   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
524     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
525     if (oReader!=null) {
526       if (oReader.isValid()) {
527         InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
528         VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
529         if (doc!=null) 
530           return doc;
531       } else {
532         // deprecated data handler
533         InputStream vxmlis = oReader.getVamsasXmlStream();
534         if (vxmlis!=null) { // Might be an old vamsas file.
535           BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
536           InputStreamReader vxml = new InputStreamReader(ixml);
537           VAMSAS root[] = new VAMSAS[1];
538           root[0] = VAMSAS.unmarshal(vxml);
539           if (root[0]!=null) {
540             log.debug("Reading old format vamsas.xml into a dummy document.");
541             VamsasDocument doc = DocumentStuff.newVamsasDocument(root, 
542                 ProvenanceStuff.newProvenance(
543                     "org.vamsas.simpleclient.VamsasArchive", // TODO: VAMSAS: decide on 'system' operations provenance form
544                     "Vamsas Document constructed from vamsas.xml in <file>" 
545                     // TODO: VAMSAS: decide on machine readable info embedding in provenance should be done
546                     +ths.original+"</file>"), VersionEntries.ALPHA_VERSION);
547             root[0]=null;
548             root=null;
549             return doc;
550           }
551         }
552       }
553     } // otherwise - there was no valid original document to read.
554     return null;    
555   }
556 }