added overwrite flag to VamsasArchive writer class (avoids loss of locks on file...
[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.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
19 import org.apache.commons.logging.Log;
20 import org.apache.commons.logging.LogFactory;
21 import org.vamsas.client.ClientHandle;
22 import org.vamsas.client.IVorbaIdFactory;
23 import org.vamsas.client.SessionHandle;
24 import org.vamsas.client.UserHandle;
25 import org.vamsas.client.VorbaIdFactory;
26 import org.vamsas.client.VorbaXmlBinder;
27 import org.vamsas.client.object;
28 import org.vamsas.objects.core.ApplicationData;
29 import org.vamsas.objects.core.VAMSAS;
30 import org.vamsas.objects.core.VamsasDocument;
31 import org.vamsas.objects.utils.AppDataReference;
32 import org.vamsas.objects.utils.DocumentStuff;
33 import org.vamsas.objects.utils.ProvenanceStuff;
34 import org.vamsas.objects.utils.document.VersionEntries;
35
36 /**
37  * Class for high-level io and Jar manipulation involved in creating 
38  * or updating a vamsas archive (with backups).
39  * Writes to a temporary file and then swaps new file for backup.
40  * uses the sessionFile locking mechanism for safe I/O
41  * @author jimp
42  *
43  */
44 public class VamsasArchive {
45   private static Log log = LogFactory.getLog(VamsasArchive.class);
46   /**
47    * Access original document if it exists, and get VAMSAS root objects.
48    * @return vector of vamsas roots from original document
49    * @throws IOException
50    */
51   public static object[] getOriginalRoots(VamsasArchive ths) throws IOException, 
52   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
53     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
54     if (oReader!=null) {
55       
56       if (oReader.isValid()) {
57         InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
58         VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
59         if (doc!=null) 
60           return doc.getVAMSAS();
61         // TODO ensure embedded appDatas are garbage collected to save memory
62       } else {
63         InputStream vxmlis = oReader.getVamsasXmlStream();
64         if (vxmlis!=null) { // Might be an old vamsas file.
65           BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
66           InputStreamReader vxml = new InputStreamReader(ixml);
67           VAMSAS root[] = new VAMSAS[1];
68           root[0] = VAMSAS.unmarshal(vxml);
69           if (root[0]!=null)
70             return root;
71         }
72       }
73     }
74     return null;
75   }
76   /**
77    * Access the original vamsas document for a VamsasArchive class, and return it.
78    * Users of the VamsasArchive class should use the getVamsasDocument method to retrieve
79    * the current document - only use this one if you want the 'backup' version.
80    * TODO: catch OutOfMemoryError - they are likely to occur here.
81    * NOTE: vamsas.xml datastreams are constructed as 'ALPHA_VERSION' vamsas documents.
82    * @param ths
83    * @return null if no document exists.
84    * @throws IOException
85    * @throws org.exolab.castor.xml.MarshalException
86    * @throws org.exolab.castor.xml.ValidationException
87    */
88   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths) throws IOException, 
89   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
90     return VamsasArchive.getOriginalVamsasDocument(ths, null);
91   } 
92   /**
93    * Uses VorbaXmlBinder to retrieve the VamsasDocument from the original archive referred to by ths
94    * @param ths
95    * @param vorba
96    * @return
97    * @throws IOException
98    * @throws org.exolab.castor.xml.MarshalException
99    * @throws org.exolab.castor.xml.ValidationException
100    */
101   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths, VorbaIdFactory vorba) throws IOException, 
102   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
103     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
104     if (oReader!=null) {
105       ths.setVorba(vorba);
106       return ths.vorba.getVamsasDocument(oReader);
107     }
108     // otherwise - there was no valid original document to read.
109     return null;    
110   }
111   /**
112    * destination of new archive data (tempfile if virginarchive=true, original archive location otherwise)
113    */
114   java.io.File archive=null;
115   /**
116    * locked IO handler for new archive file
117    */
118   SessionFile rchive=null;
119   /**
120    * original archive file to be updated (or null if virgin) where new data will finally reside
121    */
122   java.io.File original=null;
123   /**
124    * original archive IO handler
125    */
126   SessionFile odoclock = null;
127   Lock destinationLock = null;
128   /**
129    * Original archive reader class
130    */
131   VamsasArchiveReader odoc = null;
132   /**
133    * true if a real vamsas document is being written.
134    */
135   boolean vamsasdocument=true;
136   /**
137    * Output stream for archived data
138    */
139   JarOutputStream newarchive=null;
140   /**
141    * JarEntries written to archive
142    */
143   Hashtable entries = null;
144   
145   /**
146    * true if we aren't just updating an archive
147    */
148   private boolean virginArchive=false;
149   
150   /**
151    * name of backup of existing archive that has been updated/overwritten.
152    * onlu one backup will be made - and this is it.
153    */
154   File originalBackup = null;
155   
156   boolean donotdeletebackup=false;
157   private final int _TRANSFER_BUFFER=4096*4;
158   protected SimpleDocument vorba = null;
159   /**
160    * Access and return current vamsas Document, if it exists, or create a new one 
161    * (without affecting VamsasArchive object state - so is NOT THREAD SAFE)
162    * TODO: possibly modify internal state to lock low-level files 
163    * (like the IClientDocument interface instance constructer would do) 
164    * @see org.vamsas.simpleclient.VamsasArchive.getOriginalVamsasDocument for additional caveats
165    * 
166    * @return
167    * @throws IOException
168    * @throws org.exolab.castor.xml.MarshalException
169    * @throws org.exolab.castor.xml.ValidationException
170    */
171   private VamsasDocument _doc=null;
172   
173   /**
174    * Create a new vamsas archive
175    * File locks are made immediately to avoid contention
176    *  
177    * @param archive - file spec for new vamsas archive
178    * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive
179    * @throws IOException if call to accessOriginal failed for updates, or openArchive failed.
180    */
181   public VamsasArchive(File archive, boolean vamsasdocument) throws IOException {
182     this(archive, false, vamsasdocument, null);
183   }
184   public VamsasArchive(File archive, boolean vamsasdocument, boolean overwrite) throws IOException {
185     this(archive, overwrite, vamsasdocument, null);
186   }
187   /**
188    * 
189    * @param archive file to write
190    * @param overwrite true if original contents should be deleted
191    * @param vamsasdocument true if a proper VamsasDocument archive is to be written.
192    * @param extantLock SessionFile object holding a lock for the <object>archive</object> 
193    * @throws IOException
194    */
195   public VamsasArchive(File archive, boolean overwrite, boolean vamsasdocument, SessionFile extantLock) throws IOException {
196     super();
197     if (archive==null || (archive!=null && !(archive.getParentFile().canWrite() && (!archive.exists() || archive.canWrite())))) {
198       log.fatal("Expect Badness! -- Invalid parameters for VamsasArchive constructor:"+((archive!=null) 
199           ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
200       return;
201     }
202
203     this.vamsasdocument = vamsasdocument;
204     if (archive.exists() && !overwrite) {
205       this.original = archive;
206       if (extantLock!=null)
207         this.odoclock = extantLock;
208       else 
209         this.odoclock = new SessionFile(archive); 
210       odoclock.lockFile(); // lock the file *immediatly*
211       this.archive = null;       // archive will be a temp file when the open method is called
212       virginArchive=false;
213       try {
214         this.accessOriginal();
215       } catch (IOException e)  {
216         throw new IOException("Lock failed for existing archive"+archive);
217       }
218     } else {
219       this.original = null;
220       this.archive = archive; // archive is written in place.
221       if (extantLock!=null)
222         rchive=extantLock;
223       else
224         rchive = new SessionFile(archive);
225       rchive.lockFile();
226       rchive.fileLock.rafile.setLength(0); // empty the archive.
227       virginArchive = true;
228     }
229     this.openArchive(); // open archive
230   }
231   /**
232    * open original archive file for exclusive (locked) reading.
233    * @throws IOException
234    */
235   private void accessOriginal() throws IOException {
236     if (original!=null && original.exists()) {
237       if (odoclock==null) 
238         odoclock = new SessionFile(original);
239       odoclock.lockFile();
240       if (odoc == null) 
241         odoc = new VamsasArchiveReader(original);
242     }
243   }
244   
245   /**
246    * Add unique entry strings to internal JarEntries list.
247    * @param entry
248    * @return true if entry was unique and was added.
249    */
250   private boolean addEntry(String entry) {
251     if (entries!=null)
252       entries=new Hashtable();
253     if (entries.containsKey(entry))
254       return false;
255     entries.put(entry, new Integer(entries.size()));
256     return true;
257   }
258   /**
259    * adds named entry to newarchive or returns false.
260    * @param entry
261    * @return true if entry was unique and could be added
262    * @throws IOException if entry name was invalid or a new entry could not be made on newarchive
263    */
264   private boolean addValidEntry(String entry) throws IOException {
265     JarEntry je = new JarEntry(entry);
266     if (!addEntry(entry))
267       return false;
268     newarchive.putNextEntry(je);
269     return true;
270   }
271   /**
272    * called by app to get name of backup if it was made.
273    * If this is called, the caller app *must* delete the backup themselves.
274    * @return null or a valid file object
275    */
276   public File backupFile() {
277     
278     if (!virginArchive) {
279       makeBackup();
280       donotdeletebackup=false; // external reference has been made.
281       return ((original!=null) ? originalBackup : null);
282     }
283     return null;
284   }
285   
286   /**
287    * Stops any current write to archive, and reverts to the backup if it exists.
288    * All existing locks on the original will be released. All backup files are removed.
289    */
290   public boolean cancelArchive() {
291     if (newarchive!=null) {
292       try { 
293         newarchive.close();
294         
295       } catch (Exception e) {
296         log.debug("Whilst closing newarchive",e);
297       };
298       if (!virginArchive) {
299         // then there is something to recover.
300         try {
301           recoverBackup();
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.warn("Client Error: 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     removeBackup();
338     newarchive=null;
339     original=null;
340     entries=null;
341   }
342   /**
343    * Tidies up and closes archive, removing any backups that were created.
344    * NOTE: It is up to the caller to delete the original archive backup obtained from backupFile()
345    * TODO: ensure all extant AppDataReference jar entries are transferred to new Jar
346    * TODO: provide convenient mechanism for generating new unique AppDataReferences and adding them to the document
347    */
348   public void closeArchive() throws IOException {
349     if (newarchive!=null) {
350       newarchive.closeEntry();
351       if (!isDocumentWritten())
352         log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
353       newarchive.close();
354       updateOriginal();
355       closeAndReset();
356     } else {
357       log.warn("Attempt to close archive that has not been opened for writing.");
358     }
359   }
360   /**
361    * Opens and returns the applicationData output stream for the appdataReference string.
362    * @param appdataReference
363    * @return Output stream to write to
364    * @throws IOException
365    */
366   public AppDataOutputStream getAppDataStream(String appdataReference) throws IOException {
367     if (newarchive==null)
368       throw new IOException("Attempt to write to closed VamsasArchive object.");
369     if (addValidEntry(appdataReference)) {
370       return new AppDataOutputStream(newarchive);
371     }
372     return null;
373   }
374   
375   /**
376    * 
377    * @return JarEntry name for the vamsas XML stream in this archive
378    */
379   protected String getDocumentJarEntry() {
380     if (vamsasdocument)
381       return VamsasArchiveReader.VAMSASDOC;
382     return VamsasArchiveReader.VAMSASXML;
383   }
384   /**
385    * Safely initializes the VAMSAS XML document Jar Entry. 
386    * @return Writer to pass to the marshalling function.
387    * @throws IOException if a document entry has already been written. 
388    */
389   public PrintWriter getDocumentOutputStream() throws IOException {
390     if (newarchive==null)
391       openArchive();
392     if (!isDocumentWritten()) {
393       try {
394         if (addValidEntry(getDocumentJarEntry())) 
395           return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
396       } catch (Exception e) {
397         log.warn("Problems opening XML document JarEntry stream",e);
398       }
399     } else {
400       throw new IOException("Vamsas Document output stream is already written.");
401     }
402     return null;
403   }
404   
405   /**
406    * Access original archive if it exists, pass the reader to the client
407    * Note: this is NOT thread safe and a call to closeArchive() will by necessity 
408    * close and invalidate the VamsasArchiveReader object.
409    * @return null if no original archive exists.
410    */
411   public VamsasArchiveReader getOriginalArchiveReader() throws IOException {
412     if (!virginArchive) {
413       accessOriginal();
414       return odoc;
415     }
416     return null;
417   }
418   /**
419    * returns original document's root vamsas elements.
420    * @return
421    * @throws IOException
422    * @throws org.exolab.castor.xml.MarshalException
423    * @throws org.exolab.castor.xml.ValidationException
424    */
425   public object[] getOriginalRoots() throws IOException, 
426   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException  {
427     return VamsasArchive.getOriginalRoots(this);
428   }
429   public VamsasDocument getVamsasDocument() throws IOException, 
430   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
431     if (_doc!=null)
432       return _doc;
433     _doc = getOriginalVamsasDocument(this, getVorba());
434     if (_doc!=null)
435       return _doc;
436     // Create a new document and return it
437     _doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS()}, 
438         ProvenanceStuff.newProvenance("org.vamsas.simpleclient.VamsasArchive", "Created new empty document")
439         , VersionEntries.latestVersion());
440     return _doc;
441   }
442   /**
443    * @return Returns the current VorbaIdFactory for the archive.
444    */
445   public VorbaIdFactory getVorba() {
446     if (vorba==null)
447       vorba = new SimpleDocument("simpleclient.VamsasArchive");
448     return vorba.getVorba();
449   }
450   /**
451    * @return true if Vamsas Document has been written to archive
452    */
453   protected boolean isDocumentWritten() {
454     if (newarchive==null)
455       log.warn("isDocumentWritten() called for unopened archive.");
456     if (entries!=null) {
457       if (entries.containsKey(getDocumentJarEntry()))
458         return true;
459     }
460     return false;
461   }
462   private void makeBackup() {
463     if (!virginArchive) {
464       if (originalBackup==null && original!=null && original.exists()) {
465         try {
466           accessOriginal();
467           originalBackup = odoclock.backupSessionFile(null, original.getName(), ".bak", original.getParentFile());
468         }
469         catch (IOException e) {
470           log.warn("Problem whilst making a backup of original archive.",e);
471         }
472       }
473     }
474   }
475   /**
476    * opens the new archive ready for writing. If the new archive is replacing an existing one, 
477    * then the existing archive will be locked, and the new archive written to a temporary file. 
478    * The new archive will be put in place once close() is called.
479    * @param doclock TODO
480    * @throws IOException
481    */
482   private void openArchive() throws IOException {
483     
484     if (newarchive!=null) {
485       log.warn("openArchive() called multiple times.");
486       throw new IOException("Vamsas Archive '"+archive.getAbsolutePath()+"' is already open.");
487     }
488     if (archive==null && (virginArchive || original==null)) {
489       log.warn("openArchive called on uninitialised VamsasArchive object.");
490       throw new IOException("Badly initialised VamsasArchive object - no archive file specified.");
491     }
492     if (!virginArchive) {
493       // lock the original
494       accessOriginal();
495       // make a temporary file to write to
496       archive = File.createTempFile(original.getName(), ".new",original.getParentFile());
497     } else {
498       if (archive.exists())
499         log.warn("New archive file name already in use! Possible lock failure imminent?");
500     }
501     
502     if (rchive==null)
503       rchive = new SessionFile(archive);
504     if (!rchive.lockFile()) 
505       throw new IOException("Failed to get lock on file "+archive);
506     newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
507     entries = new Hashtable();
508   }
509   public void putVamsasDocument(VamsasDocument doc) throws IOException, 
510   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
511     putVamsasDocument(doc, getVorba());
512   }
513   
514   public void putVamsasDocument(VamsasDocument doc, VorbaIdFactory vorba) throws IOException, 
515   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
516     VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);
517   }
518   
519   /**
520    * recovers the original file's contents from the (temporary) backup. 
521    * @throws Exception if any SessionFile or file removal operations fail.
522    */
523   private void recoverBackup() throws Exception {
524     if (originalBackup!=null) {
525       // backup has been made.
526       // revert from backup and delete it (changing backup filename)
527       if (rchive==null) {
528         rchive = new SessionFile(original);
529       }
530       SessionFile bckup = new SessionFile(originalBackup);
531       
532       rchive.updateFrom(null, bckup); // recover from backup file.
533       bckup.unlockFile();
534       bckup=null;
535       removeBackup();
536     }
537   }
538   
539   /**
540    * forget about any backup that was made - removing it first if it was only temporary.
541    */
542   private void removeBackup() {
543     if (originalBackup!=null) {
544       log.debug("Removing backup in "+originalBackup.getAbsolutePath());
545       if (!donotdeletebackup)
546         if (!originalBackup.delete())
547           log.info("VamsasArchive couldn't remove temporary backup "+originalBackup.getAbsolutePath());
548       originalBackup=null;
549     }
550   } 
551   /**
552    * @param vorba the VorbaIdFactory to use for accessing vamsas objects.
553    */
554   public void setVorba(VorbaIdFactory Vorba) {
555     if (Vorba!=null) {
556       if (vorba==null)
557         vorba = new SimpleDocument(Vorba);
558       else
559         vorba.setVorba(Vorba);
560     } else
561       getVorba();
562   }
563   /**
564    * Convenience method to copy over the referred entry from the backup to the new version.
565    * Warning messages are raised if no backup exists or the 
566    * entry doesn't exist in the backed-up original.
567    * Duplicate writes return true - but a warning message will also be raised.
568    * @param AppDataReference
569    * @return true if AppDataReference now exists in the new document
570    * @throws IOException
571    */
572   public boolean transferAppDataEntry(String AppDataReference) throws IOException {
573     return transferAppDataEntry(AppDataReference, AppDataReference);
574   }
575   
576   /**
577    * Transfers an AppDataReference from old to new vamsas archive, with a name change.
578    * @see transferAppDataEntry(String AppDataReference)
579    * @param AppDataReference
580    * @param NewAppDataReference - AppDataReference in new Archive
581    * @return
582    * @throws IOException
583    */
584   public boolean transferAppDataEntry(String AppDataReference, String NewAppDataReference) throws IOException {
585     // TODO: Specify valid AppDataReference form in all VamsasArchive handlers
586     if (AppDataReference==null)
587       throw new IOException("null AppDataReference!");
588     if (original==null || !original.exists()) {
589       log.warn("No backup archive exists.");
590       return false;
591     }
592     if (entries.containsKey(NewAppDataReference)) {
593       log.warn("Attempt to write '"+NewAppDataReference+"' twice! - IGNORED");
594       return true;
595     }
596     
597     accessOriginal();
598     
599     java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
600     
601     if (adstream==null) {
602       log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
603       return false;
604     }
605     
606     java.io.OutputStream adout = getAppDataStream(NewAppDataReference);
607     // copy over the bytes
608     int written=-1;
609     long count=0;
610     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
611     do {
612       if ((written = adstream.read(buffer))>-1) {
613         adout.write(buffer, 0, written);
614         log.debug("Transferring "+written+".");
615         count+=written;
616       }
617     } while (written>-1);
618     log.debug("Sucessfully transferred AppData for '"
619         +AppDataReference+"' as '"+NewAppDataReference+"' ("+count+" bytes)");
620     return true;
621   }
622   
623   /**
624    * transfers any AppDataReferences existing in the old document 
625    * that haven't already been transferred to the new one
626    * TODO: LATER: do the same for transfers requiring a namechange - more document dependent.
627    *  @return true if data was transferred.
628    */
629   public boolean transferRemainingAppDatas() throws IOException {
630     boolean transfered=false;
631     if (original==null || !original.exists()) {
632       log.warn("No backup archive exists.");
633       return false;
634     }
635     accessOriginal();
636     
637     if (getVorba()!=null) {
638       Vector originalRefs=null;
639       try {
640         originalRefs = vorba.getReferencedEntries(getVamsasDocument(), getOriginalArchiveReader());
641       } catch (Exception e) {
642         log.warn("Problems accessing original document entries!",e);
643       }
644       if (originalRefs!=null) {
645         Iterator ref = originalRefs.iterator();
646         while (ref.hasNext()) {
647           String oldentry = (String) ref.next();
648           if (oldentry!=null && !entries.containsKey(oldentry)) {
649             log.debug("Transferring remaining entry '"+oldentry+"'");
650             transfered |= transferAppDataEntry(oldentry);
651           }
652         }
653       }
654     } 
655     return transfered;
656   }
657   /**
658    * called after archive is written to put file in its final place
659    */
660   private void updateOriginal() {
661     if (!virginArchive) {
662       // make sure original document really is backed up and then overwrite it.
663       if (odoc!=null) {
664         // try to shut the odoc reader.
665         odoc.close();
666         odoc = null;
667       }
668       // Make a backup if it isn't done already
669       makeBackup();
670       try {
671         // copy new Archive data that was writen to a temporary file
672         odoclock.updateFrom(null, rchive);
673       }
674       catch (IOException e) {
675         // TODO: LATER: decide if leaving nastily named backup files around is necessary. 
676         log.error("Problem updating archive from temporary file! - backup left in '"
677             +backupFile().getAbsolutePath()+"'",e);
678       }
679       // Tidy up if necessary.
680       removeBackup();
681     } else {
682       
683     }
684   }
685 }