/* * This file is part of the Vamsas Client version 0.1. * Copyright 2009 by Jim Procter, Iain Milne, Pierre Marguerite, * Andrew Waterhouse and Dominik Lindner. * * Earlier versions have also been incorporated into Jalview version 2.4 * since 2008, and TOPALi version 2 since 2007. * * The Vamsas Client is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The Vamsas Client is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the Vamsas Client. If not, see . */ package uk.ac.vamsas.client.simpleclient; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.OverlappingFileLockException; import java.util.Hashtable; import java.util.Vector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import uk.ac.vamsas.client.ClientHandle; import uk.ac.vamsas.client.Events; import uk.ac.vamsas.client.IClient; import uk.ac.vamsas.client.IClientDocument; import uk.ac.vamsas.client.IObjectUpdate; import uk.ac.vamsas.client.InvalidSessionDocumentException; import uk.ac.vamsas.client.InvalidSessionUrnException; import uk.ac.vamsas.client.SessionHandle; import uk.ac.vamsas.client.UserHandle; import uk.ac.vamsas.client.picking.IPickManager; import uk.ac.vamsas.objects.core.Entry; import uk.ac.vamsas.objects.core.VamsasDocument; import uk.ac.vamsas.objects.utils.ProvenanceStuff; /** * @author jimp */ public class SimpleClient implements IClient { private static Log log = LogFactory.getLog(SimpleClient.class); protected UserHandle user = null; protected SessionUrn session = null; protected VamsasSession _session; protected ClientHandle client = null; protected EventGeneratorThread evgen = null; protected ClientDocument cdocument = null; private Lock activeClientFilelock = null; private File clientlockFile = null; /** * object hash table that persists in each client holding vorbaIds and hash * values after a document write */ protected Hashtable extantobjects = null; /** * construct a transient IdFactory instance - this should last only as long as * the SimpleClient object holds the lock on the vamsas document being * created/manipulated. * * @return */ private IdFactory makeVorbaIdFactory() { if (extantobjects == null) extantobjects = new Hashtable(); return new IdFactory(getSessionHandle(), client, user, extantobjects); } /** * construct SimpleClient for user, client and VamsasSession directory use the * SimpleClientFactory rather than this constructor directly. * * @param user * @param client * @param sess */ protected SimpleClient(UserHandle user, ClientHandle client, VamsasSession sess) throws InvalidSessionUrnException { // TODO: validate user/client/session _session = sess; this.user = user; this.client = client; // try { log.debug("Creating new session for " + _session); session = new SessionUrn(_session); log.debug("Creating new Event Generator"); evgen = new EventGeneratorThread(_session, this, handlers); /* * } catch (MalformedURLException e) { * log.error("Couldn't form a valid SessionUrn object!",e); throw new * InvalidSessionUrnException(_session.toString()); } */ log .debug("SimpleClient constructed for session " + session.getSessionUrn()); } /** * construct new SimpleClientsession by importing objects from an existing * vamsas document * * @param user * @param client * @param sess * @param importingArchive * @throws Exception * IOExceptions for Session IO problems, and general Exception if * importing document is invalid. protected SimpleClient(UserHandle * user, ClientHandle client, VamsasSession sess, File * importingArchive) throws Exception { this(user, client, sess); if * (log.isDebugEnabled()) { * log.debug("Attempting to overwrite session document with file: " * +importingArchive); } // TODO: write provenance entry for new * session indicating the import. * * } */ /* * (non-Javadoc) LATER: check that build substitution variables are correct * * @see uk.ac.vamsas.client.IClient#getAbout() */ public String getAbout() { return new String("VORBA SimpleClient version $version$ build $build$"); } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#getSessionUrn() */ public String getSessionUrn() { return session.getSessionUrn(); } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#getSessionHandle() */ public SessionHandle getSessionHandle() { // TODO: eliminate SessionHandle ? need to refactor interfaces. SessionHandle sh = new SessionHandle(session.getSessionUrn()); return sh; } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#getClientHandle() */ public ClientHandle getClientHandle() { return client; } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#getUserHandle() */ public UserHandle getUserHandle() { return user; } /** * * @return user field for a provenance entry */ protected String getProvenanceUser() { return new String(user.getFullName()); } /** * construct a provenance entry for this client with the specified action * string. * * @param action * @return properly completed provenance entry */ protected Entry getProvenanceEntry(String action) { Entry prov = ProvenanceStuff.newProvenanceEntry(client.getClientUrn(), getProvenanceUser(), action); return prov; } private Hashtable handlers = initHandlers(); private Vector listeners = new Vector(); /** * make all the PropertyChangeSupport objects for the events described in * uk.ac.vamsas.client.Event * * @return */ private static Hashtable initHandlers() { Hashtable events = new Hashtable(); java.util.Iterator evt = Events.EventList.iterator(); while (evt.hasNext()) { Object ths = evt.next(); events.put(ths, (Object) new PropertyChangeSupport(ths)); } return events; } /* * (non-Javadoc) * * @see * uk.ac.vamsas.client.IClient#addDocumentUpdateHandler(java.util.EventListener * ) */ public void addDocumentUpdateHandler(PropertyChangeListener evt) { this.addVorbaEventHandler(Events.DOCUMENT_UPDATE, evt); } boolean finalized = false; private void haltPickmanager() { if (pickmanager != null) { final SimpleClient dying = this; new Thread() { public void run() { SimpleClient.log.debug("Stopping pickManager.."); dying.pickmanager.shutdown(); SimpleClient.log.debug("pickManager halted."); } }.start(); } } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#finalizeClient() */ public void finalizeClient() { if (finalized) throw new Error( "VAMSAS Client Implementation Error: Finalized called twice for same client instance."); // mark this instance as finalized finalized = true; // TODO: determine if this is last client in session // TODO: raise events like : ((lst_client && document.request.to.close), // (client_finalization), ( evgen._raise(Events.CLIENT_FINALIZATION, null, this, null); // if (handlers.containsKey(Events.)) // if (handlers.containsKey(Events.CLIENT_FINALIZATION)) // deregister listeners. log.debug("Stopping pickManager"); haltPickmanager(); log.debug("Deregistering Client"); _session.removeClient(this); // log.debug("Stopping EventGenerator.."); // evgen.stopWatching(); this.cdocument = null; SimpleClient.log.debug("EventGenerator halted."); log.debug("finalization Complete."); } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#getClientDocument() */ public IClientDocument getClientDocument() throws IOException { log.debug("getClientDocument"); if (cdocument != null) { // cdocument is non-nill if the ClientDocument.finalise() method hasn't // been called. return cdocument; } evgen.disableDocumentWatch(); VamsasArchive va = null; try { // LATER: bail out if it takes too long to get the lock ? va = _session.getVamsasDocument(); } catch (IOException e) { throw new IOException("Failed to get lock on session document"); } VamsasDocument doc = null; IdFactory vorba = null; // TODO: LATER: reduce size of vorba ids generated from these parameters to // IdFactory (mainly sessionHandle rationalization ?) try { va.setVorba(vorba = makeVorbaIdFactory()); // if session currently holds data - read it in - or get a dummy log.debug("Accessing document"); doc = va.getVamsasDocument(getProvenanceUser(), "created new session document.", null); if (doc != null) log.debug("Successfully retrieved document."); else log.error("Unexpectedly retrieved null document!"); } catch (Exception e) { log.error("Failed to get session document for session directory '" + _session.sessionDir + "'", e); throw new IOException( "Failed to get session document for session directory '" + _session.sessionDir + "'"); } // Construct the IClientDocument instance cdocument = new ClientDocument(doc, va, vorba, this); return cdocument; } /* * (non-Javadoc) * * @throws Errors for invalid newdoc parameter * * @see * uk.ac.vamsas.client.IClient#updateDocument(uk.ac.vamsas.client.IClientDocument * ) */ public void updateDocument(IClientDocument newdoc) { log.debug("updateDocument:"); // Check validity of simpleclient instance and that it holds a lock on the // session's document if (!(newdoc instanceof ClientDocument)) { throw new Error("Invalid IClientDocument passsed to SimpleClient."); } if (cdocument == null) throw new Error( "Client Error - updateDocument() called before getClientDocument() on this SimpleClient instance."); if (newdoc != cdocument) throw new Error( "Client Error - SimpleClient.updateDocument() can only take the IClientDocument instance returned from SimpleClient.getClientDocument()"); if (evgen.isDocumentWatchEnabled()) 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."); if (cdocument.isInvalidModification()) { log .info("Client has corrupted the vamsas document. It will not be written back to the session - sorry."); // TODO: modify updateDocument signature: We should really raise an // exception here to tell the client that it broke the datamodel. } else { // actually try to write - if necessary. if (!cdocument.isModified()) { // client document is silently got rid of, with no session update // events. if (log.isDebugEnabled()) log.debug("updateDocument for " + session.getSessionUrn() + " with unmodified IClientDocument (skipping the write)"); } else { writeSessionDocument(); } } // release locks, reset and start to receive events again tidyAwaySessionDocumentState(); } /** * garbage collect the ClientDocument instance and re-enable watchers. */ protected void tidyAwaySessionDocumentState() { try { log.debug("Finalizing ClientDocument instance."); cdocument.finalize(); } catch (Throwable e) { log.error("Exception when trying to garbage collect ClientDocument for " + session.getSessionUrn(), e); } cdocument = null; // this is probably done by finalize try { _session.unlockVamsasDocument(); evgen.enableDocumentWatch(); } catch (IOException e) { log.warn("IO Problems when releasing lock on session document!", e); _session.slog .error("IO problems when attempting to release lock on session document."); } } /** * write the cdocument instance to the session for real. */ private void writeSessionDocument() { try { boolean updated = cdocument.updateSessionDocument(); boolean hasContent = cdocument.isModified(); if (!updated) { log .warn("Session document did not update properly for session directory " + _session.sessionDir); // cdocument.archive.cancelArchive(); // LATER: could be done - would // need to prevent updateSessionDocument closing the iohandler. _session.slog .warn("Session Document updated but may not be valid (false return from org.vamsas.simpleclient.ClientDocument.updateSessionDocument()"); } else { log.debug("Document update successful."); } if (hasContent) { _session.setUnsavedFlag(); } } catch (IOException e) { log.warn("IO Problems when updating document!", e); _session.slog.error("IO problems when attempting to update document."); } } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#storeDocument(java.io.File) */ public void storeDocument(File location) { if (location == null) throw new Error( "Vamsas Client API Usage Error: storeDocument called with null location."); log.debug("StoreDocument to " + location); // write storeDocument file to inform other clients that they should raise Lock vamlock = evgen.want_to_store(); // Events.DOCUMENT_FINALIZEAPPDATA try { _session.writeVamsasDocument(location, vamlock); _session.clearUnsavedFlag(); } catch (Exception e) { log.warn("Exception whilst trying to store document in " + location, e); } vamlock.release(); } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#addVorbaEventHandler(java.lang.String, * java.beans.PropertyChangeListener) */ public void addVorbaEventHandler(String EventChain, PropertyChangeListener evt) { if (handlers.containsKey(EventChain)) { log.debug("Adding new handler for " + EventChain); Object handler; ((PropertyChangeSupport) (handler = handlers.get(EventChain))) .addPropertyChangeListener(evt); listeners.add(handler); listeners.add((Object) evt); } } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#pollUpdate() */ public void pollUpdate() { log.debug("pollUpdate"); if (evgen == null) { log.warn("pollUpdate called on incomplete SimpleClient object."); return; } if (!evgen.isWatcherAlive()) { log.warn("pollUpdate called before joinSession() - trying to do this."); try { joinSession(); } catch (Exception e) { log.error("Unexpected exception on default call to joinSession", e); } } // TODO ensure event generator robustly handles these interrupts. log.debug("interrrupting event generator."); evgen.interruptWatching(); log.debug("interrrupted event generator."); } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#joinSession() */ public void joinSession() throws Exception { log.debug("Joining Session."); // start the EventGenerator thread. if (evgen == null) { log.warn("joinSession called on incomplete SimpleClient object."); return; } if (evgen.isWatcherAlive()) throw new Error( "Join session called twice for the same SimpleClient (IClient instance)."); evgen.startWatching(); if (evgen.isWatcherAlive()) log.debug("Started EventGenerator thread."); else { log.warn("Failed to start EventGenerator thread."); throw new Exception( "Failed to start event generator thread - client cannot be instantiated."); } if (evgen.countHandlersFor(Events.DOCUMENT_CREATE) > 0) { // TODO: LATER: is this application connecting to a newly created session // document ? // evgen.raise(Events.DOCUMENT_CREATE); } } /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#importDocument(java.io.File) */ public void importDocument(File location) { // TODO LATER: implement SimpleClient.importDocument() // if (numberOfClients<1 or document file is empty/new, then can import // directly.) // more complex if data is already present in document. Could have a // 'clearSession' method, too, or dump and overwrite instead. log .error("importDocument is not yet implemented for a SimpleClient Session."); /* * try { this._session.setVamsasDocument(location); } catch (IOException e) * { log.error("importDocument failed."); } */ } public IObjectUpdate getUpdateHandler(Class rootObject) { // TODO Auto-generated method stub return null; } public IObjectUpdate[] getUpdateHandlers() { // TODO Auto-generated method stub return null; } public void removeUpdateHandler(Class rootObject) { // TODO Auto-generated method stub } public void setUpdateHandler(IObjectUpdate handler) { // TODO Auto-generated method stub } /** * retrieves the current VamsasSession to which belong the client * * @return the _session */ protected VamsasSession get_session() { return this._session; } SimplePickManager pickmanager = null; /* * (non-Javadoc) * * @see uk.ac.vamsas.client.IClient#getPickManager() */ public IPickManager getPickManager() { createPickManager(); return pickmanager; } private void createPickManager() { if (pickmanager == null) { // TODO: Construct PickManager for session using details from sessionURN! log.debug("Creating PickManager (not from sessionURN yet)"); pickmanager = new SimplePickManager( new uk.ac.vamsas.client.picking.SocketManager()); } } protected void releaseActiveClientFile() throws IOException { log.debug("Releasing active client locks"); if (activeClientFilelock != null) {// Release the lock log.debug("Releasing lock on active client lock file"); activeClientFilelock.release(); log .debug("ReleaseActiveClientFile called when client has no lock on its clientLockFile"); activeClientFilelock = null; } else { log .debug("ReleaseActiveClientFile called when client has no lock on its clientLockFile"); } if (this.clientlockFile != null) { log.debug("trying to delete active client lock file"); if (this.clientlockFile.exists()) { this.clientlockFile.delete(); log.debug("deleted active client lock file"); } } else { log .debug("ReleaseActiveClientFile called when client has no clientLockFile"); } } protected void createActiveClientFile() throws IOException { if (this.clientlockFile != null) return; log.debug("createActiveClientFile"); // create, if need, subdirectory to contain client files File clientlockFileDir = new File(this.get_session().sessionDir, this .get_session().clientFileDirectory); if (!clientlockFileDir.exists()) {// the directory does not exist, create it if (!clientlockFileDir.mkdirs()) { throw new IOException( "Failed to create sub directory to session directory for client lock files'" + clientlockFileDir.getAbsolutePath() + "'"); } } else { if (!(clientlockFileDir.isDirectory() && clientlockFileDir.canWrite())) { throw new IOException( "Directory for client lock files is not a directory or is not accessibl: '" + clientlockFileDir.getAbsolutePath() + "'"); } } this.clientlockFile = new File(clientlockFileDir, this.getClientHandle() .getClientUrn().replaceAll("[:;/\\\\]+", "")); log.debug("Creating active client lock file " + this.clientlockFile.getAbsolutePath()); Lock clientLock = uk.ac.vamsas.client.simpleclient.LockFactory.getLock( clientlockFile, false); if (clientLock == null || !clientLock.isLocked()) { log .fatal("IMPLEMENTATION ERROR: Couldn't get a lock for the client lock file " + clientlockFile); } activeClientFilelock = clientLock; } /** * @return the clientlockFile */ protected File getClientlockFile() { return clientlockFile; } /** * * @return the lock for the client in the session */ protected Lock getClientLock() { return activeClientFilelock; } SimpleClientConfig _config = null; public SimpleClientConfig getSimpleClientConfig() { if (_config == null) { _config = new SimpleClientConfig(); } return _config; } }