package org.peerbox.watchservice; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.hive2hive.core.exceptions.NoPeerConnectionException; import org.hive2hive.core.exceptions.NoSessionException; import org.hive2hive.processframework.exceptions.InvalidProcessStateException; import org.hive2hive.processframework.exceptions.ProcessExecutionException; import org.peerbox.app.manager.file.IFileManager; import org.peerbox.watchservice.filetree.composite.FileComponent; import org.peerbox.watchservice.filetree.composite.FolderComposite; import org.peerbox.watchservice.states.AbstractActionState; import org.peerbox.watchservice.states.EstablishedState; import org.peerbox.watchservice.states.ExecutionHandle; import org.peerbox.watchservice.states.InitialState; import org.peerbox.watchservice.states.LocalMoveState; import org.peerbox.watchservice.states.RemoteUpdateState; import org.peerbox.watchservice.states.StateType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Action class has the context role in the State Pattern used to implement * the state machine. It provides a systematic and lose-coupled way to change the * state of a {@link org.peerbox.watchservice.filetree.composite.FileComponent * FileComponent}. In general, there is exactly one instance of each {@link org.peerbox. * watchservice.filetree.composite.FileComponent FileComponent} and Action which form * a tightly coupled pair. While the {@link org.peerbox.watchservice.filetree. * composite.FileComponent FileComponent} is primarily used to represent files * and their properties, Action is used to represent the application internal state * and to define the upcoming network operation for files. * * * @author albrecht, anliker, winzenried * */ public class Action implements IAction { private final static Logger logger = LoggerFactory.getLogger(Action.class); /** * An instance of {@link org.peerbox.watchservice.filetree.composite. * FileComponent FileComponent} which is tightly coupled with this * Action. This object represents the file in the PeerWasp system for * which this Action object maintains the state. */ private FileComponent file; private final AtomicLong timestamp; /** * This state defines what operations are performed if the Action is executed * in the future. Events are applied to this state if the Action is not executed * when the event occurs. */ private volatile AbstractActionState currentState; private volatile AbstractActionState nextState; /** * Is true if the Action is currenly executed, i.e. after {@link #execute(IFileManager)} * is called and before {@link #onSucceeded()} or {@link #onFailed()} are called. */ private volatile boolean isExecuting = false; private volatile boolean changedWhileExecuted = false; /** * How many times the PeerWasp tried to perform the current execution. * If the Action is currently not executed, this is 0. */ private volatile int executionAttempts = 0; /** * This dependency is important as most states need this instance in various places * to perform a correct event handling. */ private IFileEventManager fileEventManager; private final Lock lock = new ReentrantLock(); public Action() { this(null); } /** * A new action is always in the {@link org.peerbox.watchservice.states.InitialState * InitialState}. * @param fileEventManager used to provide for */ public Action(final IFileEventManager fileEventManager) { this.currentState = new InitialState(this); this.nextState = new EstablishedState(this); timestamp = new AtomicLong(Long.MAX_VALUE); this.fileEventManager = fileEventManager; updateTimestamp(); } /** * Handles the local create event. If {@link isExecuting} == false, * the event is handled on the currentState. If it is true, the nextState * is changed accordingly and the {@link changedWhileExecuted} flag is set * if needed. */ @Override public void handleLocalCreateEvent() { logger.trace("handleLocalCreateEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnLocalCreate(); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleLocalCreate(); nextState = currentState.getDefaultState(); } } finally { releaseLock(); } } /** * Handles the local update event. For further documentation on event handling, * check {@link #handleLocalCreateEvent()}. If currentState is of * type {@link org.peerbox.watchservice.states.RemoteUpdateState * RemoteUpdateState} and {@link org.peerbox.watchservice.states. * RemoteUpdateState#getLocalUpdateHappened() RemoteUpdateState.getLocal * UpdateHappened()} == false (i.e. no local update event happened yet), the * event is ignored. This is useful, as the local update event introduced by the * H2H download() API call should not be mistaken for an actual local file change. * In any other case, the local update is simply forwarded to the corresponding * state. */ @Override public void handleLocalUpdateEvent() { logger.trace("handleLocalUpdateEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); if (isExecuting()) { if(currentState.getStateType() == StateType.REMOTE_CREATE){ // RemoteCreateState castedState = (RemoteCreateState)currentState; // if(!castedState.localCreateHappened()){ // nextState = nextState.changeStateOnLocalUpdate(); // checkIfChanged(); // } else { // logger.debug("File {}: LocalUpdateEvent after LocalCreateEvent " // + "in RemoteCreateState - ignored!", file.getPath()); // } } else if(currentState.getStateType() == StateType.REMOTE_UPDATE){ RemoteUpdateState castedState = (RemoteUpdateState)currentState; if(castedState.getLocalUpdateHappened()){ nextState = nextState.changeStateOnLocalUpdate(); checkIfChanged(); } else { castedState.setLocalUpdateHappened(true); logger.debug("File {}: First LocalUpdateEvent " + "in RemoteUpdateState - ignored!", file.getPath()); } } else { nextState = nextState.changeStateOnLocalUpdate(); checkIfChanged(); } } else { updateTimestamp(); if (currentState instanceof LocalMoveState) { logger.trace("A LocalUpdateEvent occured in LocalMoveState for file {}." + " Even though the Action is not yet executing, we apply the event" + "to the next state.", getFile().getPath()); nextState = nextState.changeStateOnLocalUpdate(); checkIfChanged(); } else { currentState = currentState.handleLocalUpdate(); nextState = currentState.getDefaultState(); } } } finally { releaseLock(); } } /** * Handles the local delete event triggered when a file is removed from the HDD * (or moved into the trash bin). For further documentation on event handling, * check {@link #handleLocalCreateEvent()}. */ @Override public void handleLocalSoftDeleteEvent() { logger.trace("handleLocalDeleteEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnLocalDelete(); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleLocalDelete(); nextState = currentState.getDefaultState(); } } finally { releaseLock(); } } /** * Handles the local hard delete event. This event is triggered by the PeerWasp * code and not by the file system. If the connected {@link org.peerbox.watchservice. * filetree.composite.FileComponent FileComponent} is a {@link org.peerbox.watchservice. * filetree.composite.FolderComposite FolderComposite}, the event is propagated manually * to all children to initiate a recursive handling. Besides that, the file is removed from * the HDD by code. This triggers a local delete event. For further documentation on * event handling, check {@link #handleLocalCreateEvent()}. */ @Override public void handleLocalHardDeleteEvent(){ logger.trace("handleLocalHardDeleteEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); if (getFile().isFolder()) { logger.trace("Folder {} - delete children", getFile().getPath()); FolderComposite folder = (FolderComposite) getFile(); Map<Path, FileComponent> children = folder.getChildren(); for (Map.Entry<Path, FileComponent> childEntry : children.entrySet()) { FileComponent child = childEntry.getValue(); child.getAction().handleLocalHardDeleteEvent(); } } try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnLocalHardDelete(); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleLocalHardDelete(); nextState = currentState.getDefaultState(); } try { if (!Files.exists(getFile().getPath())) { return; } Files.delete(getFile().getPath()); logger.trace("DELETED FROM DISK: {}", getFile().getPath()); } catch (IOException e) { logger.warn("Could not delete file: {} ({})", getFile().getPath(), e.getMessage(), e); } } finally { releaseLock(); } } /** * Handles the local move event. This event is triggered by the PeerWasp * code when a local delete and a local create event correspond to a * file move performed by the user. * * In general, file modifications lead to modify events. Occasionally, the * file system triggers an additional pair of delete/create events on the same * file. This case is inadvertently interpreted as a move to the same location * up to this point. Hence, the handling of such an event stops here. * * For further documentation on event handling, * check {@link #handleLocalCreateEvent()}. */ @Override public void handleLocalMoveEvent(Path oldFilePath) { logger.debug("handleLocalMoveEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnLocalMove(oldFilePath); checkIfChanged(); } else { updateTimestamp(); if (oldFilePath.equals(getFile().getPath())) { logger.trace("File {}: Move to same location due to update!", getFile().getPath()); fileEventManager.getFileTree().getDeletedByContentHash() .get(getFile().getContentHash()).remove(oldFilePath); return; } currentState = currentState.handleLocalMove(oldFilePath); nextState = currentState.getDefaultState(); } } finally { releaseLock(); } } /** * Handles the remote create event triggered by Hive2Hive. For further documentation * on event handling, check {@link #handleLocalCreateEvent()}. */ @Override public void handleRemoteCreateEvent() { logger.trace("handleRemoteCreateEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnRemoteCreate(); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleRemoteCreate(); nextState = currentState.getDefaultState(); } } finally { releaseLock(); } } /** * Handles the remote update event triggered by Hive2Hive. For further documentation * on event handling, check {@link #handleLocalCreateEvent()}. */ @Override public void handleRemoteUpdateEvent() { logger.trace("handleRemoteUpdateEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnRemoteUpdate(); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleRemoteUpdate(); nextState = currentState.getDefaultState(); } } finally { releaseLock(); } } /** * Handles the remote delete event triggered by Hive2Hive. For further documentation * on event handling, check {@link #handleLocalCreateEvent()}. If the connected * {@link org.peerbox.watchservice. filetree.composite.FileComponent FileComponent} * is a {@link org.peerbox.watchservice.filetree.composite.FolderComposite FolderComposite}, * the event is propagated manually to all children to initiate a recursive handling. * For further documentation on event handling, check {@link #handleLocalCreateEvent()}. */ @Override public void handleRemoteDeleteEvent() { logger.trace("handleRemoteDeleteEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); if (getFile().isFolder()) { logger.trace("Folder {} - delete children", getFile().getPath()); FolderComposite folder = (FolderComposite) getFile(); Map<Path, FileComponent> children = folder.getChildren(); for (Map.Entry<Path, FileComponent> childEntry : children.entrySet()) { FileComponent child = childEntry.getValue(); child.getAction().handleRemoteDeleteEvent(); } } try { acquireLock(); if (isExecuting()) { nextState = nextState.changeStateOnRemoteDelete(); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleRemoteDelete(); nextState = currentState.getDefaultState(); } } finally { releaseLock(); } } /** * Handles the remote move event triggered by Hive2Hive. This method moves the * file in the file system. For further documentation on event handling, * check {@link #handleLocalCreateEvent()}. */ @Override public void handleRemoteMoveEvent(Path path) { logger.trace("handleRemoteMoveEvent - File: {}, isExecuting({})", getFile().getPath(), isExecuting()); try { acquireLock(); Path srcPath = getFile().getPath(); if (isExecuting()) { nextState = nextState.changeStateOnRemoteMove(path); checkIfChanged(); } else { updateTimestamp(); currentState = currentState.handleRemoteMove(path); nextState = currentState.getDefaultState(); try { if (!Files.exists(srcPath)) { return; } Files.move(srcPath, path); } catch (IOException e) { logger.warn("Could not move file: from src={} to dst={} ({})", srcPath, path, e.getMessage(), e); } } } finally { releaseLock(); } } private void checkIfChanged() { if (!(nextState instanceof EstablishedState)) { logger.trace("File {}: Next state is {}, keep track of change", getFile().getPath(), getNextStateName()); changedWhileExecuted = true; } else { logger.trace("File {}: Next state is {}, no change detected", getFile().getPath(), getNextStateName()); } } /** * Each state is able to execute an action as soon the state is considered as stable. * The action itself depends on the current state (e.g. add file, delete file, etc.). * If an action does not execute anything (i.e. if the state is {@link org.peerbox. * watchservice.states.InitialState InitialState} or {@link org.peerbox. * watchservice.states.EstablishedState EstablishedState}, the returned handle is null. * As a consequence, the {@link #isExecuting} flag is set to false in this case. * @return * @throws NoSessionException * @throws NoPeerConnectionException * @throws IllegalFileLocation * @throws InvalidProcessStateException * @throws ProcessExecutionException */ @Override public ExecutionHandle execute(IFileManager fileManager) throws NoSessionException, NoPeerConnectionException, InvalidProcessStateException, ProcessExecutionException { if (isExecuting()) { throw new IllegalStateException("Action is already executing."); } ExecutionHandle ehandle = null; try { acquireLock(); setIsExecuting(true); ++executionAttempts; ehandle = currentState.execute(fileManager); if(ehandle == null){ setIsExecuting(false); } return ehandle; // } catch(IllegalArgumentException ex){ // logger.trace("Captured IllegalArgumentException ex for {}", getFile().getPath()); // setIsExecuting(false); // return null; } finally { releaseLock(); } } /** * This method performs the cleanup routine after an action's execution * terminated successfully as expected. It performs a state transition from * the {@link #currentState} to the {@link #nextState}, sets the {@link * #isExecuting} flag to false, and resets the {@link #changedWhileExecuted} flag * to false as well. To prevent concurrent manipulations on * the Action object, the object is locked in the meantime. */ @Override public void onSucceeded() { logger.trace("onSucceeded: File {} - Switch state from {} to {}", getFile().getPath(), getCurrentStateName(), getNextStateName()); try { acquireLock(); currentState = nextState; nextState = nextState.getDefaultState(); setIsExecuting(false); changedWhileExecuted = false; executionAttempts = 0; } finally { releaseLock(); } } /** * This method performs the cleanup routine after an action's execution * terminated but failed. Until know, it only sets the {@link * #isExecuting} flag to false. To prevent concurrent manipulations on * the Action object, the object is locked in the meantime. */ @Override public void onFailed() { try { acquireLock(); setIsExecuting(false); } finally { releaseLock(); } } private void acquireLock() { logger.trace("File {} with ID {}: Wait for own lock at t={} in State {}", getFile().getPath(), getFile().hashCode(), System.currentTimeMillis(), getCurrentState().getStateType()); lock.lock(); logger.trace("File {} with ID {}: Received own lock at t={} in State {}", getFile().getPath(), getFile().hashCode(), System.currentTimeMillis(), getCurrentState().getStateType()); } private void releaseLock() { lock.unlock(); logger.trace("File {} with ID {}: Released own lock at t={} in State {}", getFile().getPath(), getFile().hashCode(), System.currentTimeMillis(), getCurrentState().getStateType()); } /** * @return The {@link #currentState} */ @Override public AbstractActionState getCurrentState() { return currentState; } /** * @return The simple name of the state (e.g. "LocalCreateState") */ @Override public String getCurrentStateName() { return currentState != null ? currentState.getClass().getSimpleName() : "null"; } /** * @return The {@link #nextState} */ @Override public AbstractActionState getNextState() { return nextState; } /** * @return The simple name of the {@link #nextState} (e.g. "LocalCreateState") */ @Override public String getNextStateName() { return nextState != null ? nextState.getClass().getSimpleName() : "null"; } /** * @return True if events happened during the Action's execution which changed the Action * in a way that implies further network operations. Example: While a local * create event is executed, the file is already modified locally. */ @Override public boolean getChangedWhileExecuted() { return changedWhileExecuted; } /** * @return How many times the PeerWasp tried to perform the current execution. */ @Override public int getExecutionAttempts() { return executionAttempts; } /** * @return Returns the flag {@link isExecuting}. */ @Override public boolean isExecuting() { return isExecuting; } private void setIsExecuting(final boolean isExecuting) { this.isExecuting = isExecuting; } /** * @returns The {@link #timeStamp}. */ @Override public long getTimestamp() { return timestamp.get(); } /** * Updates the {@link timestamp} of this Action object to the current time * in milliseconds. */ @Override public void updateTimestamp() { timestamp.set(System.currentTimeMillis()); } @Override public void updateTimeAndQueue(){ getFileEventManager().getFileComponentQueue().remove(getFile()); updateTimestamp(); getFileEventManager().getFileComponentQueue().add(getFile()); } /** * @returns The {@link #fileEventManager}. */ @Override public IFileEventManager getFileEventManager() { return fileEventManager; } /** * Sets the {@link #fileEventManager}. */ @Override public void setFileEventManager(final IFileEventManager fileEventManager) { this.fileEventManager = fileEventManager; } /** * @returns The {@link #file}. */ @Override public FileComponent getFile() { return file; } /** * Sets the {@link #file}. */ @Override public void setFile(final FileComponent file) { this.file = file; } /** * Sets the {@link #currentState}. This should never be * used change the Action's state manually due to file events, * as the state machine already sets the states accordingly! */ @Override public void setCurrentState(AbstractActionState state) { this.currentState = state; } /** * Prints the Action object using a specified format. Example: * "Action[currentState(LocalCreateState), nextState(EstablishedState), * isExecuting(true), changedWhileExecuted(false), executionAttempts(2)". */ @Override public String toString() { return String.format("Action[currentState(%s), nextState(%s), isExecuting(%s), changedWhileExecuted(%s), executionAttempts(%d),]", getCurrentStateName(), getNextStateName(), isExecuting(), changedWhileExecuted, executionAttempts); } }