safety checks and informational Errors generated when IClientDocument is not stored...
[vamsas.git] / src / uk / ac / vamsas / client / simpleclient / SimpleClient.java
1 /*
2  * Created on 15-Sep-2005
3  *
4  * TODO To change the template for this generated file go to
5  * Window - Preferences - Java - Code Style - Code Templates
6  */
7 package uk.ac.vamsas.client.simpleclient;
8
9 import java.beans.PropertyChangeListener;
10 import java.beans.PropertyChangeSupport;
11 import java.io.File;
12 import java.io.IOException;
13 import java.io.RandomAccessFile;
14 import java.nio.channels.FileChannel;
15
16 import java.nio.channels.OverlappingFileLockException;
17 import java.util.Hashtable;
18 import java.util.Vector;
19
20 import org.apache.commons.logging.Log;
21 import org.apache.commons.logging.LogFactory;
22
23 import uk.ac.vamsas.client.ClientHandle;
24 import uk.ac.vamsas.client.Events;
25 import uk.ac.vamsas.client.IClient;
26 import uk.ac.vamsas.client.IClientDocument;
27 import uk.ac.vamsas.client.IObjectUpdate;
28 import uk.ac.vamsas.client.InvalidSessionUrnException;
29 import uk.ac.vamsas.client.SessionHandle;
30 import uk.ac.vamsas.client.UserHandle;
31 import uk.ac.vamsas.client.picking.IPickManager;
32 import uk.ac.vamsas.objects.core.Entry;
33 import uk.ac.vamsas.objects.core.VamsasDocument;
34 import uk.ac.vamsas.objects.utils.ProvenanceStuff;
35
36 /**
37  * @author jimp
38  */
39 public class SimpleClient implements IClient {
40   
41   private static Log log = LogFactory.getLog(SimpleClient.class);
42   
43   protected UserHandle user = null;
44   
45   protected SessionUrn session = null;
46   protected VamsasSession _session;
47   protected ClientHandle client = null;
48   protected EventGeneratorThread evgen = null;
49   protected ClientDocument cdocument = null;
50   
51   
52   
53   
54   private Lock activeClientFilelock = null;
55   private  File clientlockFile = null;
56   
57   /**
58    * object hash table that persists in each client holding vorbaIds and hash values after a document write
59    */
60   protected Hashtable extantobjects=null;
61   /**
62    * construct a transient IdFactory instance - this should last only as long as the 
63    * SimpleClient object holds the lock on the vamsas document being created/manipulated.
64    * @return
65    */
66   private IdFactory makeVorbaIdFactory() {
67     if (extantobjects==null)
68       extantobjects=new Hashtable();
69     return new IdFactory(getSessionHandle(), client, user, extantobjects);
70   }
71   
72   /**
73    * construct SimpleClient for user, client and VamsasSession directory
74    * use the SimpleClientFactory rather than this constructor directly. 
75    * @param user
76    * @param client
77    * @param sess
78    */
79   protected SimpleClient(UserHandle user, ClientHandle client, VamsasSession sess) throws InvalidSessionUrnException {
80     // TODO: validate user/client/session
81     _session = sess;
82     this.user = user;
83     this.client = client;
84     //try {
85     log.debug("Creating new session for "+_session);
86       session = new SessionUrn(_session);
87       log.debug("Creating new Event Generator");
88       evgen = new EventGeneratorThread(_session, this, handlers);
89       /*} catch (MalformedURLException e) {
90       log.error("Couldn't form a valid SessionUrn object!",e);
91       throw new InvalidSessionUrnException(_session.toString());
92     }*/
93       log.debug("SimpleClient constructed for session "+session.getSessionUrn());
94       
95   }
96   /**
97    * construct new session by importing objects from an existing vamsas document
98    * @param user
99    * @param client
100    * @param sess
101    * @param importingArchive
102    * @throws Exception IOExceptions for Session IO problems, and general Exception if importing document is invalid.
103    */
104   protected SimpleClient(UserHandle user, ClientHandle client, VamsasSession sess, File importingArchive) throws Exception {
105     this(user, client, sess);
106     VamsasArchive sessdoc = _session.getVamsasDocument();
107     try {
108       VamsasArchiveReader odoc = new VamsasArchiveReader(importingArchive);
109       SimpleDocument sdoc = new SimpleDocument(makeVorbaIdFactory());
110       VamsasDocument doc = sdoc.getVamsasDocument(odoc);
111       sessdoc.putVamsasDocument(doc, sdoc.vorba);
112       sessdoc.closeArchive();
113       log.debug("Imported new vamsas data from "+importingArchive);
114     } catch (Exception e) {
115       sessdoc.cancelArchive();
116       // write a dummy iohandler
117       _session.slog.info("Exception when importing document data from "+importingArchive);
118       log.warn("While importing session data from existing archive in "+importingArchive, e);      
119       throw new Exception("Failed to import data from "+importingArchive, e);
120     }
121   }
122   
123   /*
124    * (non-Javadoc)
125    * LATER: check that build substitution variables are correct
126    * @see uk.ac.vamsas.client.IClient#getAbout()
127    */
128   public String getAbout() {
129     return new String("VORBA SimpleClient version $version$ build $build$");
130   }
131   
132   /*
133    * (non-Javadoc)
134    * 
135    * @see uk.ac.vamsas.client.IClient#getSessionUrn()
136    */
137   public String getSessionUrn() {
138     return session.getSessionUrn();
139   }
140   
141   /*
142    * (non-Javadoc)
143    * 
144    * @see uk.ac.vamsas.client.IClient#getSessionHandle()
145    */
146   public SessionHandle getSessionHandle() {
147     // TODO: eliminate SessionHandle ? need to refactor interfaces.
148     SessionHandle sh = new SessionHandle(session.getSessionUrn());
149     return sh;
150   }
151   
152   /*
153    * (non-Javadoc)
154    * 
155    * @see uk.ac.vamsas.client.IClient#getClientHandle()
156    */
157   public ClientHandle getClientHandle() {
158     return client;
159   }
160   
161   /*
162    * (non-Javadoc)
163    * 
164    * @see uk.ac.vamsas.client.IClient#getUserHandle()
165    */
166   public UserHandle getUserHandle() {
167     return user;
168   }
169   /**
170    * 
171    * @return user field for a provenance entry
172    */
173   protected String getProvenanceUser() {
174     return new String(user.getFullName());
175   }
176   /**
177    * construct a provenance entry for this client with the specified action string.
178    * @param action
179    * @return properly completed provenance entry
180    */
181   protected Entry getProvenanceEntry(String action) {
182     Entry prov = ProvenanceStuff.newProvenanceEntry(client.getClientUrn(), getProvenanceUser(), action);
183     return prov;
184   }
185   private Hashtable handlers = initHandlers();
186   
187   private Vector listeners = new Vector();
188   
189   /**
190    * make all the PropertyChangeSupport objects for the
191    * events described in uk.ac.vamsas.client.Event
192    * @return
193    */
194   private static Hashtable initHandlers() {
195     Hashtable events = new Hashtable();
196     java.util.Iterator evt = Events.EventList.iterator();
197     while (evt.hasNext()) {
198       Object ths = evt.next();
199       events.put(ths, (Object) new PropertyChangeSupport(ths));
200     }
201     return events;
202   }
203   
204   /*
205    * (non-Javadoc)
206    * 
207    * @see uk.ac.vamsas.client.IClient#addDocumentUpdateHandler(java.util.EventListener)
208    */
209   public void addDocumentUpdateHandler(PropertyChangeListener evt) {
210     this.addVorbaEventHandler(Events.DOCUMENT_UPDATE, evt);
211   }
212   boolean finalized=false;
213   private void haltPickmanager() {
214     if (pickmanager!=null) {
215       final SimpleClient dying=this; 
216       new Thread() {
217         public void run() {
218           SimpleClient.log.debug("Stopping pickManager..");
219           dying.pickmanager.shutdown();
220           SimpleClient.log.debug("pickManager halted.");
221         }
222       }.start();
223     }
224   }
225   /*
226    * (non-Javadoc)
227    * 
228    * @see uk.ac.vamsas.client.IClient#finalizeClient()
229    */
230   public void finalizeClient() {
231     if (finalized)
232       throw new Error("VAMSAS Client Implementation Error: Finalized called twice for same client instance.");
233
234     // mark this instance as finalized
235     finalized=true;
236     
237     // TODO: determine if this is last client in session
238     
239     // TODO: raise events like : ((lst_client && document.request.to.close), (client_finalization), (
240     evgen._raise(Events.CLIENT_FINALIZATION, null, this,null);
241     // if (handlers.containsKey(Events.))
242     // if (handlers.containsKey(Events.CLIENT_FINALIZATION))
243     // deregister listeners.
244     log.debug("Stopping pickManager");
245     haltPickmanager();
246     
247     log.debug("Deregistering Client");
248     _session.removeClient(this);
249     //log.debug("Stopping EventGenerator..");
250     //evgen.stopWatching();
251     SimpleClient.log.debug("EventGenerator halted.");
252     this.cdocument = null;
253     log.debug("finalization Complete.");
254   }
255   
256   /*
257    * (non-Javadoc)
258    * 
259    * @see uk.ac.vamsas.client.IClient#getClientDocument()
260    */
261   public IClientDocument getClientDocument() throws IOException {
262     log.debug("getClientDocument");
263     if (cdocument!=null) {
264       // cdocument is non-nill if the ClientDocument.finalise() method hasn't been called.
265       return cdocument;
266     }
267     evgen.disableDocumentWatch();
268     VamsasArchive va = null;
269     try {
270       // LATER: bail out if it takes too long to get the lock ?
271       va = _session.getVamsasDocument();
272     }
273     catch (IOException e) {
274       throw new IOException("Failed to get lock on session document");
275     }
276     VamsasDocument doc=null;
277     IdFactory vorba = null;
278     // TODO: LATER: reduce size of vorba ids generated from these parameters to IdFactory (mainly sessionHandle rationalization ?)
279     try {
280       va.setVorba(vorba=makeVorbaIdFactory());
281       // if session currently holds data - read it in - or get a dummy
282       log.debug("Accessing document");
283       doc = 
284         va.getVamsasDocument(getProvenanceUser(),
285             "created new session document.", null);
286       if (doc!=null)
287         log.debug("Successfully retrieved document.");
288       else
289         log.error("Unexpectedly retrieved null document!");
290     }
291     catch (Exception e) {
292       log.error("Failed to get session document for session directory '"+_session.sessionDir+"'", e);
293       throw new IOException("Failed to get session document for session directory '"+_session.sessionDir+"'");
294     }
295     // Construct the IClientDocument instance
296     
297     cdocument = new ClientDocument(doc, va, vorba, this);
298     return cdocument;
299   }
300   
301   /*
302    * (non-Javadoc)
303    * @throws Errors for invalid newdoc parameter
304    * @see uk.ac.vamsas.client.IClient#updateDocument(uk.ac.vamsas.client.IClientDocument)
305    */
306   public void updateDocument(IClientDocument newdoc) {
307     log.debug("updateDocument:");
308     // Check validity of simpleclient instance and that it holds a lock on the session's document
309     if (!(newdoc instanceof ClientDocument)) {
310       throw new Error("Invalid IClientDocument passsed to SimpleClient.");
311     }
312     if (cdocument==null)
313       throw new Error("Client Error - updateDocument() called before getClientDocument() on this SimpleClient instance.");
314     if (newdoc!=cdocument)
315       throw new Error("Client Error - SimpleClient.updateDocument() can only take the IClientDocument instance returned from SimpleClient.getClientDocument()");
316     
317     if (evgen.isDocumentWatchEnabled())
318       throw new Error("Probable Client Error (did you remember to call SimpleClient.updateDocument(clientdoc) at the end of the document update handler?) - or Library Bug : Document watcher still enabled whilst ClientDocument instance exists.");
319     
320     if (!cdocument.isModified()) {
321       // client document is silently got rid of, with no session update events.
322       if (log.isDebugEnabled())
323         log.debug("updateDocument for "+session.getSessionUrn()+" with unmodified IClientDocument (skipping the write)");
324     } else {
325       writeSessionDocument();
326     }
327     tidyAwaySessionDocumentState(); 
328   }
329   /**
330    * garbage collect the ClientDocument instance and re-enable watchers.
331    */
332   protected void tidyAwaySessionDocumentState() {
333     try {
334       log.debug("Finalizing ClientDocument instance.");
335       cdocument.finalize();
336     } catch (Throwable e) {
337       log.error("Exception when trying to garbage collect ClientDocument for "+session.getSessionUrn(), e);
338     }
339     cdocument = null; // this is probably done by finalize
340
341     try {
342       _session.unlockVamsasDocument();
343       evgen.enableDocumentWatch();
344     } catch (IOException e) {
345       log.warn("IO Problems when releasing lock on session document!",e);
346       _session.slog.error("IO problems when attempting to release lock on session document.");
347     }
348   }
349
350   /**
351    * write the cdocument instance to the session for real.
352    */
353   private void writeSessionDocument() {
354     try {
355       boolean updated=cdocument.updateSessionDocument();
356       if (!updated) {
357         log.warn("Session document did not update properly for session directory "+_session.sessionDir);
358         // cdocument.archive.cancelArchive(); // LATER: could be done - would need to prevent updateSessionDocument closing the iohandler.
359         _session.slog.warn("Session Document updated but may not be valid (false return from org.vamsas.simpleclient.ClientDocument.updateSessionDocument()");
360       } else {
361         log.debug("Document update successful.");
362       }
363       _session.setUnsavedFlag();
364     }
365     catch (IOException e) {
366       log.warn("IO Problems when updating document!",e);
367       _session.slog.error("IO problems when attempting to update document.");
368     }
369   }
370
371   /*
372    * (non-Javadoc)
373    * 
374    * @see uk.ac.vamsas.client.IClient#storeDocument(java.io.File)
375    */
376   public void storeDocument(File location) {
377     if (location==null)
378       throw new Error("Vamsas Client API Usage Error: storeDocument called with null location.");
379     log.debug("StoreDocument to "+location);
380     // write storeDocument file to inform other clients that they should raise
381     Lock vamlock = evgen.want_to_store();
382     // Events.DOCUMENT_FINALIZEAPPDATA
383     try {
384       _session.writeVamsasDocument(location, vamlock);
385        _session.clearUnsavedFlag();
386     } catch (Exception e) {
387       log.warn("Exception whilst trying to store document in "+location,e);
388     }
389     vamlock.release();
390   }
391   
392   /*
393    * (non-Javadoc)
394    * 
395    * @see uk.ac.vamsas.client.IClient#addVorbaEventHandler(java.lang.String,
396    *      java.beans.PropertyChangeListener)
397    */
398   public void addVorbaEventHandler(String EventChain, PropertyChangeListener evt) {
399     if (handlers.containsKey(EventChain)) {
400       log.debug("Adding new handler for "+EventChain);
401       Object handler;
402       ((PropertyChangeSupport) (handler = handlers.get(EventChain)))
403       .addPropertyChangeListener(evt);
404       listeners.add(handler);
405       listeners.add((Object) evt);
406     }
407   }
408   
409   /* (non-Javadoc)
410    * @see uk.ac.vamsas.client.IClient#pollUpdate()
411    */
412   public void pollUpdate() {
413     log.debug("pollUpdate");
414     if (evgen==null) {
415       log.warn("pollUpdate called on incomplete SimpleClient object.");
416       return;
417     }
418     
419     if (!evgen.isWatcherAlive()) {
420       log.warn("pollUpdate called before joinSession() - trying to do this.");
421       try {
422         joinSession();
423       } catch (Exception e) {
424         log.error("Unexpected exception on default call to joinSession",e);
425       }
426     }
427     
428     //TODO ensure event generator robustly handles these interrupts.
429     log.debug("interrrupting event generator.");
430     evgen.interruptWatching();
431     log.debug("interrrupted event generator.");
432   }
433   
434   /* (non-Javadoc)
435    * @see uk.ac.vamsas.client.IClient#joinSession()
436    */
437   public void joinSession() throws Exception {
438     log.debug("Joining Session.");
439     // start the EventGenerator thread.
440     if (evgen==null) {
441       log.warn("joinSession called on incomplete SimpleClient object.");
442       return;
443     }
444     if (evgen.isWatcherAlive())
445       throw new Error("Join session called twice for the same SimpleClient (IClient instance).");
446     evgen.startWatching();
447     if (evgen.isWatcherAlive())
448       log.debug("Started EventGenerator thread.");
449     else {
450       log.warn("Failed to start EventGenerator thread.");
451       throw new Exception("Failed to start event generator thread - client cannot be instantiated.");
452     }
453     if (evgen.countHandlersFor(Events.DOCUMENT_CREATE)>0) {
454       //TODO: LATER: is this application connecting to a newly created session document ?
455       //evgen.raise(Events.DOCUMENT_CREATE);
456     }
457     
458   }
459   
460   
461   
462   /* (non-Javadoc)
463    * @see uk.ac.vamsas.client.IClient#importDocument(java.io.File)
464    */
465   public void importDocument(File location) {
466     // TODO LATER: implement SimpleClient.importDocument()
467     // if (numberOfClients<1 or document file is empty/new, then can import directly.)
468     // more complex if data is already present in document. Could have a 'clearSession' method, too, or dump and overwrite instead.
469     log.error("importDocument is not yet implemented for a SimpleClient Session.");
470     
471     /*try {
472       this._session.setVamsasDocument(location);
473     } catch (IOException e) {
474       log.error("importDocument failed.");
475     }*/
476   }
477
478   public IObjectUpdate getUpdateHandler(Class rootObject) {
479     // TODO Auto-generated method stub
480     return null;
481   }
482
483   public IObjectUpdate[] getUpdateHandlers() {
484     // TODO Auto-generated method stub
485     return null;
486   }
487
488   public void removeUpdateHandler(Class rootObject) {
489     // TODO Auto-generated method stub
490     
491   }
492
493   public void setUpdateHandler(IObjectUpdate handler) {
494     // TODO Auto-generated method stub
495     
496   }
497
498   /**
499    * retrieves the current VamsasSession to which belong the client
500    * @return the _session
501    */
502   protected VamsasSession get_session() {
503     return this._session;
504   }
505   SimplePickManager pickmanager=null;
506   /* (non-Javadoc)
507    * @see uk.ac.vamsas.client.IClient#getPickManager()
508    */
509   public IPickManager getPickManager() {
510     createPickManager();
511     return pickmanager;
512   }
513
514   private void createPickManager() {
515     if (pickmanager==null){
516       // TODO: Construct PickManager for session using details from sessionURN!
517       log.debug("Creating PickManager (not from sessionURN yet)");
518       pickmanager = new SimplePickManager(new uk.ac.vamsas.client.picking.SocketManager());
519     }
520   }
521   
522   
523   protected void releaseActiveClientFile() throws IOException
524   {
525    
526     log.debug("Releasing active client locks");
527     if( activeClientFilelock != null)
528     {// Release the lock
529       log.debug("Releasing lock on active client lock file");
530       activeClientFilelock.release();
531       log.debug("ReleaseActiveClientFile called when client has no lock on its clientLockFile");
532       activeClientFilelock = null;
533     } else {
534       log.debug("ReleaseActiveClientFile called when client has no lock on its clientLockFile");
535     }
536     if (this.clientlockFile != null)
537     {
538       log.debug("trying to delete active client lock file");
539       if (this.clientlockFile.exists())
540       {
541         this.clientlockFile.delete();
542         log.debug("deleted active client lock file");
543       }
544     } else {
545       log.debug("ReleaseActiveClientFile called when client has no clientLockFile");
546     }
547   }
548   
549   protected void  createActiveClientFile() throws IOException
550   {
551     if(this.clientlockFile != null )return; 
552    log.debug("createActiveClientFile");
553     //create, if need,  subdirectory to contain client files
554    File clientlockFileDir = new File ( this.get_session().sessionDir, this.get_session().clientFileDirectory);
555     if( !clientlockFileDir.exists())
556       {//the directory does not exist, create it
557         if (! clientlockFileDir.mkdirs())
558         {
559           throw new IOException("Failed to create sub directory to session directory  for client lock files'"+clientlockFileDir.getAbsolutePath()+"'");
560         }
561       }
562     else
563     {
564       if (!(clientlockFileDir.isDirectory() && clientlockFileDir.canWrite()))
565       {
566         throw new IOException("Directory  for client lock files is not a directory or is not accessibl: '"+clientlockFileDir.getAbsolutePath()+"'");
567        }
568     }
569     this.clientlockFile = new File (clientlockFileDir, this.getClientHandle().getClientUrn().replaceAll("[:;/\\\\]+",""));
570     
571     log.debug("Creating active client lock file "+ this.clientlockFile.getAbsolutePath());
572     Lock clientLock = uk.ac.vamsas.client.simpleclient.LockFactory.getLock(clientlockFile, false);
573     if (clientLock==null || !clientLock.isLocked())
574     {
575       log.fatal("IMPLEMENTATION ERROR: Couldn't get a lock for the client lock file "+clientlockFile);
576     }
577     activeClientFilelock = clientLock;
578   }
579   /**
580    * @return the clientlockFile
581    */
582   protected File getClientlockFile() {
583     return clientlockFile;
584   }
585   /**
586    * 
587    * @return the lock for the client in the session
588    */
589   protected Lock getClientLock() {
590     return activeClientFilelock;
591   }
592
593 }