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