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"); /** * the appender that writes to the log file inside the session's directory. */ private FileAppender slogAppender=null; /** * 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(slogAppender = new FileAppender(new PatternLayout( "%-4r [%t] %-5p %c %x - %m%n"), sessionLogFile.getAbsolutePath(), true)); } else { log.info("No appender for SessionLog"); } } private void closeSessionLog() { if (slog!=null) { if (slogAppender!=null) { slog.removeAppender(slogAppender); slogAppender.close(); slogAppender = null; } } } /** * 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"); if (clistWatchElement!=null) { clistWatchElement.haltWatch(); clistWatchElement.watched.unlockFile(); } //clist.clearList(); //clist.unlockFile(); 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); } //cwe.haltWatch(); client.evgen.stopWatching(); try { log.debug("Attempting to release active client locks"); client.releaseActiveClientFile(); } catch (IOException e) { log.error("error during active file client release"); } tidyUp(); if (closeSession) { log.debug("Last active client: closing session"); log.info("Closing session"); getSessionManager().removeSession(client.getSessionHandle()); } } /** * close every file and stop. */ private void tidyUp() { if (clist != null) clist.unlockFile(); clist = null; storedocfile.unlockFile(); storedocfile = null; closeSessionLog(); } 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; /** * get or create a watcher on clist. * @return the contents of clistWatchElement or initialise it */ 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; } */ }