/** * This file is licensed under the University of Illinois/NCSA Open Source License. See LICENSE.TXT for details. */ package edu.illinois.codingtracker.replaying; import java.io.File; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.MessageBox; import org.eclipse.ui.IEditorPart; import edu.illinois.codingtracker.helpers.Configuration; import edu.illinois.codingtracker.compare.helpers.EditorHelper; import edu.illinois.codingtracker.helpers.ResourceHelper; import edu.illinois.codingtracker.helpers.ViewerHelper; import edu.illinois.codingtracker.operations.JavaProjectsUpkeeper; import edu.illinois.codingtracker.operations.OperationDeserializer; import edu.illinois.codingtracker.operations.UserOperation; import edu.illinois.codingtracker.operations.files.EditedFileOperation; import edu.illinois.codingtracker.operations.files.EditedUnsychronizedFileOperation; import edu.illinois.codingtracker.operations.files.SavedFileOperation; import edu.illinois.codingtracker.operations.files.snapshoted.SnapshotedFileOperation; import edu.illinois.codingtracker.operations.resources.CreatedResourceOperation; import edu.illinois.codingtracker.operations.resources.ReorganizedResourceOperation; import edu.illinois.codingtracker.operations.resources.ResourceOperation; import edu.illinois.codingtracker.operations.textchanges.TextChangeOperation; /** * * @author Stas Negara * */ public class UserOperationReplayer { private enum ReplayPace { FAST, SIMULATE, CUSTOM } private final OperationSequenceView operationSequenceView; private IAction loadAction; private IAction resetAction; private IAction findAction; private final Collection<IAction> replayActions= new LinkedList<IAction>(); private List<UserOperation> userOperations; private Iterator<UserOperation> userOperationsIterator; private UserOperation currentUserOperation; private Collection<UserOperation> breakpoints; private Thread userOperationExecutionThread; private volatile boolean forcedExecutionStop= false; private IEditorPart currentEditor= null; public UserOperationReplayer(OperationSequenceView operationSequenceView) { this.operationSequenceView= operationSequenceView; } void addToolBarActions() { IToolBarManager toolBarManager= operationSequenceView.getToolBarManager(); toolBarManager.add(createLoadOperationSequenceAction()); toolBarManager.add(createResetOperationSequenceAction()); toolBarManager.add(new Separator()); toolBarManager.add(createReplayAction(newReplaySingleOperationAction(), "Step", "Replay the current user operation", false)); toolBarManager.add(createReplayAction(newReplayOperationSequenceAction(ReplayPace.CUSTOM), "Custom", "Replay the remaining user operations at a custom pace", true)); toolBarManager.add(createReplayAction(newReplayOperationSequenceAction(ReplayPace.SIMULATE), "Simulate", "Simulate the remaining user operations at the user pace", true)); toolBarManager.add(createReplayAction(newReplayOperationSequenceAction(ReplayPace.FAST), "Fast", "Fast replay of the remaining user operations", true)); toolBarManager.add(createReplayAction(newJumpToAction(), "Jump", "Jump as close as possible to a given timestamp", false)); toolBarManager.add(new Separator()); toolBarManager.add(createFindOperationAction()); toolBarManager.add(new Separator()); } private UserOperation findUserOperationClosestToTimestamp(long searchedTimestamp) { UserOperation foundUserOperation= null; long minimumDeltaTime= Long.MAX_VALUE; for (UserOperation userOperation : userOperations) { long currentDeltaTime= Math.abs(userOperation.getTime() - searchedTimestamp); if (currentDeltaTime < minimumDeltaTime) { minimumDeltaTime= currentDeltaTime; foundUserOperation= userOperation; if (minimumDeltaTime == 0) { //Found the exact match, no need to proceed. break; } } } return foundUserOperation; } private IAction createFindOperationAction() { findAction= new Action() { @Override public void run() { TimestampDialog dialog= new TimestampDialog(operationSequenceView.getShell(), "Find operation"); if (dialog.open() == Window.OK) { long searchedTimestamp= dialog.getTimestamp(); UserOperation foundUserOperation= findUserOperationClosestToTimestamp(searchedTimestamp); if (foundUserOperation == null) { showMessage("There are no operations near timestamp " + searchedTimestamp); } else { long deltaTime= Math.abs(searchedTimestamp - foundUserOperation.getTime()); if (deltaTime == 0) { showMessage("Found the exact match!"); } else { showMessage("Found the closest operation, delta = " + deltaTime + " ms."); } operationSequenceView.setSelection(new StructuredSelection(foundUserOperation)); if (!operationSequenceView.getOperationSequenceFilter().isShown(foundUserOperation)) { showMessage("Operation with timestamp " + foundUserOperation.getTime() + " is filtered out"); } } } } }; ViewerHelper.initAction(findAction, "Find", "Find operation by its timestamp", false, false, false); return findAction; } private IAction createLoadOperationSequenceAction() { loadAction= new Action() { @Override public void run() { FileDialog fileDialog= new FileDialog(operationSequenceView.getShell(), SWT.OPEN); String selectedFilePath= fileDialog.open(); if (selectedFilePath != null) { String operationsRecord= ResourceHelper.readFileContent(new File(selectedFilePath)); try { userOperations= OperationDeserializer.getUserOperations(operationsRecord); } catch (RuntimeException e) { showMessage("Wrong format. Could not load user operations from file: " + selectedFilePath); throw e; } if (userOperations.size() > 0) { resetAction.setEnabled(true); findAction.setEnabled(true); } breakpoints= new HashSet<UserOperation>(); prepareForReplay(); System.out.println("Loaded " + userOperations.size() + " operations."); } } }; ViewerHelper.initAction(loadAction, "Load", "Load operation sequence from a file", true, false, false); return loadAction; } private IAction createResetOperationSequenceAction() { resetAction= new Action() { @Override public void run() { JavaProjectsUpkeeper.clearWorkspace(); prepareForReplay(); } }; ViewerHelper.initAction(resetAction, "Reset", "Reset operation sequence", false, false, false); return resetAction; } private void prepareForReplay() { initializeReplay(); advanceCurrentUserOperation(null); operationSequenceView.setTableViewerInput(userOperations); updateReplayActionsStateForCurrentUserOperation(); } private void initializeReplay() { UserOperation.isReplayedRefactoring= false; currentEditor= null; userOperationsIterator= userOperations.iterator(); } private IAction createReplayAction(IAction action, String actionText, String actionToolTipText, boolean isToggable) { ViewerHelper.initAction(action, actionText, actionToolTipText, false, isToggable, false); replayActions.add(action); return action; } private IAction newReplaySingleOperationAction() { return new Action() { @Override public void run() { try { replayAndAdvanceCurrentUserOperation(null); } catch (RuntimeException e) { showReplayExceptionMessage(); throw e; } updateReplayActionsStateForCurrentUserOperation(); } }; } private IAction newReplayOperationSequenceAction(final ReplayPace replayPace) { return new Action() { @Override public void run() { if (!this.isChecked()) { forcedExecutionStop= true; this.setEnabled(false); userOperationExecutionThread.interrupt(); } else { replayUserOperationSequence(this, replayPace); } } }; } private IAction newJumpToAction() { return new Action() { private final Map<String, UserOperation> snapshotsBeforeJumpToTimestamp= new HashMap<String, UserOperation>(); private final Set<String> snapshotsAfterJumpToTimestamp= new HashSet<String>(); private final Set<String> ensureSnapshots= new HashSet<String>(); private ResourceOperation lastEditBeforeJumpToTimestamp= null; private long jumpToTimestamp= -1; private boolean shouldConsiderLastEditBeforeJumpTo= false; private boolean metFirstEditAfterJumpTo= false; @Override public void run() { TimestampDialog dialog= new TimestampDialog(operationSequenceView.getShell(), "Jump to timestamp"); if (dialog.open() == Window.OK) { jumpToTimestamp= dialog.getTimestamp(); while (true) { initializeAction(); for (UserOperation userOperation : userOperations) { if (userOperation instanceof ResourceOperation) { handleResourceOperation((ResourceOperation)userOperation); } if (doesChangeFileContent(userOperation) && isAfterJumpToTimestamp(userOperation) && !metFirstEditAfterJumpTo) { shouldConsiderLastEditBeforeJumpTo= true; } } UserOperation startOperation= getStartOperation(); if (ensureSnapshots.size() == 0) { jumpTo(startOperation); break; } else { jumpToTimestamp= startOperation.getTime(); } } } } private void initializeAction() { shouldConsiderLastEditBeforeJumpTo= false; metFirstEditAfterJumpTo= false; snapshotsBeforeJumpToTimestamp.clear(); snapshotsAfterJumpToTimestamp.clear(); ensureSnapshots.clear(); lastEditBeforeJumpToTimestamp= null; } private void handleResourceOperation(ResourceOperation resourceOperation) { if (resourceOperation instanceof ReorganizedResourceOperation) { ReorganizedResourceOperation reorganizedResourceOperation= (ReorganizedResourceOperation)resourceOperation; UserOperation snapshotOperation= snapshotsBeforeJumpToTimestamp.get(reorganizedResourceOperation.getResourcePath()); if (snapshotOperation != null) { snapshotsBeforeJumpToTimestamp.put(reorganizedResourceOperation.getDestinationPath(), snapshotOperation); } if (snapshotsAfterJumpToTimestamp.contains(reorganizedResourceOperation.getResourcePath())) { snapshotsAfterJumpToTimestamp.add(reorganizedResourceOperation.getDestinationPath()); } return; } if (isAfterJumpToTimestamp(resourceOperation)) { if (doesCreateNewFileContent(resourceOperation)) { snapshotsAfterJumpToTimestamp.add(resourceOperation.getResourcePath()); } if (doesEditFile(resourceOperation)) { metFirstEditAfterJumpTo= true; if (!snapshotsAfterJumpToTimestamp.contains(resourceOperation.getResourcePath())) { ensureSnapshots.add(resourceOperation.getResourcePath()); } } } else { if (doesCreateNewFileContent(resourceOperation)) { snapshotsBeforeJumpToTimestamp.put(resourceOperation.getResourcePath(), resourceOperation); } if (doesEditFile(resourceOperation)) { lastEditBeforeJumpToTimestamp= resourceOperation; } } } private boolean isAfterJumpToTimestamp(UserOperation userOperation) { //Note that equals is also after since this will be replayed after jump as well. return userOperation.getTime() >= jumpToTimestamp; } private boolean doesCreateNewFileContent(ResourceOperation resourceOperation) { return resourceOperation instanceof SnapshotedFileOperation || resourceOperation instanceof CreatedResourceOperation; } private boolean doesEditFile(ResourceOperation resourceOperation) { return resourceOperation instanceof EditedFileOperation || resourceOperation instanceof EditedUnsychronizedFileOperation; } private boolean doesChangeFileContent(UserOperation userOperation) { return userOperation instanceof TextChangeOperation || userOperation instanceof SavedFileOperation; } private UserOperation getStartOperation() { UserOperation startOperation= null; //Ensure that the last edited file before the "jump to" timestamp is snapshoted to account for jumps inside such edits. if (lastEditBeforeJumpToTimestamp != null && shouldConsiderLastEditBeforeJumpTo) { ensureSnapshots.add(lastEditBeforeJumpToTimestamp.getResourcePath()); } if (ensureSnapshots.size() == 0) { startOperation= findUserOperationClosestToTimestamp(jumpToTimestamp); } else { for (String fileToEnsureSnapshot : ensureSnapshots) { UserOperation snapshotOperation= snapshotsBeforeJumpToTimestamp.get(fileToEnsureSnapshot); if (snapshotOperation == null) { showMessage("A file edited after jump to timestamp was not snapshoted before it: " + fileToEnsureSnapshot); break; } if (startOperation == null || startOperation.getTime() > snapshotOperation.getTime()) { startOperation= snapshotOperation; } } } return startOperation; } private void jumpTo(UserOperation userOperation) { initializeReplay(); UserOperation oldUserOperation= currentUserOperation; currentUserOperation= userOperation; while (userOperationsIterator.hasNext()) { if (currentUserOperation == userOperationsIterator.next()) { break; } } updateSequenceView(oldUserOperation); } }; } private void replayUserOperationSequence(IAction executionAction, ReplayPace replayPace) { if (replayPace == ReplayPace.CUSTOM) { CustomDelayDialog dialog= new CustomDelayDialog(operationSequenceView.getShell()); if (dialog.open() == Window.CANCEL) { executionAction.setChecked(false); return; } } forcedExecutionStop= false; loadAction.setEnabled(false); resetAction.setEnabled(false); findAction.setEnabled(false); toggleReplayActions(false); executionAction.setEnabled(true); userOperationExecutionThread= new UserOperationExecutionThread(executionAction, replayPace, CustomDelayDialog.getDelay()); userOperationExecutionThread.start(); } private void replayAndAdvanceCurrentUserOperation(ReplayPace replayPace) { try { if (!Configuration.isInTestMode && currentEditor != null && currentEditor != EditorHelper.getActiveEditor()) { if (userOperationExecutionThread != null && userOperationExecutionThread.isAlive()) { forcedExecutionStop= true; userOperationExecutionThread.interrupt(); } showMessage("The current editor is wrong. Should be: \"" + currentEditor.getTitle() + "\""); return; } currentUserOperation.replay(); currentEditor= EditorHelper.getActiveEditor(); } catch (Exception e) { throw new RuntimeException(e); } advanceCurrentUserOperation(replayPace); } private void advanceCurrentUserOperation(ReplayPace replayPace) { UserOperation oldUserOperation= currentUserOperation; if (userOperationsIterator.hasNext()) { currentUserOperation= userOperationsIterator.next(); } else { currentUserOperation= null; } if (replayPace != ReplayPace.FAST) { //Do not display additional info during a fast replay. updateSequenceView(oldUserOperation); } } private void updateSequenceView(UserOperation oldUserOperation) { operationSequenceView.removeSelection(); if (oldUserOperation != null) { operationSequenceView.updateTableViewerElement(oldUserOperation); } operationSequenceView.displayInOperationTextPane(currentUserOperation); operationSequenceView.updateTableViewerElement(currentUserOperation); } private void updateReplayActionsStateForCurrentUserOperation() { toggleReplayActions(currentUserOperation != null); } private void toggleReplayActions(boolean state) { for (IAction action : replayActions) { action.setEnabled(state); } } boolean isBreakpoint(Object object) { return breakpoints.contains(object); } boolean isCurrentUserOperation(Object object) { return currentUserOperation == object; } void toggleBreakpoint(UserOperation userOperation) { if (breakpoints.contains(userOperation)) { breakpoints.remove(userOperation); } else { breakpoints.add(userOperation); } } private void showMessage(String message) { MessageBox messageBox= new MessageBox(operationSequenceView.getShell()); messageBox.setMessage(message); messageBox.open(); } private void showReplayExceptionMessage() { showMessage("An exception occured while executing the current user operation"); } private class UserOperationExecutionThread extends Thread { private final IAction executionAction; private final ReplayPace replayPace; private final int customDelayTime; private boolean stoppedDueToException= false; private final UserOperation firstUserOperation; private UserOperationExecutionThread(IAction executionAction, ReplayPace replayPace, int customDelayTime) { this.executionAction= executionAction; this.replayPace= replayPace; this.customDelayTime= customDelayTime; firstUserOperation= currentUserOperation; } @Override public void run() { final long startReplayTime= System.currentTimeMillis(); try { do { long executedOperationTime= currentUserOperation.getTime(); long startTime= System.currentTimeMillis(); executeUserOperationInUIThread(); if (shouldStopExecution()) { break; } else { if (replayPace == ReplayPace.SIMULATE) { long nextOperationTime= currentUserOperation.getTime(); long delayTime= nextOperationTime - executedOperationTime; simulateDelay(delayTime, startTime); } else if (replayPace == ReplayPace.CUSTOM) { simulateDelay(customDelayTime, startTime); } if (forcedExecutionStop) { break; } } } while (true); } finally { operationSequenceView.getDisplay().syncExec(new Runnable() { @Override public void run() { long replayTime= System.currentTimeMillis() - startReplayTime; if (stoppedDueToException) { showReplayExceptionMessage(); } showMessage("Replay time: " + replayTime + " ms"); } }); updateToolBarActions(); } } private void executeUserOperationInUIThread() { operationSequenceView.getDisplay().syncExec(new Runnable() { @Override public void run() { try { replayAndAdvanceCurrentUserOperation(replayPace); } catch (RuntimeException e) { stoppedDueToException= true; throw e; } } }); } private boolean shouldStopExecution() { return currentUserOperation == null || breakpoints.contains(currentUserOperation); } private void simulateDelay(long delayTime, long startTime) { long finishTime= System.currentTimeMillis(); long sleepTime= delayTime - (finishTime - startTime); if (sleepTime > 0) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { //ignore } } } private void updateToolBarActions() { executionAction.setChecked(false); loadAction.setEnabled(true); resetAction.setEnabled(true); findAction.setEnabled(true); updateReplayActionsStateForCurrentUserOperation(); if (replayPace == ReplayPace.FAST) { //Update the view after the fast replay is over. operationSequenceView.getDisplay().syncExec(new Runnable() { @Override public void run() { updateSequenceView(firstUserOperation); } }); } } } }