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