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