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