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