trivial
[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.PrintWriter;
11 import java.util.Hashtable;
12 import java.util.jar.JarEntry;
13 import java.util.jar.JarOutputStream;
14
15 import org.apache.commons.logging.Log;
16 import org.apache.commons.logging.LogFactory;
17 import org.vamsas.client.object;
18 import org.vamsas.objects.core.ApplicationData;
19 import org.vamsas.objects.core.VAMSAS;
20 import org.vamsas.objects.core.VamsasDocument;
21
22 /**
23  * Class for creating a vamsas archive
24  * (with backups)
25  * Writes to a temporary file and then swaps new file for backup.
26  * uses the sessionFile locking mechanism for safe I/O
27  * @author jimp
28  *
29  */
30 public class VamsasArchive {
31   private static Log log = LogFactory.getLog(VamsasArchive.class);
32   /**
33    * destination of new archive data
34    */
35   java.io.File archive=null;
36   /**
37    * locked IO handler for new archive file
38    */
39   SessionFile rchive=null; 
40   /**
41    * original archive file that is to be updated
42    */
43   java.io.File original=null;
44   /**
45    * original archive IO handler
46    */
47   SessionFile odoclock = null;
48   /**
49    * Original archive reader class
50    */
51   VamsasArchiveReader odoc = null;
52   /**
53    * true if a real vamsas document is being written.
54    */
55   boolean vamsasdocument=true;
56   /**
57    * Output stream for archived data
58    */
59   JarOutputStream newarchive=null;
60   /**
61    * JarEntries written to archive
62    */
63   Hashtable entries = null;
64   /**
65    * true if we aren't just updating an archive
66    */
67   private boolean virginArchive=false;
68   /**
69    * Create a new vamsas archive
70    * nb. No file locks are made until open() is called.
71    * @param archive - file spec for new vamsas archive
72    * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive
73    */
74   public VamsasArchive(File archive, boolean vamsasdocument) {
75     super();
76     if (archive==null || (archive!=null && archive.canWrite())) {
77       log.fatal("Invalid parameters for VamsasArchive constructor:"+((archive!=null) 
78           ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
79     }
80     this.vamsasdocument = vamsasdocument;
81     if (archive.exists()) {
82       this.original = archive;
83       this.archive = null;       // archive will be a temp file when the open method is called
84       virginArchive=false;
85     } else {
86       this.original = null;
87       this.archive = archive; // archive is written in place.
88       virginArchive = true;
89     }
90   }
91   /**
92    * name of backup of existing archive that has been updated/overwritten.
93    */
94   File originalBackup = null;
95   
96   private void makeBackup() {
97     if (!virginArchive) {
98       if (originalBackup==null && original!=null && original.exists()) {
99         try {
100           accessOriginal();
101           originalBackup = odoclock.backupSessionFile(null, original.getName(), ".bak", original.getParentFile());
102         }
103         catch (IOException e) {
104           log.warn("Problem whilst making a backup of original archive.",e);
105         }
106       }
107     }
108   }
109   /**
110    * called after archive is written to put file in its final place
111    * TODO: FINISH original should have sessionFile, and archive should also have sessionFile
112    */
113   private void updateOriginal() {
114     if (original!=null) {
115       if (!virginArchive) {
116         if (odoc!=null) {
117           odoc.close();
118           odoc=null;
119         }
120         if (!archive.getAbsolutePath().equals(original)) {
121           if (originalBackup==null) 
122             makeBackup();
123           try {
124             odoclock.updateFrom(null, rchive);
125           }
126           catch (IOException e) {
127             log.error("Problem updating archive from temporary file!",e);
128           }
129         } else {
130           log.warn("archive and original are the same file! ("+archive.getAbsolutePath()+")");
131         }
132       } // else virginArchive are put in correct place from the beginning
133       
134     }
135   }
136   /**
137    * called by app to get name of backup if it was made.
138    * @return null or a valid file object
139    */
140   public File backupFile() {
141     
142     if (!virginArchive) {
143       makeBackup();
144       return ((original==null) ? originalBackup : null);
145     
146     }
147     return null;
148   }
149   
150   protected String getDocumentJarEntry() {
151     if (vamsasdocument)
152       return VamsasArchiveReader.VAMSASDOC;
153     return VamsasArchiveReader.VAMSASXML;
154   }
155   
156   /**
157    * @return true if Vamsas Document has been written to archive
158    */
159   protected boolean isDocumentWritten() {
160     if (newarchive==null)
161       log.warn("isDocumentWritten called for unopened archive.");
162     if (entries!=null) {
163       if (entries.containsKey(getDocumentJarEntry()))
164           return true;
165     }
166     return false;
167   }
168   /**
169    * Add unique entry strings to internal JarEntries list.
170    * @param entry
171    * @return true if entry was unique and was added.
172    */
173   private boolean addEntry(String entry) {
174     if (entries!=null)
175       entries=new Hashtable();
176     if (entries.containsKey(entry))
177       return false;
178     entries.put(entry, new Integer(entries.size()));
179     return true;
180   }
181   /**
182    * adds named entry to newarchive or returns false.
183    * @param entry
184    * @return true if entry was unique and could be added
185    * @throws IOException if entry name was invalid or a new entry could not be made on newarchive
186    */
187   private boolean addValidEntry(String entry) throws IOException {
188     JarEntry je = new JarEntry(entry);
189     if (!addEntry(entry))
190       return false;
191     newarchive.putNextEntry(je);
192     return true;
193   }
194   
195   /**
196    * opens the new archive ready for writing. If the new archive is replacing an existing one, 
197    * then the existing archive will be locked, and the new archive written to a temporary file. 
198    * The new archive will be put in place once close() is called.
199    * @throws IOException
200    */
201   private void openArchive() throws IOException {
202     
203     if (newarchive!=null) {
204       log.warn("openArchive() called multiple times.");
205       throw new IOException("Vamsas Archive '"+archive.getAbsolutePath()+"' is already open.");
206     }
207     
208     if (archive==null) {
209       if (original==null) {
210         log.warn("openArchive called on uninitialised VamsasArchive object.");
211         throw new IOException("Badly initialised VamsasArchive object - no archive file specified.");
212       }
213       // lock the original
214       accessOriginal();
215       // make a temporary file to write to
216       archive = File.createTempFile(original.getName(), "new",original.getParentFile());
217     }
218     
219     rchive = new SessionFile(archive);
220     rchive.lockFile();
221     newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
222     entries = new Hashtable();
223   }
224   /**
225    * Safely initializes the VAMSAS XML document Jar Entry. 
226    * @return Writer to pass to the marshalling function.
227    * @throws IOException if a document entry has already been written. 
228    */
229   public PrintWriter getDocumentOutputStream() throws IOException {
230     if (newarchive==null)
231       openArchive();
232     if (!isDocumentWritten()) {
233       try {
234         if (addValidEntry(getDocumentJarEntry())) 
235           return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
236       } catch (Exception e) {
237         log.warn("Problems opening XML document JarEntry stream",e);
238       }
239     } else {
240       throw new IOException("Vamsas Document output stream is already written.");
241     }
242     return null;
243   }
244   /**
245    * Opens and returns the applicationData output stream for the appdataReference string.
246    * @param appdataReference
247    * @return Output stream to write to
248    * @throws IOException
249    */
250   public AppDataOutputStream getAppDataStream(String appdataReference) throws IOException {
251     if (newarchive!=null)
252       openArchive();
253     if (addValidEntry(appdataReference)) {
254       return new AppDataOutputStream(newarchive);
255     }
256     return null;
257   }
258   
259   /**
260    * Stops any current write to archive, and reverts to the backup if it exists.
261    * All existing locks on the original will be released.
262    */
263   public boolean cancelArchive() {
264     if (newarchive!=null) {
265       try { 
266         newarchive.close();
267       } catch (Exception e) {};
268       if (!virginArchive) {
269         // then there is something to recover.
270         if (originalBackup!=null) {
271           // backup has been made.
272           // revert from backup and delete it (changing backup filename)
273           if (rchive==null) {
274             rchive = new SessionFile(original);
275           }
276           SessionFile bckup = new SessionFile(originalBackup);
277           
278             try {
279               rchive.updateFrom(null,bckup); // recover from backup file.
280               bckup.unlockFile();
281               bckup=null;
282               originalBackup.delete();
283             }
284             catch (Exception e) {
285               log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
286               return false;
287             }
288         }
289         // original is untouched
290         // just delete temp files
291         
292         }
293     } else {
294       log.info("cancelArchive called before archive("+original.getAbsolutePath()+") has been opened!");
295     }
296     closeAndReset();
297     return true;
298   }
299   
300   /**
301    * only do this if you want to destroy the current file output stream
302    *
303    */
304   private void closeAndReset() {
305     rchive.unlockFile();
306     rchive = null;
307     if (original!=null) {
308       if (odoc!=null) {
309         odoc.close();
310         odoc=null;
311       }
312       archive.delete();
313       if (odoclock!=null) {
314         odoclock.unlockFile();
315         odoclock = null;
316       }
317     }
318     newarchive=null;
319     original=null;
320     entries=null;
321   }
322
323   private final int _TRANSFER_BUFFER=4096*4;
324   /**
325    * open backup for exclusive (locked) reading.
326    * @throws IOException
327    */
328   private void accessOriginal() throws IOException {
329     if (original!=null && original.exists()) {
330       if (odoclock==null) 
331         odoclock = new SessionFile(original);
332       odoclock.lockFile();
333       if (odoc == null) 
334         odoc = new VamsasArchiveReader(original);
335     }
336   }
337
338   /**
339    * Convenience method to copy over the referred entry from the backup to the new version.
340    * Warning messages are raised if no backup exists or the 
341    * entry doesn't exist in the backed-up original.
342    * Duplicate writes return true - but a warning message will also be raised.
343    * @param AppDataReference
344    * @return true if AppDataReference now exists in the new document
345    * @throws IOException
346    */
347   public boolean transferAppDataEntry(String AppDataReference) throws IOException {
348     return transferAppDataEntry(AppDataReference, AppDataReference);
349   }
350   /**
351    * Transfers an AppDataReference from old to new vamsas archive, with a name change.
352    * @see transferAppDataEntry(String AppDataReference)
353    * @param AppDataReference
354    * @param NewAppDataReference - AppDataReference in new Archive
355    * @return
356    * @throws IOException
357    */
358   public boolean transferAppDataEntry(String AppDataReference, String NewAppDataReference) throws IOException {
359     // TODO: Specify valid AppDataReference form in all VamsasArchive handlers
360     if (AppDataReference==null)
361       throw new IOException("null AppDataReference!");
362     if (original==null || !original.exists()) {
363       log.warn("No backup archive exists.");
364       return false;
365     }
366     if (entries.containsKey(NewAppDataReference)) {
367       log.warn("Attempt to write '"+NewAppDataReference+"' twice! - IGNORED");
368       return true;
369     }
370     
371     accessOriginal();
372     
373     java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
374     
375     if (adstream==null) {
376       log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
377       return false;
378     }
379     
380     java.io.OutputStream adout = getAppDataStream(NewAppDataReference);
381     // copy over the bytes
382     int written=-1;
383     long count=0;
384     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
385     do {
386       if ((written = adstream.read(buffer))>-1) {
387         adout.write(buffer, 0, written);
388         log.debug("Transferring "+written+".");
389         count+=written;
390       }
391     } while (written>-1);
392     log.debug("Sucessfully transferred AppData for '"
393         +AppDataReference+"' as '"+NewAppDataReference+"' ("+count+" bytes)");
394     return true;
395   }
396   /**
397    * Tidies up and closes archive, removing any backups that were created.
398    * NOTE: It is up to the caller to delete the original archive backup obtained from backupFile()
399    */
400   public void closeArchive() throws IOException {
401     if (newarchive!=null) {
402       newarchive.closeEntry();
403       if (!isDocumentWritten())
404         log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"': No document has been written.");
405       newarchive.close();
406       updateOriginal();
407       closeAndReset();
408     } else {
409       log.warn("Attempt to close archive that has not been opened for writing.");
410     }
411   }
412   /**
413    * Access original archive if it exists, pass the reader to the client
414    * Note: this is NOT thread safe and a call to closeArchive() will by necessity 
415    * close and invalidate the VamsasArchiveReader object.
416    * @return null if no original archive exists.
417    */
418   public VamsasArchiveReader getOriginalArchiveReader() throws IOException {
419     if (!virginArchive) {
420       accessOriginal();
421       return odoc;
422     }
423     return null;
424   }
425   /**
426    * returns original document's root vamsas elements.
427    * @return
428    * @throws IOException
429    * @throws org.exolab.castor.xml.MarshalException
430    * @throws org.exolab.castor.xml.ValidationException
431    */
432   public object[] getOriginalRoots() throws IOException, 
433   org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException  {
434     return VamsasArchive.getOriginalRoots(this);
435   }
436   /**
437    * Access original document if it exists, and get VAMSAS root objects.
438    * @return vector of vamsas roots from original document
439    * @throws IOException
440    */
441   public static object[] getOriginalRoots(VamsasArchive ths) throws IOException, 
442     org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
443     VamsasArchiveReader oReader = ths.getOriginalArchiveReader();
444     if (oReader!=null) {
445       
446       if (oReader.isValid()) {
447         InputStreamReader vdoc = new InputStreamReader(oReader.getVamsasDocumentStream());
448         VamsasDocument doc = VamsasDocument.unmarshal(vdoc);
449         if (doc!=null) 
450           return doc.getVAMSAS();
451         // TODO ensure embedded appDatas are garbage collected 
452       } else {
453         InputStream vxmlis = oReader.getVamsasXmlStream();
454         if (vxmlis!=null) { // Might be an old vamsas file.
455           BufferedInputStream ixml = new BufferedInputStream(oReader.getVamsasXmlStream());
456           InputStreamReader vxml = new InputStreamReader(ixml);
457           VAMSAS root[] = new VAMSAS[1];
458           root[0] = VAMSAS.unmarshal(vxml);
459           if (root[0]!=null)
460             return root;
461         }
462       }
463     }
464     return null;
465   }
466   
467
468 }