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