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