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