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