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