refactored IClientAppdata and implementation has been minimally tested and bugfixed...
[vamsas.git] / src / uk / ac / vamsas / client / simpleclient / SimpleClientAppdata.java
1 /**
2  * 
3  */
4 package uk.ac.vamsas.client.simpleclient;
5
6 import java.io.BufferedInputStream;
7 import java.io.BufferedOutputStream;
8 import java.io.ByteArrayInputStream;
9 import java.io.ByteArrayOutputStream;
10 import java.io.DataInput;
11 import java.io.DataInputStream;
12 import java.io.DataOutput;
13 import java.io.DataOutputStream;
14 import java.io.FileInputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.Vector;
18 import java.util.jar.JarEntry;
19 import java.util.jar.JarInputStream;
20 import java.util.jar.JarOutputStream;
21
22 import org.apache.commons.logging.Log;
23 import org.apache.commons.logging.LogFactory;
24
25 import uk.ac.vamsas.client.AppDataInputStream;
26 import uk.ac.vamsas.client.AppDataOutputStream;
27 import uk.ac.vamsas.client.IClientAppdata;
28 import uk.ac.vamsas.objects.core.AppData;
29 import uk.ac.vamsas.objects.core.ApplicationData;
30 import uk.ac.vamsas.objects.core.User;
31 import uk.ac.vamsas.objects.utils.AppDataReference;
32
33 /**
34  * @author jimp
35  * Access interface to data chunks read from a VamsasArchiveReader stream 
36  * (or byte buffer input stream) or written to a VamsasArchive stream.
37  * // TODO: get VamsasArchiveReader from sclient
38  */
39 public class SimpleClientAppdata implements IClientAppdata {
40   private static Log log = LogFactory.getLog(SimpleClientAppdata.class);
41   /**
42    * has the session's document been accessed to get the AppData entrys?
43    */
44   protected boolean accessedDocument = false;
45   /**
46    * has the user datablock been modified ? 
47    * temporary file containing new user specific application data chunk
48    */
49   SessionFile newUserData=null;
50   JarOutputStream newUserDataStream = null;
51   /**
52    * has the apps global datablock been modified ?
53    * temporary file containing new global application data chunk
54    */
55   SessionFile newAppData=null;
56   JarOutputStream newAppDataStream=null;
57   /**
58    * set by extractAppData
59    */
60   protected ApplicationData appsGlobal = null;
61   /**
62    * set by extractAppData
63    */
64   protected User usersData = null;
65   
66   ClientDocument clientdoc;
67   /**
68    * state flags
69    * - accessed ClientAppdata
70    * - accessed UserAppdata
71    * => inputStream from embedded xml or jar entry of backup has been created
72    * - set ClientAppdata
73    * - set UserAppdata
74    * => an output stream has been created and written to - or a data chunk has been written.
75    *  - need flag for switching between embedded and jar entry mode ? - always write a jar entry for a stream.
76    *  - need code for rewind and overwriting if the set*Appdata methods are called more than once.
77    *  - need flags for streams to except a call to set*Appdata when an output stream exists and is open.
78    *  - need 
79    * @param clientdoc The ClientDocument instance that this IClientAppData is accessing
80    */
81   protected SimpleClientAppdata(ClientDocument clientdoc) {
82     if (clientdoc==null) {
83       log.fatal("Implementation error - Null ClientDocument for SimpleClientAppdata construction.");
84       throw new Error("Implementation error - Null ClientDocument for SimpleClientAppdata construction.");
85     }
86     this.clientdoc = clientdoc;
87   }
88   /**
89    * gets appropriate app data for the application, if it exists in this dataset
90    * Called by every accessor to ensure data has been retrieved from document.
91    */
92   private void extractAppData(uk.ac.vamsas.objects.core.VamsasDocument doc) {
93     if (doc==null) {
94       log.debug("extractAppData called for null document object");
95       return;
96     }
97     if (accessedDocument) {
98       return;
99     }
100     Vector apldataset = AppDataReference.getUserandApplicationsData(
101         doc, clientdoc.sclient.getUserHandle(), clientdoc.sclient.getClientHandle());
102     accessedDocument = true;
103     if (apldataset!=null) {
104       if (apldataset.size()>0) {
105         AppData clientdat = (AppData) apldataset.get(0);
106         if (clientdat instanceof ApplicationData) {
107           appsGlobal = (ApplicationData) clientdat;
108           if (apldataset.size()>1) {
109             clientdat = (AppData) apldataset.get(1);
110             if (clientdat instanceof User) {
111               usersData = (User) clientdat;
112             }
113             if (apldataset.size()>2)
114               log.info("Ignoring additional ("+(apldataset.size()-2)+") AppDatas returned by document appdata query.");
115           } 
116         } else {
117           log.warn("Unexpected entry in AppDataReference query: id="+clientdat.getVorbaId()+" type="+clientdat.getClass().getName());
118         }
119         apldataset.removeAllElements(); // destroy references.
120       }
121     }
122   }
123   /**
124    * LATER: generalize this for different low-level session implementations (it may not always be a Jar)
125    * @param appdata
126    * @param docreader
127    * @return
128    */
129   private InputStream getAppDataStream(AppData appdata, VamsasArchiveReader docreader) {
130     String entryRef = appdata.getDataReference();
131     if (entryRef!=null) {
132       log.debug("Resolving appData reference +"+entryRef);
133       InputStream entry = docreader.getAppdataStream(entryRef);
134       if (entry!=null) {
135         return entry;
136         // log.warn("Implementation problem - docreader didn't return a JarInputStream entry.");
137       }
138     } else {
139       log.debug("GetAppDataStream called for an AppData without a data reference.");
140     }
141     return null;
142   }
143   /**
144    * yuk - size of buffer used for slurping appData JarEntry into a byte array.
145    */
146   private final int _TRANSFER_BUFFER=4096*4;
147
148   /**
149    * Resolve AppData object to a byte array.
150    * @param appdata
151    * @param archiveReader
152    * @return null or the application data as a byte array
153    */
154   private byte[] getAppDataAsByteArray(AppData appdata, VamsasArchiveReader docreader) {
155     if (appdata.getData()==null) {
156       if (docreader==null) {
157         log.warn("Silently failing getAppDataAsByteArray with null docreader.",new Exception());
158         return null;
159       }
160       // resolve and load data
161       InputStream entry = getAppDataStream(appdata, docreader); 
162       ByteArrayOutputStream bytes = new ByteArrayOutputStream();
163       try {
164         byte buff[] = new byte[_TRANSFER_BUFFER];
165         int olen=0;
166         while (entry!=null && entry.available()>0) {
167           int len = entry.read(buff, 0, _TRANSFER_BUFFER);
168           if (len>-1)
169           { bytes.write(buff, 0, len);
170             olen+=len;
171           }
172         }
173         buff=null;
174       } catch (Exception e) {
175         log.warn("Unexpected exception - probable truncation when accessing VamsasDocument entry "+appdata.getDataReference(), e);
176       }
177       if (bytes.size()>0) {
178         // LATER: deal with probable OutOfMemoryErrors here
179         log.debug("Got "+bytes.size()+" bytes from AppDataReference "+appdata.getDataReference());
180         byte data[] = bytes.toByteArray();
181         bytes = null;
182         return data;
183       }
184       return null;
185     } else {
186       log.debug("Returning inline AppData block for "+appdata.getVorbaId());
187       return appdata.getData();
188     }
189   }
190   /**
191    * internal method for getting a DataInputStream from an AppData object.
192    * @param appdata
193    * @param docreader
194    * @return data in object or null if no data is accessible
195    */
196   private AppDataInputStream getAppDataAsDataInputStream(AppData appdata, VamsasArchiveReader docreader) {
197     if (appdata!=null && docreader!=null) {
198       String entryRef = appdata.getDataReference();
199       if (entryRef!=null) {
200         log.debug("Resolving AppData reference for "+entryRef);
201         InputStream jstrm = docreader.getAppdataStream(entryRef);
202         if (jstrm!=null)
203           return new AppDataInputStream(jstrm);
204         else {
205           log.debug("Returning null input stream for unresolved reference ("+entryRef+") id="+appdata.getVorbaId());
206           return null;
207         }
208       } else {
209         // return a byteArray input stream
210         byte[] data=appdata.getData();
211         if (data.length>0) {
212           ByteArrayInputStream stream = new ByteArrayInputStream(data);
213           return new AppDataInputStream(stream);
214         } else {
215           log.debug("Returning null input stream for empty Appdata data block in id="+appdata.getVorbaId());
216           return null;
217         }
218       }
219     } else {
220       log.debug("Returning null DataInputStream for appdata entry:"+appdata.getVorbaId());
221     }
222     return null;
223   }
224
225   /**
226    * internal method for getting ByteArray from AppData object
227    * @param clientOrUser - true for returning userData, otherwise return Client AppData.
228    * @return null or byte array
229    */
230   private byte[] _getappdataByteArray(boolean clientOrUser) {
231     if (clientdoc==null)
232       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
233     byte[] data=null;
234     String appdName;
235     if (!clientOrUser) {
236       appdName = "Client's Appdata";
237     } else {
238       appdName = "User's Appdata";
239     }    
240     log.debug("getting "+appdName+" as a byte array");
241     extractAppData(clientdoc.getVamsasDocument());
242     AppData object;
243     if (!clientOrUser) {
244       object = appsGlobal;
245     } else {
246       object = usersData;
247     }
248     if (object!=null) {
249       log.debug("Trying to resolve "+appdName+" object to byte array.");
250       data = getAppDataAsByteArray(object, clientdoc.getVamsasArchiveReader());
251     }
252     if (data == null)
253       log.debug("Returning null for "+appdName+"ClientAppdata byte[] array");
254     return data;
255     
256   }
257   
258   /**
259    * common method for Client and User AppData->InputStream accessor
260    * @param clientOrUser - the appData to resolve - false for client, true for user appdata.
261    * @return null or the DataInputStream desired.
262    */
263   private AppDataInputStream _getappdataInputStream(boolean clientOrUser) {
264     if (clientdoc==null)
265       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
266     String appdName;
267     if (!clientOrUser) {
268       appdName = "Client's Appdata";
269     } else {
270       appdName = "User's Appdata";
271     }
272     if (log.isDebugEnabled())
273       log.debug("getting "+appdName+" as an input stream.");
274     extractAppData(clientdoc.getVamsasDocument());
275     AppData object;
276     if (!clientOrUser) {
277       object = appsGlobal;
278     } else {
279       object = usersData;
280     }
281     if (object!=null) {
282       log.debug("Trying to resolve ClientAppdata object to an input stream.");
283       return getAppDataAsDataInputStream(object, clientdoc.getVamsasArchiveReader());
284     }
285     log.debug("getClientInputStream returning null.");
286     return null;
287   }
288   /* (non-Javadoc)
289    * @see uk.ac.vamsas.client.IClientAppdata#getClientAppdata()
290    */
291   public byte[] getClientAppdata() {
292     return _getappdataByteArray(false);
293   }
294   /* (non-Javadoc)
295    * @see uk.ac.vamsas.client.IClientAppdata#getClientInputStream()
296    */
297   public AppDataInputStream getClientInputStream() {
298     return _getappdataInputStream(false);
299   }
300
301   /* (non-Javadoc)
302    * @see uk.ac.vamsas.client.IClientAppdata#getUserAppdata()
303    */
304   public byte[] getUserAppdata() {
305     return _getappdataByteArray(true);
306   }
307
308   /* (non-Javadoc)
309    * @see uk.ac.vamsas.client.IClientAppdata#getUserInputStream()
310    */
311   public AppDataInputStream getUserInputStream() {
312     return _getappdataInputStream(true);
313   }
314   /**
315    * methods for writing new AppData entries.
316    */
317   private AppDataOutputStream _getAppdataOutputStream(boolean clientOrUser) {
318     String apdname;
319     SessionFile apdfile=null;
320     if (!clientOrUser) {
321       apdname = "clientAppData";
322       apdfile = newAppData;
323     } else {
324       apdname = "userAppData";
325       apdfile = newUserData;
326     }
327     try {
328       if (apdfile==null) {
329         apdfile=clientdoc.sclient._session.getTempSessionFile(apdname,".jar");
330         log.debug("Successfully made temp appData file for "+apdname);
331       } else {
332         // truncate to remove existing data.
333         apdfile.fileLock.getRaFile().setLength(0);
334         log.debug("Successfully truncated existing temp appData for "+apdname);
335       }
336       } catch (Exception e) {
337       log.error("Whilst opening temp file in directory "+clientdoc.sclient._session.sessionDir, e);
338     }
339     // we do not make another file for the new entry if one exists already
340     if (!clientOrUser) {
341       newAppData = apdfile;
342     } else {
343       newUserData = apdfile;
344     }
345     try {
346       apdfile.lockFile();
347       // LATER: Refactor these local AppDatastream IO stuff to their own class.
348       JarOutputStream dstrm = 
349         new JarOutputStream(apdfile.fileLock.getBufferedOutputStream(true));
350       if (!clientOrUser) {
351         newAppDataStream = dstrm;
352       } else {
353         newUserDataStream = dstrm;
354       }
355       dstrm.putNextEntry(new JarEntry("appData_entry.dat"));
356       // LATER: there may be trouble ahead if an AppDataOutputStream is written to by one thread when another truncates the file. This situation should be prevented if possible
357       return new AppDataOutputStream(dstrm);
358     }
359     catch (Exception e) {
360       log.error("Whilst opening jar output stream for file "+apdfile.sessionFile);
361     }
362     // tidy up and return null
363     apdfile.unlockFile();
364     return null;
365    }
366   /**
367    * copy data from the appData jar file to an appropriately 
368    * referenced jar or Data entry for the given ApplicationData
369    * Assumes the JarFile is properly closed. 
370    * @param vdoc session Document handler
371    * @param appd the AppData whose block is being updated
372    * @param apdjar the new data in a Jar written by this class
373    */
374   protected void updateAnAppdataEntry(VamsasArchive vdoc, AppData appd, SessionFile apdjar) throws IOException {
375     if (apdjar==null || apdjar.sessionFile==null || !apdjar.sessionFile.exists()) {
376       throw new IOException("No temporary Appdata to recover and transfer.");
377     }
378     if (vdoc==null) {
379       log.fatal("FATAL! NO DOCUMENT TO WRITE TO!");
380       throw new IOException("FATAL! NO DOCUMENT TO WRITE TO!");
381     }
382     log.debug("Recovering AppData entry from "+apdjar.sessionFile);
383     JarInputStream istrm = new JarInputStream(apdjar.getBufferedInputStream(true));
384     JarEntry je=null;
385     while (istrm.available()>0 && (je=istrm.getNextJarEntry())!=null && !je.getName().equals("appData_entry.dat")) {
386       if (je!=null)
387         log.debug("Ignoring extraneous entry "+je.getName());
388     }
389     if (istrm.available()>0 && je!=null) {
390       log.debug("Found appData_entry.dat in Jar");
391       String ref = appd.getDataReference();
392       if (ref==null) {
393         throw new IOException("Null AppData.DataReference passed.");
394       }
395       log.debug("Writing appData_entry.dat as "+ref);
396       if (vdoc.writeAppdataFromStream(ref, istrm)) {
397         log.debug("Entry updated successfully.");
398       } else {
399         throw new IOException("writeAppdataFromStream did not return true - expect future badness."); // LATER - verify why this might occur.
400       }
401     } else {
402       throw new IOException("Couldn't find appData_entry.dat in temporary jar file "+apdjar.sessionFile.getAbsolutePath());
403     }
404     istrm.close();
405   }
406   /* (non-Javadoc)
407    * @see uk.ac.vamsas.client.IClientAppdata#getClientOutputStream()
408    */
409   public AppDataOutputStream getClientOutputStream() {
410     if (clientdoc==null)
411       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
412     if (log.isDebugEnabled())
413       log.debug("trying to getClientOutputStream for "+clientdoc.sclient.client.getClientUrn());   
414     return _getAppdataOutputStream(false);
415   }
416
417   /* (non-Javadoc)
418    * @see uk.ac.vamsas.client.IClientAppdata#getUserOutputStream()
419    */
420   public AppDataOutputStream getUserOutputStream() {
421     if (clientdoc==null)
422       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
423     if (log.isDebugEnabled())
424       log.debug("trying to getUserOutputStream for ("
425           +clientdoc.sclient.getUserHandle().getFullName()+")"+clientdoc.sclient.client.getClientUrn());   
426     return _getAppdataOutputStream(true);
427   }
428
429   /* (non-Javadoc)
430    * @see uk.ac.vamsas.client.IClientAppdata#hasClientAppdata()
431    */
432   public boolean hasClientAppdata() {
433     if (clientdoc==null)
434       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
435     extractAppData(clientdoc.getVamsasDocument());
436     // LATER - check validity of a DataReference before we return true
437     // TODO: return true if usersData is null but we have already written a new data stream 
438     if ((appsGlobal!=null) && (appsGlobal.getDataReference()!=null || appsGlobal.getData()!=null))
439       return true;
440     return false;
441   }
442
443   /* (non-Javadoc)
444    * @see uk.ac.vamsas.client.IClientAppdata#hasUserAppdata()
445    */
446   public boolean hasUserAppdata() {
447     if (clientdoc==null)
448       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
449     extractAppData(clientdoc.getVamsasDocument());
450     // LATER - check validity of a DataReference before we return true
451     // TODO: return true if usersData is null but we have already written a new data stream 
452     if ((usersData!=null) && (usersData.getDataReference()!=null || usersData.getData()!=null))
453       return true;
454     return false;
455   }
456   private boolean _writeAppDataStream(JarOutputStream ostrm, byte[] data) {
457     try {
458       if (data!=null && data.length>0) 
459         ostrm.write(data);
460       ostrm.closeEntry();
461       return true;
462     }
463     catch (Exception e) {
464       log.error("Serious! - IO error when writing AppDataStream to file "+newAppData.sessionFile, e);
465     }
466     return false;
467   }
468   /* (non-Javadoc)
469    * @see uk.ac.vamsas.client.IClientAppdata#setClientAppdata(byte[])
470    */
471   public void setClientAppdata(byte[] data) {
472     if (clientdoc==null)
473       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
474     _getAppdataOutputStream(false);
475     if (newAppDataStream==null) {
476       // LATER: define an exception for this ? - operation may fail even if file i/o not involved
477       log.error("Serious! - couldn't open new AppDataStream in session directory "+clientdoc.sclient._session.sessionDir);
478     } else {
479       _writeAppDataStream(newAppDataStream, data);
480       // LATER: deal with error case - do we make session read only, or what ?
481     }
482   }
483
484   /* (non-Javadoc)
485    * @see uk.ac.vamsas.client.IClientAppdata#setUserAppdata(byte[])
486    */
487   public void setUserAppdata(byte[] data) {
488     if (clientdoc==null)
489       throw new Error("Implementation error, Improperly initialized SimpleClientAppdata.");
490     _getAppdataOutputStream(true);
491     if (newUserDataStream==null) {
492       // LATER: define an exception for this ? - operation may fail even if file i/o not involved
493       log.error("Serious! - couldn't open new UserDataStream in session directory "+clientdoc.sclient._session.sessionDir);
494     } else {
495       _writeAppDataStream(newUserDataStream, data);
496       // LATER: deal with error case - do we make session read only, or what ?
497     }
498   }
499   /**
500    * flush and close outstanding output streams. 
501    *  - do this before checking data length.
502    * @throws IOException
503    */
504   protected void closeForWriting() throws IOException {
505     if (newAppDataStream!=null) {
506       newAppDataStream.flush();
507       newAppDataStream.closeEntry();
508       newAppDataStream.close();
509     }
510     if (newUserDataStream!=null) {
511       newUserDataStream.flush();
512       newUserDataStream.closeEntry();
513       newUserDataStream.close();
514     }
515   }
516
517
518   /**
519    * 
520    * @return true if any AppData blocks have to be updated in session Jar
521    */
522   protected boolean isModified() {
523     // LATER differentiate between core xml modification and Jar Entry modification.
524     if ((newAppData!=null && newAppData.sessionFile.exists()) || (newUserData!=null && newUserData.sessionFile.exists()))
525       return true;
526     return false;
527   }
528   /* (non-Javadoc)
529    * @see java.lang.Object#finalize()
530    */
531   protected void finalize() throws Throwable {
532     if (newAppDataStream!=null) {
533       newAppDataStream = null;
534     }
535     if (newAppDataStream!=null) {
536       newUserDataStream = null;
537     }
538     if (newAppData!=null) {
539       newAppData.eraseExistence();
540       newAppData = null;
541     }
542     if (newUserData!=null) {
543       newUserData.eraseExistence();
544       newUserData = null;
545     }
546     super.finalize();
547   }
548
549 }