reorganising vamsasArchive (write to temp file, rather than write to original filename)
[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   java.io.File archive;
29   SessionFile rchive=null; 
30   java.io.File original=null;
31   SessionFile odoclock = null;
32   VamsasArchiveReader odoc = null;
33   boolean vamsasdocument=true;  // make a document archive (rather than a vamsas.xml archive)
34   JarOutputStream newarchive=null;
35   Hashtable entries = null;
36   
37   /**
38    * Create a new vamsas archive
39    * @param archive - file spec for new vamsas archive
40    * @param vamsasdocument true if archive is to be a fully fledged vamsas document archive
41    */
42   public VamsasArchive(File archive, boolean vamsasdocument) {
43     super();
44     if (archive==null || (archive!=null && archive.canWrite())) {
45       log.fatal("Invalid parameters for VamsasArchive constructor:"+((archive!=null) 
46           ? "File cannot be overwritten." : "Null Object not valid constructor parameter"));
47     }
48     this.vamsasdocument = vamsasdocument;
49     if (archive.exists()) {
50       this.original = archive;
51       this.archive = null;
52       // make the archive temp file when the open method is called
53     } else {
54       this.original = null;
55       this.archive = archive; // write directly to the new archive
56     }
57   }
58   /**
59    * called by app to determine if a backup will be made or not.
60    * @return
61    */
62   public File backupFile() {
63     
64     if (original!=null) {
65       makeBackup();
66       return ((original==null) ? originalBackup : null);
67     
68     }
69     return null;
70   }
71   
72   protected String getDocumentJarEntry() {
73     if (vamsasdocument)
74       return VamsasArchiveReader.VAMSASDOC;
75     return VamsasArchiveReader.VAMSASXML;
76   }
77   protected boolean isDocumentWritten() {
78     if (newarchive==null)
79       log.warn("isDocumentWritten called for unopened archive.");
80     if (entries!=null) {
81       if (entries.containsKey(getDocumentJarEntry()))
82           return true;
83     }
84     return false;
85   }
86   /**
87    * Add unique entry strings to internal JarEntries list.
88    * @param entry
89    * @return true if entry was unique and was added.
90    */
91   private boolean addEntry(String entry) {
92     if (entries!=null)
93       entries=new Hashtable();
94     if (entries.containsKey(entry))
95       return false;
96     entries.put(entry, new Integer(entries.size()));
97     return true;
98   }
99   /**
100    * adds named entry to newarchive or returns false.
101    * @param entry
102    * @return true if entry was unique and could be added
103    * @throws IOException if entry name was invalid or a new entry could not be made on newarchive
104    */
105   private boolean addValidEntry(String entry) throws IOException {
106     JarEntry je = new JarEntry(entry);
107     if (!addEntry(entry))
108       return false;
109     newarchive.putNextEntry(je);
110     return true;
111   }
112   
113   File originalBackup = null;
114   
115   private void makeBackup() {
116     if (originalBackup!=null && original!=null && original.exists()) {
117       try {
118         accessBackup();
119         originalBackup = odoclock.backupSessionFile(null, original.getName(), ".bak", original.getParentFile());
120         // rchive.fileLock.rafile.getChannel().truncate(0);
121       }
122       catch (IOException e) {
123         log.warn("Problem whilst making a backup of original archive.",e);
124       }
125     }
126   }
127   File tempoutput = null;
128   SessionFile trchive = null;
129   private void openArchive() throws IOException {
130     
131     if (newarchive!=null)
132       throw new IOException("Vamsas Archive '"+archive.getAbsolutePath()+"' is already open.");
133
134     if (archive==null) {
135       if (original==null) {
136         throw new IOException("Badly initialised VamsasArchive object - null archive file.");
137       }
138       // make a temporary file to write to 
139       // TODO: finish
140         
141     } else {
142       // tempfile is real archive
143     }
144     
145     /* if (archive.exists()) 
146       if (original==null) {
147         original = rchive.backupSessionFile(null, archive.getName(), ".bak", archive.getParentFile());
148         rchive.fileLock.rafile.getChannel().truncate(0);
149       } else
150         throw new IOException("Backup of current VamsasArchive already exists. Fix this BUG");
151     */
152     
153     rchive.lockFile();
154     newarchive = new JarOutputStream(new BufferedOutputStream(new java.io.FileOutputStream(archive)));  
155     entries = new Hashtable();
156   }
157   
158   /**
159     if (archive.exists()) 
160       if (original==null) {
161         original = rchive.backupSessionFile(null, archive.getName(), ".bak", archive.getParentFile());
162         rchive.fileLock.rafile.getChannel().truncate(0);
163       } else
164     
165    * Safely initializes the VAMSAS XML document Jar Entry. 
166    * @return Writer to pass to the marshalling function.
167    * @throws IOException if a document entry has already been written. 
168    */
169   public PrintWriter getDocumentOutputStream() throws IOException {
170     if (newarchive==null)
171       openArchive();
172     if (!isDocumentWritten()) {
173       try {
174         if (addValidEntry(getDocumentJarEntry())) 
175           return new PrintWriter(new java.io.OutputStreamWriter(newarchive, "UTF-8"));
176       } catch (Exception e) {
177         log.warn("Problems opening XML document JarEntry stream",e);
178       }
179     } else {
180       throw new IOException("Vamsas Document output stream is already written.");
181     }
182     return null;
183   }
184   /**
185    * Opens and returns the applicationData output stream for the appdataReference string.
186    * TODO: Make a wrapper class to catch calls to OutputStream.close() which normally close the Jar output stream.
187    * @param appdataReference
188    * @return Output stream to write to
189    * @throws IOException
190    */
191   public OutputStream getAppDataStream(String appdataReference) throws IOException {
192     if (newarchive!=null)
193       openArchive();
194     if (addValidEntry(appdataReference)) {
195       return new DataOutputStream(newarchive);
196     }
197     return null;
198   }
199   
200   /**
201    * Stops any current write to archive, and reverts to the backup if it exists.
202    * 
203    */
204   public boolean cancelArchive() {
205     if (newarchive!=null) {
206       try { 
207         newarchive.close();
208       } catch (Exception e) {};
209       if (original!=null) {
210         if (rchive!=null) {
211           try {
212             // recover from backup file.
213             rchive.fileLock.rafile.getChannel().truncate(0);
214             SessionFile bck = new SessionFile(original);
215             if (bck.lockFile()) {
216               rchive.fileLock.rafile.getChannel().transferFrom(
217                   bck.fileLock.rafile.getChannel(), 
218                   0, bck.fileLock.rafile.getChannel().size());
219               bck.unlockFile();
220               closeAndReset();
221               return true;
222             } else {
223               log.warn("Could not get lock on backup file to recover");
224             }
225           }
226           catch (Exception e) {
227             log.warn("Problems when trying to cancel Archive "+archive.getAbsolutePath(), e);
228             return false;
229           }
230         }
231         log.fatal("Attempt to backup from Archive Backup without valid write lock! ('"+archive.getAbsolutePath()+"') - BUG!");
232       }
233     }
234     return false;
235   }
236   
237   /**
238    * only do this if you want to destroy the current file output stream
239    *
240    */
241   private void closeAndReset() {
242     rchive.unlockFile();
243     rchive = null;
244     if (original!=null) {
245       if (odoc!=null) {
246         odoc.close();
247         odoc=null;
248       }
249       original.delete();
250       if (odoclock!=null) {
251         odoclock.unlockFile();
252         odoclock = null;
253       }
254     }
255     newarchive=null;
256     original=null;
257     entries=null;
258   }
259
260   private final int _TRANSFER_BUFFER=4096*4;
261   /**
262    * open backup for exclusive (locked) reading.
263    * @throws IOException
264    */
265   private void accessBackup() throws IOException {
266     if (original!=null && original.exists()) {
267       if (odoclock==null) 
268         odoclock = new SessionFile(original);
269       odoclock.lockFile();
270       if (odoc == null) 
271         odoc = new VamsasArchiveReader(original);
272     }
273   }
274   /**
275    * Convenience method to copy over the referred entry from the backup to the new version.
276    * Warning messages are raised if no backup exists or the 
277    * entry doesn't exist in the backed-up original.
278    * Duplicate writes return true - but a warning message will also be raised.
279    * @param AppDataReference
280    * @return true if AppDataReference now exists in the new document
281    * @throws IOException
282    */
283   public boolean transferAppDataEntry(String AppDataReference) throws IOException {
284     // TODO: Specify valid AppDataReference form in all VamsasArchive handlers
285     if (AppDataReference==null)
286       throw new IOException("Invalid AppData Reference!");
287     if (original==null || !original.exists()) {
288       log.warn("No backup archive exists.");
289       return false;
290     }
291     if (entries.containsKey(AppDataReference)) {
292       log.warn("Attempt to write '"+AppDataReference+"' twice! - IGNORED");
293       return true;
294     }
295     
296     accessBackup();
297     
298     java.io.InputStream adstream = odoc.getAppdataStream(AppDataReference);
299     
300     if (adstream==null) {
301       log.warn("AppDataReference '"+AppDataReference+"' doesn't exist in backup archive.");
302       return false;
303     }
304     
305     java.io.OutputStream adout = getAppDataStream(AppDataReference);
306     // copy over the bytes
307     int written=-1;
308     long count=0;
309     byte[] buffer = new byte[_TRANSFER_BUFFER]; // conservative estimate of a sensible buffer
310     do {
311       if ((written = adstream.read(buffer))>-1) {
312         adout.write(buffer, 0, written);
313         log.debug("Transferring "+written+".");
314         count+=written;
315       }
316     } while (written>-1);
317     log.debug("Sucessfully transferred AppData for "+AppDataReference+" ("+count+" bytes)");
318     return true;
319   }
320   
321   /**
322    * Tidies up and closes archive, removing any backups that were created.
323    * NOTE: It is up to the caller to 
324    */
325   public void closeArchive() throws IOException {
326     if (newarchive!=null) {
327       newarchive.closeEntry();
328       if (!isDocumentWritten())
329         log.warn("Premature closure of archive '"+archive.getAbsolutePath()+"'");
330       newarchive.close();
331       closeAndReset();
332     } else {
333       log.warn("Attempt to close archive that has not been opened for writing.");
334     }
335   }
336 }