package uk.ac.vamsas.client.simpleclient; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.OverlappingFileLockException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.FileAppender; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import uk.ac.vamsas.client.ClientHandle; import uk.ac.vamsas.client.Events; import uk.ac.vamsas.client.IClient; import uk.ac.vamsas.client.SessionHandle; import uk.ac.vamsas.client.UserHandle; /** * Does all the IO operations for a SimpleClient instance accessing * a SimpleClient vamsas session. * * Basically, it defines the various standard names for the files * in the session directory (that maps to the sessionUrn), * provides constructors for the file handlers and watchers of * those file entities, and some higher level methods * to check and change the state flags for the session. * * TODO: move the stuff below to the SimpleClientFactory documentation. * much may not be valid now : * Vamsas client is intialised with a path to create live session directories. * This path may contain a vamsas.properties file * that sets additional parameters (otherwise client * just uses the one on the classpath). * * A vamsas session consists of : * SessionDir - translates to urn of a live session. * Contains: Vamsas Document (as a jar), Session client list file, * both of which may be locked, and additional * temporary versions of these files when write * operations are taking place. * * Zip file entries * - vamsasdocument.xml : core info * one or more: * - .version.sessionnumber.raw (string given in vamsasdocument.xml applicationData entry) * * Lockfile * - filename given in the vamsasdocument.xml. Should be checked for validity by any client and rewritten if necessary. * The lockfile can point to the jar itself. * Mode of operation. * Initially - documentHandler either: * - creates a zip for a new session for the client * - connect to an existing session zip * 1. reads session urn file * 2. waits for lock * 3. examines session - decide whether to create new application data slice or connect to one stored in session. * 4. writes info into session file * 5. releases lock and generates local client events. * 6. Creates Watcher thread to generate events. * * During the session * - Update watcher checks for file change - * * Procedures for file based session message exchange * - session document modification flag * */ public class VamsasSession { /** * indicator file for informing other processes that * they should finalise their vamsas datasets for * storing into a vamsas archive. */ public static final String CLOSEANDSAVE_FILE="stored.log"; /** * session file storing the last_stored_stat data */ public static final String MODIFIEDDOC_FILE="modified"; private SimpleSessionManager sessionManager = null; /** * Count of cycles before considering the current client as the last one of the session (if no other client registered as active ) */ private final int watchCycleCountBeforeLastClient = 1220 ; /** * time between checking */ public int WATCH_SLEEP=30; protected String clientFileDirectory = "clients"; /** * called to clear update flag after a successful offline storage event */ protected void clearUnsavedFlag() { SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE)); if (!laststored.clearFlag()) log.warn("Unsaved flag was not cleared for "+sessionDir); } /** * called to indicate session document has been modified. * */ protected void setUnsavedFlag() { SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE)); if (!laststored.setFlag()) log.warn("Couldn't set the Unsaved flag for "+sessionDir); } /** * * @return true if session document has been modified since last offline storage event */ protected boolean getUnsavedFlag() { SessionFlagFile laststored = new SessionFlagFile(new File(sessionDir, MODIFIEDDOC_FILE)); return laststored.checkFlag(); } /** * log file location */ public static final String SESSION_LOG="Log.txt"; private static Log log = LogFactory.getLog(VamsasSession.class); protected Logger slog = Logger.getLogger("uk.ac.vamsas.client.SessionLog"); /** * setup the sessionLog using Log4j. * @throws IOException */ private void initLog() throws IOException { // TODO: fix session event logging // LATER: make dedicated appender format for session log. /*Appender app = slog.getAppender("log4j.appender.SESSIONLOG"); // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath())); // slog.addAppender(new FileAppender(app.getLayout(), new File(sessionDir, SESSION_LOG).getAbsolutePath())); for (Enumeration e = slog.getAllAppenders() ; e.hasMoreElements() ;) { System.out.println(e.nextElement()); }*/ if (slog!= null ) { File sessionLogFile = new File(this.sessionDir, SESSION_LOG); slog.addAppender(new FileAppender(new PatternLayout("%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(), true)); } else { log.info("No appender for SessionLog"); } } /** * the sessionDir is given as the session location for new clients. */ protected File sessionDir; /** * holds the list of attached clients */ ClientsFile clist; public static final String CLIENT_LIST="Clients.obj"; /** * holds the data */ VamsasFile vamArchive; public static final String VAMSAS_OBJ="VamDoc.jar"; /** * sets up the vamsas session files and watchers in sessionDir * @param sessionDir1 */ protected VamsasSession(File sessionDir1) throws IOException { if (sessionDir1==null) throw new Error("Null directory for VamsasSession."); if (sessionDir1.exists()) { if (!sessionDir1.isDirectory() || !sessionDir1.canWrite() || !sessionDir1.canRead()) throw new IOException("Cannot access '"+sessionDir1+"' as a read/writable Directory."); if (!checkSessionFiles(sessionDir1)) log.warn("checkSessionFiles() returned false. Possible client implementation error"); this.sessionDir = sessionDir1; initSessionObjects(); slog.debug("Initialising additional VamsasSession instance"); log.debug("Attached to VamsasSession in "+sessionDir1); //} } else { // start from scratch if (!sessionDir1.mkdir()) throw new IOException("Failed to make VamsasSession directory in "+sessionDir1); createSessionFiles(); initSessionObjects(); slog.debug("Session directory created."); log.debug("Initialised VamsasSession in "+sessionDir1); } } /** * tests presence of existing sessionfiles files in dir * @param dir * @return */ private boolean checkSessionFiles(File dir) throws IOException { File c_file = new File(dir,CLIENT_LIST); File v_doc = new File(dir,VAMSAS_OBJ); if (c_file.exists() && v_doc.exists()) return true; return false; } /** * create new empty files in dir * */ private void createSessionFiles() throws IOException { if (sessionDir==null) throw new IOException("Invalid call to createSessionFiles() with null sessionDir"); File c_file = new File(sessionDir,CLIENT_LIST); File v_doc = new File(sessionDir,VAMSAS_OBJ); if (!c_file.exists() && c_file.createNewFile()) log.debug("Created new ClientFile "+c_file); // don't care if this works or not if (!v_doc.exists() && v_doc.createNewFile()) log.debug("Created new Vamsas Session Document File "+v_doc); } /** * construct SessionFile objects and watchers for each */ private void initSessionObjects() throws IOException { createSessionFiles(); if (clist!=null || vamArchive!=null) throw new IOException("initSessionObjects called for initialised VamsasSession object."); clist = new ClientsFile(new File(sessionDir,CLIENT_LIST)); vamArchive = new VamsasFile(new File(sessionDir,VAMSAS_OBJ)); storedocfile=new ClientsFile(new File(sessionDir, CLOSEANDSAVE_FILE)); initLog(); } /** * make a new watcher object for the clientFile * @return new ClientFile watcher instance */ public FileWatcher getClientWatcher() { return new FileWatcher(clist.sessionFile); } /** * make a new watcher object for the vamsas Document * @return new ClientFile watcher instance */ public FileWatcher getDocWatcher() { return new FileWatcher(vamArchive.sessionFile); } FileWatcher store_doc_file=null; public ClientsFile storedocfile=null; /** * make a new watcher object for the messages file * @return new watcher instance */ public FileWatcher getStoreWatcher() { return new FileWatcher(new File(sessionDir,CLOSEANDSAVE_FILE)); } /** * write to the StoreWatcher file to indicate that a storeDocumentRequest has been made. * The local client's storeWatcher FileWatcher object is updated so the initial change is not registered. * @param client * @param user * @return */ public void addStoreDocumentRequest(ClientHandle client, UserHandle user) throws IOException { // TODO: replace this with clientsFile mechanism SessionFile sfw = new SessionFile(new File(sessionDir, CLOSEANDSAVE_FILE)); while (!sfw.lockFile()) log.debug("Trying to get lock for "+CLOSEANDSAVE_FILE); RandomAccessFile sfwfile=sfw.fileLock.getRaFile(); sfwfile.setLength(0); // wipe out any old info. // TODO: rationalise what gets written to this file (ie do we want other clients to read the id of the requestor?) sfwfile.writeUTF(client.getClientUrn()+":"+user.getFullName()+"@"+user.getOrganization()); sfw.unlockFile(); if (store_doc_file!=null) store_doc_file.setState(); slog.info("FinalizeAppData request from "+user.getFullName()+" using "+client.getClientUrn()+""); } /** * create a new session with an existing vamsas Document - by copying it into the session. * @param archive */ public void setVamsasDocument(File archive) throws IOException { log.debug("Transferring vamsas data from "+archive+" to session:"+vamArchive.sessionFile); SessionFile xtantdoc = new SessionFile(archive); vamArchive.updateFrom(null, xtantdoc); // LATER: decide if session archive provenance should be updated to reflect access. // TODO: soon! do a proper import objects from external file log.debug("Transfer complete."); } /** * write session as a new vamsas Document (this will overwrite any existing file without warning) * TODO: test * TODO: verify that lock should be released for vamsas document. * @param destarchive */ protected void writeVamsasDocument(File destarchive, Lock extlock) throws IOException { log.debug("Transferring vamsas data from "+vamArchive.sessionFile+" to session:"+destarchive); SessionFile newdoc = new SessionFile(destarchive); if (extlock==null && !vamArchive.lockFile()) while (!vamArchive.lockFile()) log.info("Trying to get lock for "+vamArchive.sessionFile); // TODO: LATER: decide if a provenance entry should be written in the exported document recording the export from the session newdoc.updateFrom(extlock, vamArchive); // LATER: LATER: fix use of updateFrom for file systems where locks cannot be made (because they don't have a lockManager, ie NFS/Unix, etc). vamArchive.unLock(); newdoc.unlockFile(); log.debug("Transfer complete."); } /** * extant archive IO handler */ VamsasArchive _va=null; /** * Creates a VamsasArchive Vobject for accessing and updating document * Note: this will lock the Vamsas Document for exclusive access to the client. * @return session vamsas document * @throws IOException if locks fail or vamsas document read fails. */ protected VamsasArchive getVamsasDocument() throws IOException { // check we haven't already done this once - probably should be done by caller if (_va!=null) return _va; // patiently wait for a lock on the document. (from ArchiveClient.getUpdateable()) long tries=5000; while (vamArchive.getLock()==null && --tries>0) { // Thread.sleep(1); log.debug("Trying to get a document lock for the "+tries+"'th time."); } if (tries==0) throw new IOException("Failed to get lock for vamsas archive."); VamsasArchive va = new VamsasArchive(vamArchive.sessionFile, false, true, vamArchive); return va; } /** * Unlocks the vamsas archive session document after it has been closed. * @throws IOException */ protected void unlockVamsasDocument() throws IOException { if (_va!=null) _va.closeArchive(); _va=null; if (vamArchive!=null) vamArchive.unLock(); } /** * create a uniquely named uk.ac.vamsas.client.simpleclient.ClientsFile.addClient(ClientHandle)ile in the session Directory * @see java.io.File.createTempFile * @param pref Prefix for name * @param suff Suffix for name * @return SessionFile object configured for the new file (of length zero) * @throws IOException */ protected SessionFile getTempSessionFile(String pref, String suff) throws IOException { File tfile = File.createTempFile(pref,suff,sessionDir); SessionFile tempFile = new SessionFile(tfile); return tempFile; } /** * add a IClient to the session * * add the client to the client list file * @param client client to add to the session */ protected void addClient(SimpleClient client) { if (client == null) slog.error("Try to add a null client to the session "); else { log.debug("Adding client "+client.getClientHandle().getClientUrn()); getClientWatcherElement().haltWatch(); clist.addClient(client.getClientHandle()); log.debug("Added."); log.debug("Register Client as Active."); try { client.createActiveClientFile(); } catch (IOException e) { log.debug("Error during active client file creation."); } //tracks modification to the client list and readds client to the list getClientWatcherElement().setHandler(new AddClientWatchCallBack(client)); getClientWatcherElement().enableWatch(); } } /** * Handler for the client watcher. * * If (the current client is not in the client list, it is added again;) */ private class AddClientWatchCallBack implements WatcherCallBack { private SimpleClient client ; /** *Inits the handler with the client to check in the list * @param client client to monitor in the client list */ protected AddClientWatchCallBack (SimpleClient client) { this.client = client; } /** * If the client list is modified, checks if the current is still in the list. otherwise, readds ti. * @return true to enable watcher, or false to disable it in future WatcherThread cycles. */ public boolean handleWatchEvent(WatcherElement watcher, Lock lock) { boolean isWatchEnable = watcher.isWatchEnabled(); if (lock== null)//no update on the list return isWatchEnable; log.debug("change on the client list "); if (client != null) { //checks if the client is not already in the lists ClientHandle[] cl = clist.retrieveClientList(lock);//clist.retrieveClientList(); boolean found = false; if (cl != null) { for (int chi = cl.length-1; !found && chi > -1; chi--) { found = cl[chi].equals(this.client.getClientHandle()); } } if (! found) {log.debug("client not in the list "); if( log.isDebugEnabled()) log.debug("the client has not been found in the list. Adding it again :"+cl); addClient(client); } else log.debug("client is in the list"); } log.debug("isWatchEnable "+isWatchEnable); return isWatchEnable; } } /** * * removes a client from the current session * removes the client from the session client list * if the client is the last one from the session (ClientList), the current session is removed * from active session list. * * The active should add them self to the client list. To insure to close the session,when the current client is the lact active client, * clears the list of clients and when two cycles to insure there is no more active client, that otherwise would have readd themself to the list * * @param client client to remove */ protected void removeClient(SimpleClient client)//IClient client) { if (client == null) { log.error("Null client passed to removeClient"); return; } ClientSessionFileWatcherElement cwe=getClientWatcherElement(); if (cwe!=null && cwe.isWatchEnabled()) { cwe.haltWatch(); }; //set handler to check is the the last active client of the session //Wait for several watchers cycle to see if the current client was the last client active in the session. //if yes, close the session // getClientWatcherElement().setHandler(new RemoveClientWatchCallBack (client)); getClientWatcherElement().setTimeoutBeforeLastCycle(this.watchCycleCountBeforeLastClient); log.info("remove client from list"); clist.clearList(); log.info("list cleared"); if (cwe!=null) { cwe.enableWatch(); log.debug("Stopping EventGenerator.."); client.evgen.stopWatching(); cwe.setHandler(null); // ask to the client to copy application data into the document client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null); boolean closeSession=isLastActiveClient(client); if (closeSession) { log.debug("Raising request-to-save event"); client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client, null); client.evgen._raise(Events.SESSION_SHUTDOWN, null, client.getSessionHandle(), null); } try { log.debug("Attempting to release active client locks"); client.releaseActiveClientFile(); } catch (IOException e) { log.error("error during active file client release"); } if (closeSession) { log.debug("Last active client: closing session"); log.info("Closing session"); getSessionManager().removeSession(client.getSessionHandle()); } } } private boolean isLastActiveClient(SimpleClient client) { log.debug("Testing if current client is the last one."); log.debug("current client lockfile is '"+client.getClientlockFile()+"'"); boolean noOtherActiveClient = true; //create, if need, subdirectory to contain client files File clientlockFileDir = new File (this.sessionDir, clientFileDirectory); if( !clientlockFileDir.exists()) { log.error("Something wrong the active client file does not exits... should not happen"); return false; } try { //no check every file in the directory and try to get lock on it. File [] clientFiles = clientlockFileDir.listFiles(); if(clientFiles == null || clientFiles.length==0) {//there is not file on the directory. the current client should be the last one. return true; } for (int i = clientFiles.length - 1; i>-1&& noOtherActiveClient ;i--) { File clientFile = clientFiles[i]; log.debug("testing file for lock: "+clientFile.getAbsolutePath()); if(client.getClientLock().isTargetLockFile(clientFile)) { log.debug("current client file found"); continue; } if (clientFile != null && clientFile.exists() ) { try { log.debug("Try to acquire a lock on the file"); // Get a file channel for the file FileChannel channel = new RandomAccessFile(clientFile, "rw").getChannel(); // Use the file channel to create a lock on the file. // This method blocks until it can retrieve the lock. // java.nio.channels.FileLock activeClientFilelock = channel.lock(); // Try acquiring the lock without blocking. This method returns // null or throws an exception if the file is already locked. try { java.nio.channels.FileLock activeClientFilelock = channel.tryLock(); //the lock has been acquired. //the file was not lock and so the corresponding application seems to have die if(activeClientFilelock != null) { log.debug("lock obtained : file must be from a crashed application"); activeClientFilelock.release(); log.debug("lock released"); channel.close(); log.debug("channel closed"); //delete file clientFile.delete(); log.debug("crashed application file deleted"); } else { noOtherActiveClient = false; log.debug("lock not obtained : another application is active"); } } catch (OverlappingFileLockException e) { // File is already locked in this thread or virtual machine //that the expected behaviour log.debug("lock not accessible ",e); } } catch (Exception e) { log.debug("error during lock testing ",e); } } } } catch (Exception e) { log.error("error during counting active clients"); } return noOtherActiveClient; } /** * Handler for the client watcher. after a client have been removed * * Checks if the client is not the last active one. * * If (the current client is not in the client list readd it;) */ private class RemoveClientWatchCallBack implements WatcherCallBack { private SimpleClient client ; private boolean manualCheckOfClientCount = false; /** *Inits the handler with the client to check in the list * @param client client to monitor in the client list */ protected RemoveClientWatchCallBack (SimpleClient client) { this.client = client; } /** * If the client list is modified, checks if the current is still in the list. otherwise, readds ti. * @return true to enable watcher, or false to disable it in future WatcherThread cycles. */ public boolean handleWatchEvent(WatcherElement watcher, Lock lock) { // if lock is null, no client has been added since last, clear. //the client is then the last client if (client != null) { if (lock == null ) { //checks if the client is not already in the lists // ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList(); boolean islastClient = true; if (manualCheckOfClientCount) { log.debug("manual checking of count of client"); //checks if the client is not already in the lists ClientHandle[] cl = clist.retrieveClientList();//lock);//clist.retrieveClientList(); if(cl == null || cl.length<1 ) // {//no client has registered as active { islastClient = true; log.debug("list is empty"); } else islastClient = false; log.debug("list is not empty"); } // if(cl == null || cl.length<1 ) // {//no client has registered as active if (islastClient) { //the client is the last one, so close current session log.info("last client removed: closing session"); closeSession(client); } } else { log.debug("not the last client found "); // ask to the client to cpoy application data into the document // client.evgen._raise(Events.DOCUMENT_FINALIZEAPPDATA, null, client,null); // / } } log.debug("Stopping EventGenerator.."); // TODO: ensure ClientsFile lock is really released!! clist.unlockFile(); client.evgen.stopWatching(); } watcher.setHandler(null);//Do not check if the client is the last client. watcher will shutdown anyway // watcher.haltWatch(); // watcher. return false; } } /** * closes the current session, * and send an event to the last client to close the document * @param client the last client of the client */ private void closeSession(SimpleClient client) { // close document client.evgen._raise(Events.DOCUMENT_REQUESTTOCLOSE, null, client,null); log.debug("close document request done"); closeSession(client.getSessionHandle()); } /** * CLoses the current session * @param sessionHandle sessionHandle of the session to remove */ private void closeSession(SessionHandle sessionHandle) { getSessionManager().removeSession(sessionHandle); log.debug("Session removed"); } /** * @return the sessionManager */ protected SimpleSessionManager getSessionManager() { return sessionManager; } /** * @param sessionManager the sessionManager to set */ protected void setSessionManager(SimpleSessionManager sessionManager) { this.sessionManager = sessionManager; } public ClientsFile getStoreDocFile() { if (storedocfile==null) { } return storedocfile; } ClientSessionFileWatcherElement clistWatchElement=null; public ClientSessionFileWatcherElement getClientWatcherElement() { if (clistWatchElement==null) { clistWatchElement=new ClientSessionFileWatcherElement(clist,null); } return clistWatchElement; } /** * writes a vector of vorba Ids to the session. * @param modObjects public void setModObjectList(Vector modObjects) { log.debug("Writing "+modObjects.size()+" ids to ModObjectList"); // TODO Auto-generated method stub } ** * get current list of modified objects. * @return null or Vector of objects * public Vector getModObjectList() { log.debug("Reading modObjectList"); return null; } */ }