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