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