started on ClientDocument implementation and Locked IO tests for VamsasArchive
[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     archive.createNewFile();
267     if (!rchive.lockFile()) 
268       throw new IOException("Failed to get lock on file "+archive);
269   
270     newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
271     entries = new Hashtable();
272   }
273   /**
274    * Safely initializes the VAMSAS XML document Jar Entry. 
275    * @return Writer to pass to the marshalling function.
276    * @throws IOException if a document entry has already been written. 
277    */
278   public PrintWriter getDocumentOutputStream() throws IOException {
279     if (newarchive==null)
280       openArchive();
281     if (!isDocumentWritten()) {
282       try {
283         if (addValidEntry(getDocumentJarEntry())) 
284           return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
285       } catch (Exception e) {
286         log.warn("Problems opening XML document JarEntry stream",e);
287       }
288     } else {
289       throw new IOException("Vamsas Document output stream is already written.");
290     }
291     return null;
292   }
293   /**
294    * Opens and returns the applicationData output stream for the appdataReference string.
295    * @param appdataReference
296    * @return Output stream to write to
297    * @throws IOException
298    */
299   public AppDataOutputStream getAppDataStream(String appdataReference) throws IOException {
300     if (newarchive==null)
301       throw new IOException("Attempt to write to closed VamsasArchive object.");
302     if (addValidEntry(appdataReference)) {
303       return new AppDataOutputStream(newarchive);
304     }
305     return null;
306   }
307   
308   /**
309    * Stops any current write to archive, and reverts to the backup if it exists.
310    * All existing locks on the original will be released. All backup files are removed.
311    */
312   public boolean cancelArchive() {
313     if (newarchive!=null) {
314       try { 
315         newarchive.close();
316         
317       } catch (Exception e) {
318         log.debug("Whilst closing newarchive",e);
319       };
320       if (!virginArchive) {
321         // then there is something to recover.
322         try {
323           recoverBackup();
324         }
325         catch (Exception e) {
326           log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
327           return false;
328         }
329       }
330       
331     } else {
332       log.warn("Client Error: cancelArchive called before archive("+original.getAbsolutePath()+") has been opened!");
333     }
334     closeAndReset(); // tidy up and release locks.
335     return true;
336   }
337   
338   /**
339    * recovers the original file's contents from the (temporary) backup. 
340    * @throws Exception if any SessionFile or file removal operations fail.
341    */
342   private void recoverBackup() throws Exception {
343     if (originalBackup!=null) {
344       // backup has been made.
345       // revert from backup and delete it (changing backup filename)
346       if (rchive==null) {
347         rchive = new SessionFile(original);
348       }
349       SessionFile bckup = new SessionFile(originalBackup);
350       
351       rchive.updateFrom(null, bckup); // recover from backup file.
352       bckup.unlockFile();
353       bckup=null;
354       removeBackup();
355     }
356   }
357   /**
358    * forget about any backup that was made - removing it first if it was only temporary.
359    */
360   private void removeBackup() {
361     if (originalBackup!=null) {
362       log.debug("Removing backup in "+originalBackup.getAbsolutePath());
363       if (!donotdeletebackup)
364         if (!originalBackup.delete())
365           log.info("VamsasArchive couldn't remove temporary backup "+originalBackup.getAbsolutePath());
366       originalBackup=null;
367     }
368   }
369   /**
370    * only do this if you want to destroy the current file output stream
371    *
372    */
373   private void closeAndReset() {
374     if (rchive!=null) {
375       rchive.unlockFile();
376       rchive=null;
377     }
378     if (original!=null) {
379       if (odoc!=null) {
380         odoc.close();
381         odoc=null;
382       }
383       if (archive!=null)
384         archive.delete();
385       if (odoclock!=null) {
386         odoclock.unlockFile();
387         odoclock = null;
388       }
389     }
390     removeBackup();
391     newarchive=null;
392     original=null;
393     entries=null;
394   }
395   
396   private final int _TRANSFER_BUFFER=4096*4;
397   /**
398    * open original archive file for exclusive (locked) reading.
399    * @throws IOException
400    */
401   private void accessOriginal() throws IOException {
402     if (original!=null && original.exists()) {
403       if (odoclock==null) 
404         odoclock = new SessionFile(original);
405       odoclock.lockFile();
406       if (odoc == null) 
407         odoc = new VamsasArchiveReader(original);
408     }
409   }
410   
411   /**
412    * Convenience method to copy over the referred entry from the backup to the new version.
413    * Warning messages are raised if no backup exists or the 
414    * entry doesn't exist in the backed-up original.
415    * Duplicate writes return true - but a warning message will also be raised.
416    * @param AppDataReference
417    * @return true if AppDataReference now exists in the new document
418    * @throws IOException
419    */
420   public boolean transferAppDataEntry(String AppDataReference) throws IOException {
421     return transferAppDataEntry(AppDataReference, AppDataReference);
422   }
423   /**
424    * Transfers an AppDataReference from old to new vamsas archive, with a name change.
425    * @see transferAppDataEntry(String AppDataReference)
426    * @param AppDataReference
427    * @param NewAppDataReference - AppDataReference in new Archive
428    * @return
429    * @throws IOException
430    */
431   public boolean transferAppDataEntry(String AppDataReference, String NewAppDataReference) throws IOException {
432     // TODO: Specify valid AppDataReference form in all VamsasArchive handlers
433     if (AppDataReference==null)
434       throw new IOException("null AppDataReference!");
435     if (original==null || !original.exists()) {
436       log.warn("No backup archive exists.");
437       return false;
438     }
439     if (entries.containsKey(NewAppDataReference)) {
440       log.warn("Attempt to write '"+NewAppDataReference+"' twice! - IGNORED");
441       return true;
442     }
443     
444     accessOriginal();
445     
446     java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
447     
448     if (adstream==null) {
449       log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
450       return false;
451     }
452     
453     java.io.OutputStream adout = getAppDataStream(NewAppDataReference);
454     // copy over the bytes
455     int written=-1;
456     long count=0;
457     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
458     do {
459       if ((written = adstream.read(buffer))>-1) {
460         adout.write(buffer, 0, written);
461         log.debug("Transferring "+written+".");
462         count+=written;
463       }
464     } while (written>-1);
465     log.debug("Sucessfully transferred AppData for '"
466         +AppDataReference+"' as '"+NewAppDataReference+"' ("+count+" bytes)");
467     return true;
468   }
469   /**
470    * transfers any AppDataReferences existing in the old document 
471    * that haven't already been transferred to the new one
472    * TODO: LATER: do the same for transfers requiring a namechange - more document dependent.
473    *  @return true if data was transferred.
474    */
475   public boolean transferRemainingAppDatas() throws IOException {
476     boolean transfered=false;
477     if (original==null || !original.exists()) {
478       log.warn("No backup archive exists.");
479       return false;
480     }
481     accessOriginal();
482     
483     if (getVorba()!=null) {
484       Vector originalRefs=null;
485       try {
486         originalRefs = vorba.getReferencedEntries(getVamsasDocument(), getOriginalArchiveReader());
487       } catch (Exception e) {
488         log.warn("Problems accessing original document entries!",e);
489       }
490       if (originalRefs!=null) {
491         Iterator ref = originalRefs.iterator();
492         while (ref.hasNext()) {
493           String oldentry = (String) ref.next();
494           if (oldentry!=null && !entries.containsKey(oldentry)) {
495             log.debug("Transferring remaining entry '"+oldentry+"'");
496             transfered |= transferAppDataEntry(oldentry);
497           }
498         }
499       }
500     } 
501     return transfered;
502   }
503   /**
504    * Tidies up and closes archive, removing any backups that were created.
505    * NOTE: It is up to the caller to delete the original archive backup obtained from backupFile()
506    * TODO: ensure all extant AppDataReference jar entries are transferred to new Jar
507    * TODO: provide convenient mechanism for generating new unique AppDataReferences and adding them to the document
508    */
509   public void closeArchive() throws IOException {
510     if (newarchive!=null) {
511       newarchive.closeEntry();
512       if (!isDocumentWritten())
513         log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
514       newarchive.close();
515       updateOriginal();
516       closeAndReset();
517     } else {
518       log.warn("Attempt to close archive that has not been opened for writing.");
519     }
520   }
521   /**
522    * Access original archive if it exists, pass the reader to the client
523    * Note: this is NOT thread safe and a call to closeArchive() will by necessity 
524    * close and invalidate the VamsasArchiveReader object.
525    * @return null if no original archive exists.
526    */
527   public VamsasArchiveReader getOriginalArchiveReader() throws IOException {
528     if (!virginArchive) {
529       accessOriginal();
530       return odoc;
531     }
532     return null;
533   }
534   /**
535    * returns original document's root vamsas elements.
536    * @return
537    * @throws IOException
538    * @throws org.exolab.castor.xml.MarshalException
539    * @throws org.exolab.castor.xml.ValidationException
540    */
541   public object[] getOriginalRoots() throws IOException, 
542   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException  {
543     return VamsasArchive.getOriginalRoots(this);
544   }
545   /**
546    * Access original document if it exists, and get VAMSAS root objects.
547    * @return vector of vamsas roots from original document
548    * @throws IOException
549    */
550   public static object[] getOriginalRoots(VamsasArchive ths) throws IOException, 
551   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
552     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
553     if (oReader!=null) {
554       
555       if (oReader.isValid()) {
556         InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
557         VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
558         if (doc!=null) 
559           return doc.getVAMSAS();
560         // TODO ensure embedded appDatas are garbage collected to save memory
561       } else {
562         InputStream vxmlis = oReader.getVamsasXmlStream();
563         if (vxmlis!=null) { // Might be an old vamsas file.
564           BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
565           InputStreamReader vxml = new InputStreamReader(ixml);
566           VAMSAS root[] = new VAMSAS[1];
567           root[0] = VAMSAS.unmarshal(vxml);
568           if (root[0]!=null)
569             return root;
570         }
571       }
572     }
573     return null;
574   }
575   protected SimpleDocument vorba = null;
576   
577   /**
578    * @return Returns the current VorbaIdFactory for the archive.
579    */
580   public VorbaIdFactory getVorba() {
581     if (vorba==null)
582       vorba = new SimpleDocument("simpleclient.VamsasArchive");
583     return vorba.getVorba();
584   }
585   
586   /**
587    * @param vorba the VorbaIdFactory to use for accessing vamsas objects.
588    */
589   public void setVorba(VorbaIdFactory Vorba) {
590     if (Vorba!=null) {
591       if (vorba==null)
592         vorba = new SimpleDocument(Vorba);
593       else
594         vorba.setVorba(Vorba);
595     } else
596       getVorba();
597   }
598   
599   /**
600    * Access and return current vamsas Document, if it exists, or create a new one 
601    * (without affecting VamsasArchive object state - so is NOT THREAD SAFE)
602    * TODO: possibly modify internal state to lock low-level files 
603    * (like the IClientDocument interface instance constructer would do) 
604    * @see org.vamsas.simpleclient.VamsasArchive.getOriginalVamsasDocument for additional caveats
605    * 
606    * @return
607    * @throws IOException
608    * @throws org.exolab.castor.xml.MarshalException
609    * @throws org.exolab.castor.xml.ValidationException
610    */
611   private VamsasDocument _doc=null; 
612   public VamsasDocument getVamsasDocument() throws IOException, 
613   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
614     if (_doc!=null)
615       return _doc;
616     _doc = getOriginalVamsasDocument(this, getVorba());
617     if (_doc!=null)
618       return _doc;
619     // Create a new document and return it
620     _doc = DocumentStuff.newVamsasDocument(new VAMSAS[] { new VAMSAS()}, 
621         ProvenanceStuff.newProvenance("org.vamsas.simpleclient.VamsasArchive", "Created new empty document")
622         , VersionEntries.latestVersion());
623     return _doc;
624   }
625   /**
626    * Access the original vamsas document for a VamsasArchive class, and return it.
627    * Users of the VamsasArchive class should use the getVamsasDocument method to retrieve
628    * the current document - only use this one if you want the 'backup' version.
629    * TODO: catch OutOfMemoryError - they are likely to occur here.
630    * NOTE: vamsas.xml datastreams are constructed as 'ALPHA_VERSION' vamsas documents.
631    * @param ths
632    * @return null if no document exists.
633    * @throws IOException
634    * @throws org.exolab.castor.xml.MarshalException
635    * @throws org.exolab.castor.xml.ValidationException
636    */
637   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths) throws IOException, 
638   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
639     return VamsasArchive.getOriginalVamsasDocument(ths, null);
640   }
641   
642   /**
643    * Uses VorbaXmlBinder to retrieve the VamsasDocument from the original archive referred to by ths
644    * @param ths
645    * @param vorba
646    * @return
647    * @throws IOException
648    * @throws org.exolab.castor.xml.MarshalException
649    * @throws org.exolab.castor.xml.ValidationException
650    */
651   public static VamsasDocument getOriginalVamsasDocument(VamsasArchive ths, VorbaIdFactory vorba) throws IOException, 
652   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
653     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
654     if (oReader!=null) {
655       ths.setVorba(vorba);
656       return ths.vorba.getVamsasDocument(oReader);
657     }
658     // otherwise - there was no valid original document to read.
659     return null;    
660   }
661   
662   public void putVamsasDocument(VamsasDocument doc) throws IOException, 
663   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
664     putVamsasDocument(doc, getVorba());
665   }
666   public void putVamsasDocument(VamsasDocument doc, VorbaIdFactory vorba) throws IOException, 
667   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
668     VorbaXmlBinder.putVamsasDocument(getDocumentOutputStream(), vorba, doc);
669   }
670 }