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