diff options
author | Isaac Arvestad | 2016-08-08 11:53:39 +0000 |
---|---|---|
committer | Isaac Arvestad | 2016-10-27 15:17:12 +0000 |
commit | da175faa30356d4cf8ff1f0ee9b0fa069806482b (patch) | |
tree | cca27d344a46f8151f7d39e49ef45c941654a4c3 | |
parent | 89c39c8c5bf74ab5e165dddc97e19a0faca1b486 (diff) | |
download | org.eclipse.swtbot-da175faa30356d4cf8ff1f0ee9b0fa069806482b.tar.gz org.eclipse.swtbot-da175faa30356d4cf8ff1f0ee9b0fa069806482b.tar.xz org.eclipse.swtbot-da175faa30356d4cf8ff1f0ee9b0fa069806482b.zip |
Bug 506628: Create SWTBot recorder server/client
Adds a server for recording SWTBot code and a client for viewing
generated code. The server runs in one Eclipse instance where tests
are recorded and the client runs in a separate Eclipse instance where
tests cases can be written and edited.
This change makes it possible to simultaneously have one workspace
open where a test is being recorded and another workspace where test
cases are being written.
Change-Id: I7b071498a143c210d4ed7692d28fe1e99562a0cf
Signed-off-by: Isaac Arvestad <isaac.arvestad@ericsson.com>
27 files changed, 1919 insertions, 0 deletions
diff --git a/org.eclipse.swtbot.generator.client/.classpath b/org.eclipse.swtbot.generator.client/.classpath new file mode 100644 index 00000000..0b1bcf94 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="src" path="src/"/> + <classpathentry kind="output" path="target/classes"/> +</classpath> diff --git a/org.eclipse.swtbot.generator.client/.gitignore b/org.eclipse.swtbot.generator.client/.gitignore new file mode 100644 index 00000000..e4e5f6c8 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/.gitignore @@ -0,0 +1 @@ +*~
\ No newline at end of file diff --git a/org.eclipse.swtbot.generator.client/.project b/org.eclipse.swtbot.generator.client/.project new file mode 100644 index 00000000..aad1e45f --- /dev/null +++ b/org.eclipse.swtbot.generator.client/.project @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.eclipse.swtbot.generator.client</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.m2e.core.maven2Builder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.m2e.core.maven2Nature</nature> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/org.eclipse.swtbot.generator.client/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.swtbot.generator.client/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..c537b630 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/org.eclipse.swtbot.generator.client/META-INF/MANIFEST.MF b/org.eclipse.swtbot.generator.client/META-INF/MANIFEST.MF new file mode 100644 index 00000000..98918aff --- /dev/null +++ b/org.eclipse.swtbot.generator.client/META-INF/MANIFEST.MF @@ -0,0 +1,22 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: SWTBot Generator Client +Bundle-SymbolicName: org.eclipse.swtbot.generator.client;singleton:=true +Bundle-Version: 2.6.0.qualifier +Bundle-Activator: org.eclipse.swtbot.generator.client.SWTBotRecorderClientPlugin +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.ui.console, + org.eclipse.jdt.core, + org.eclipse.jdt.ui, + org.eclipse.jdt.launching, + org.eclipse.debug.ui, + org.eclipse.pde.ui, + org.eclipse.swtbot.generator, + org.eclipse.jface.text, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.editors, + org.eclipse.jdt +Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Bundle-ActivationPolicy: lazy diff --git a/org.eclipse.swtbot.generator.client/README b/org.eclipse.swtbot.generator.client/README new file mode 100644 index 00000000..374f014e --- /dev/null +++ b/org.eclipse.swtbot.generator.client/README @@ -0,0 +1,54 @@ +# General information about the SWTBot server/client recorder plugin + +This plugin makes it possible to record and edit SWTBot tests at the +same time. By recording tests in a separate Eclipse instance the +original editor can be used to write tests in the regular Java editing +environment while being able to change perspective, open new views, +etc. in the test instance. + +The main parts of this plugin is the client view which displays +generated code and the recorder server which generates code. The +server generates code using the SWTBot code generator and then sends +it to the client view. The server code is part of the +'org.eclipse.swtbot.generator' plugin. + +## First time launching +Create a new 'SWTBot Recorder Server' launch configuration. This +configuration is similar to a regular Eclipse Application but contains +a VM argument for specifying that the recorder server should be +started. By default the server will use port 8000 but this can be +changed by changing the port number in the VM arguments. It is +possible to use a regular Eclipse Application launch configuration but +then one has to supply the VM argument manually and the client view +will not start connecting automatically when launched. + +The default VM argument for the server is +"-Dorg.eclipse.swtbot.generator.server.enable=8000". The integer at +the end of the string is the selected port number and can be +changed. When launching the edited configuration the client view will +update the port number before starting to connect. + +Make sure that the Eclipse application being launched has the +'org.eclipse.swtbot.generator.*' plugins included. + +## Launching +Launch the SWTBot Recorder Server launch configuration. This will +start a second Eclipse instance for recording tests. While launching, +the client view will also start trying to connect to the recorder +server. When the client view displays that it is connected you are +ready to record tests! + +## Recording +Toggle the record button to decide if code generated on the server +side should be recorded in the view. + +To insert code directly in a Java method, open a Java editor, click +"Add to method" in the client view and select the method to add the +code to. If you are recording, the code should now be inserted last in +the method. + +It is also possible to quickly move text from the client view and the +editor by right clicking in the editor and selecting "Import SWTBot +code" in the context menu. This moves the code from the client view to +the selection within the editor. The shortcut for this action is +"Ctrl+Alt+Y".
\ No newline at end of file diff --git a/org.eclipse.swtbot.generator.client/build.properties b/org.eclipse.swtbot.generator.client/build.properties new file mode 100644 index 00000000..0d3d3a74 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = bin/ +bin.includes = plugin.xml,\ + META-INF/,\ + .,\ + icons/ diff --git a/org.eclipse.swtbot.generator.client/icons/refresh.gif b/org.eclipse.swtbot.generator.client/icons/refresh.gif Binary files differnew file mode 100644 index 00000000..3ca04d06 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/icons/refresh.gif diff --git a/org.eclipse.swtbot.generator.client/icons/swtbot_rec16.png b/org.eclipse.swtbot.generator.client/icons/swtbot_rec16.png Binary files differnew file mode 100644 index 00000000..743ce38d --- /dev/null +++ b/org.eclipse.swtbot.generator.client/icons/swtbot_rec16.png diff --git a/org.eclipse.swtbot.generator.client/icons/swtbot_rec64.png b/org.eclipse.swtbot.generator.client/icons/swtbot_rec64.png Binary files differnew file mode 100644 index 00000000..2b4b7eb5 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/icons/swtbot_rec64.png diff --git a/org.eclipse.swtbot.generator.client/plugin.xml b/org.eclipse.swtbot.generator.client/plugin.xml new file mode 100644 index 00000000..9b04a62d --- /dev/null +++ b/org.eclipse.swtbot.generator.client/plugin.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.4"?> +<plugin> + + <extension + point="org.eclipse.debug.core.launchConfigurationTypes"> + <launchConfigurationType + delegate="org.eclipse.swtbot.generator.client.launcher.RecorderServerLaunchConfiguration" + id="org.eclipse.swtbot.generator.client.launcher.SWTBotServerRecorderType" + name="SWTBot Recorder Server" + modes="run, debug"> + </launchConfigurationType> + </extension> + <extension point="org.eclipse.debug.ui.launchConfigurationTabGroups"> + <launchConfigurationTabGroup + type="org.eclipse.swtbot.generator.client.launcher.SWTBotServerRecorderType" + class="org.eclipse.swtbot.generator.client.launcher.RecorderServerLauncherTabGroup" + id="org.eclipse.swtbot.generator.client.launcher.SWTBotServerRecorderTabGroup"> + <launchMode + description="Launch a SWTBot recorder server" + mode="run"> + </launchMode> + <launchMode + description="Launch a SWTBot recorder server" + mode="debug"> + </launchMode> + </launchConfigurationTabGroup> + </extension> + <extension + point="org.eclipse.debug.ui.launchConfigurationTypeImages"> + <launchConfigurationTypeImage + configTypeID="org.eclipse.swtbot.generator.client.launcher.SWTBotServerRecorderType" + icon="icons/swtbot_rec16.png" + id="org.eclipse.swtbot.generator.client.launcher.SWTBotServerRecorderTypeImage"> + </launchConfigurationTypeImage> + </extension> + <extension + point="org.eclipse.ui.views"> + <view + class="org.eclipse.swtbot.generator.client.views.RecorderClientView" + icon="icons/swtbot_rec16.png" + id="org.eclipse.swtbot.generator.client.view.recorder.client" + name="SWTBot Recorder " + restorable="true"> + </view> + </extension> + <extension + point="org.eclipse.ui.commands"> + <command + description="Imports code to editor and removes it from the client recorder view" + id="org.eclipse.swtbot.generator.client.commands.import.code" + name="Import SWTBot code"> + </command> + </extension> + <extension + point="org.eclipse.ui.menus"> + <menuContribution + allPopups="false" + locationURI="popup:#AbstractTextEditorContext?after=additions"> + <command + commandId="org.eclipse.swtbot.generator.client.commands.import.code" + label="Import SWTBot code" + style="push"> + </command> + </menuContribution> + </extension> + <extension + point="org.eclipse.ui.commandImages"> + <image + commandId="org.eclipse.swtbot.generator.client.commands.import.code" + icon="icons/swtbot_rec64.png"> + </image> + </extension> + <extension + point="org.eclipse.ui.handlers"> + <handler + class="org.eclipse.swtbot.generator.client.commands.ImportClientCodeHandler" + commandId="org.eclipse.swtbot.generator.client.commands.import.code"> + </handler> + </extension> + <extension + point="org.eclipse.ui.bindings"> + <key + commandId="org.eclipse.swtbot.generator.client.commands.import.code" + contextId="org.eclipse.ui.contexts.window" + schemeId="org.eclipse.ui.defaultAcceleratorConfiguration" + sequence="Ctrl+Alt+Y"> + </key> + </extension> +</plugin> diff --git a/org.eclipse.swtbot.generator.client/pom.xml b/org.eclipse.swtbot.generator.client/pom.xml new file mode 100644 index 00000000..bf6b0187 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/pom.xml @@ -0,0 +1,13 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.eclipse.swtbot.generator.client</groupId> + <artifactId>org.eclipse.swtbot.generator.client</artifactId> + <packaging>eclipse-plugin</packaging> + <parent> + <groupId>org.eclipse.swtbot</groupId> + <artifactId>parent</artifactId> + <version>2.6.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> +</project>
\ No newline at end of file diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/Recorder.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/Recorder.java new file mode 100644 index 00000000..92ce73db --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/Recorder.java @@ -0,0 +1,442 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.core.ElementChangedEvent; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElementDelta; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Statement; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.widgets.Display; +import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.TextEdit; + +/** + * Recorder is a singleton which keeps track of recorder state information and + * the generated code received from the server. + */ +public enum Recorder implements RecorderClientCodeListener, RecorderClientStatusListener { + INSTANCE; + + private RecorderClient recorderClient; + private List<RecorderClientCodeListener> codeListeners; + private List<RecorderClientStatusListener> statusListeners; + + private boolean isInitialized = false; + private boolean isRecording; + private boolean isInsertingDirectlyInEditor; + private ConnectionState connectionState; + private IDocument document; + private IMethod selectedMethod; + private IDocument selectedMethodDocument; + + /** + * Initialize recorder. + */ + public void initialize() { + isInitialized = true; + + codeListeners = new ArrayList<RecorderClientCodeListener>(); + statusListeners = new ArrayList<RecorderClientStatusListener>(); + codeListeners.add(this); + statusListeners.add(this); + + document = new Document(); + + reset(); + } + + /** + * Reset recording state. + */ + public void reset() { + isRecording = false; + isInsertingDirectlyInEditor = false; + selectedMethod = null; + selectedMethodDocument = null; + connectionState = ConnectionState.DISCONNECTED; + } + + /** + * Start a recorder client session. + */ + public void startRecorderClient(int port) { + interruptRecorderClient(); + + connectionState = ConnectionState.CONNECTING; + recorderClient = new RecorderClient(codeListeners, statusListeners, port); + recorderClient.start(); + } + + /** + * Interrupt a recorder client session and reset recording state. + */ + public void interruptRecorderClient() { + if (recorderClient != null) { + recorderClient.interrupt(); + recorderClient.closeSocket(); + recorderClient = null; + } + + reset(); + } + + /** + * Takes care of new code. If a method is selected and + * <code>isInsertingDirectlyInEditor<code> is true, add code directly to + * editor. Otherwise add it to the recorder view document. + */ + @Override + public void codeGenerated(String code) { + if (isRecording == false) { + return; + } + + if (isInsertingDirectlyInEditor && selectedMethod != null && selectedMethodDocument != null) { + insertInEditor(code); + } else { + insertInView(code); + } + } + + @Override + public void connectionStarted() { + connectionState = ConnectionState.CONNECTED; + } + + @Override + public void connectionEnded() { + connectionState = ConnectionState.DISCONNECTED; + + reset(); + } + + /** + * Appends a row of code to the document contained in the recorder view. + * + * @param code + * The code to append. + */ + private void insertInView(final String code) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + if (document.get().length() == 0) { + document.set(code + ";"); + } else { + document.set(document.get() + "\n" + code + ";"); + } + } + }); + } + + /** + * Adds a new line of code to the currently selected method and the editor + * which contains this method. + * + * @param code + * The code to append. + */ + private void insertInEditor(String code) { + ICompilationUnit methodCompilationUnit = selectedMethod.getCompilationUnit(); + ASTParser parser = ASTParser.newParser(AST.JLS8); + parser.setSource(methodCompilationUnit); + parser.setResolveBindings(true); + parser.setBindingsRecovery(true); + ASTNode rootNode = parser.createAST(null); + CompilationUnit compilationUnit = (CompilationUnit) rootNode; + + // Create a visitor which finds all method declarations + MethodDeclarationVisitor methodDeclarationVisitor = new MethodDeclarationVisitor(); + compilationUnit.accept(methodDeclarationVisitor); + + // Search for the method declaration corresponding to selectedMethod + MethodDeclaration method = methodDeclarationVisitor.findMethodDeclaration(selectedMethod); + + final ASTRewrite rewrite = ASTRewrite.create(rootNode.getAST()); + ListRewrite listRewrite = rewrite.getListRewrite(method.getBody(), Block.STATEMENTS_PROPERTY); + Statement statement = (Statement) rewrite.createStringPlaceholder(code + ";", ASTNode.EMPTY_STATEMENT); + listRewrite.insertLast(statement, null); + + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + try { + TextEdit edits = rewrite.rewriteAST(); + try { + edits.apply(selectedMethodDocument); + } catch (MalformedTreeException e) { + e.printStackTrace(); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } catch (JavaModelException e1) { + e1.printStackTrace(); + } catch (IllegalArgumentException e1) { + e1.printStackTrace(); + } + }; + }); + } + + /** + * MethodDeclarationVisitor store all MethodDeclarations it can find and + * provides functionality for finding a specific MethodDeclaration with a + * corresponding IMethod. + */ + private class MethodDeclarationVisitor extends ASTVisitor { + private List<MethodDeclaration> methodDeclarations = new ArrayList<MethodDeclaration>(); + + @Override + public boolean visit(MethodDeclaration node) { + methodDeclarations.add(node); + return super.visit(node); + } + + /** + * Returns the MethodDeclaration from a corresponding IMethod. + * + * @param method + * The IMethod to search with. + * @return The first corresponding MethodDeclaration found, or null if + * no MethodDeclaration can be found. + */ + public MethodDeclaration findMethodDeclaration(IMethod method) { + for (MethodDeclaration methodDeclaration : methodDeclarations) { + if (methodDeclaration.resolveBinding().getJavaElement().equals(method)) { + return methodDeclaration; + } + } + + return null; + } + } + + /** + * Determines if UI which is related to code in the open editor needs to be + * updated. The method selection viewer should be updated if the document is + * closed or if a method is removed/added. + * + * @param event + * The received ElementChangedEvent. + * @return True if method selection viewer should be updated and false if + * not. + */ + public boolean shouldUpdateMethodSelectionViewer(ElementChangedEvent event) { + IJavaElementDelta delta = event.getDelta(); + + List<IJavaElementDelta> children = getAffectedLeafDeltas(delta); + for (IJavaElementDelta child : children) { + if (child.getElement() instanceof IMethod) { + return true; + } else if (child.getElement() instanceof ICompilationUnit) { + if (child.getKind() == IJavaElementDelta.CHANGED) { + return true; + } + } + } + + return false; + } + + /** + * Finds and returns the leaves of the change tree. + * + * @param delta + * The root to be iterated through. + * @return The leaf nodes. + */ + private List<IJavaElementDelta> getAffectedLeafDeltas(IJavaElementDelta delta) { + List<IJavaElementDelta> leaves = new ArrayList<IJavaElementDelta>(); + + return getAffectedLeafDeltasRecursively(delta, leaves); + } + + /** + * Helper method for <code>getAffectedLeafDeltas</code> which recursively + * iterates over a IJavaElementDelta and returns the leaf nodes of the + * change tree. + * + * @param delta + * The root to be iterated through. + * @param leaves + * The leaf nodes already found. + * @return The leaf nodes found. + */ + private List<IJavaElementDelta> getAffectedLeafDeltasRecursively(IJavaElementDelta delta, + List<IJavaElementDelta> leaves) { + for (IJavaElementDelta childDelta : delta.getAffectedChildren()) { + if (childDelta.getAffectedChildren().length == 0) { + // Found a leaf! + leaves.add(childDelta); + } else { + getAffectedLeafDeltasRecursively(childDelta, leaves); + } + } + + return leaves; + } + + /** + * ConnectionState describes the various possible connection states between + * the recorder client and server. + */ + public enum ConnectionState { + CONNECTED, CONNECTING, DISCONNECTED; + } + + public boolean isInsertingDirectlyInEditor() { + return isInsertingDirectlyInEditor; + } + + public void setInsertingDirectlyInEditor(boolean isInsertingDirectlyInEditor) { + this.isInsertingDirectlyInEditor = isInsertingDirectlyInEditor; + } + + public IMethod getSelectedMethod() { + return selectedMethod; + } + + /** + * Sets the selected method. It is important that this method is part of the + * selected method document. + * + * @param selectedMethod + * The selected method. + */ + public void setSelectedMethod(IMethod selectedMethod) { + this.selectedMethod = selectedMethod; + } + + public IDocument getSelectedMethodDocument() { + return selectedMethodDocument; + } + + /** + * Sets the seletctedMethodDocument. It is important that this document + * contains the selected method. + * + * @param selectedMethodDocument + * The document that the selected method resides in. + */ + public void setSelectedMethodDocument(IDocument selectedMethodDocument) { + this.selectedMethodDocument = selectedMethodDocument; + } + + /** + * Returns the text contained in the document. Use from UI thread. + * + * @return The text contained in the document. + */ + public String getDocumentText() { + return document.get(); + } + + /** + * Clears the document by setting the contents to an empty String. Use from + * UI thread. + */ + public void clearDocument() { + document.set(""); + } + + /** + * Returns the recorder document which contains recorded code. Use from UI + * thread. + * + * @return The document. + */ + public IDocument getDocument() { + return document; + } + + public boolean isRecording() { + return isRecording; + } + + public void setRecording(boolean isRecording) { + this.isRecording = isRecording; + } + + public boolean isInitialized() { + return isInitialized; + } + + public ConnectionState getConnectionState() { + return connectionState; + } + + public int getPort() { + return recorderClient.getPort(); + } + + /** + * Add a code listener to the list of code listeners. + * + * @param listener + * The listener to add. + * @return Specified by {@link Collection#add} + */ + public boolean addCodeListener(RecorderClientCodeListener listener) { + return codeListeners.add(listener); + } + + /** + * Removes the first occurrence of the specified listener from the list of + * code listeners. + * + * @param listener + * The listener to remove. + * @return Specified by {@link Collection#remove(Object)} + */ + public boolean removeCodeListener(RecorderClientCodeListener listener) { + return codeListeners.remove(listener); + } + + /** + * Add a status listener to the list of status listeners. + * + * @param listener + * The listener to add. + * @return Specified by {@link Collection#add} + */ + public boolean addStatusListener(RecorderClientStatusListener listener) { + return statusListeners.add(listener); + } + + /** + * Removes the first occurrence of the specified listener from the list of + * code listeners. + * + * @param listener + * The listener to remove. + * @return Specified by {@link Collection#remove(Object)} + */ + public boolean removeStatusListener(RecorderClientStatusListener listener) { + return statusListeners.remove(listener); + } +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClient.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClient.java new file mode 100644 index 00000000..32de406f --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClient.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ConnectException; +import java.net.Socket; +import java.util.List; + +/** + * RecorderClient is a client for RecorderServer and collects incoming generated + * code from a RecorderServer running on localhost. RecorderClient runs on a new + * thread. + */ +public class RecorderClient extends Thread { + + /** + * Refresh time in milliseconds when attempting to connect to host. + */ + private static final int REFRESH_TIME = 1000; + + private List<RecorderClientCodeListener> codeListeners; + private List<RecorderClientStatusListener> statusListeners; + private int port; + + private Socket socket; + + /** + * Creates a new RecorderClient. + * + * @param port + * The port to connect on. + */ + public RecorderClient(List<RecorderClientCodeListener> codeListeners, + List<RecorderClientStatusListener> statusListeners, int port) { + this.codeListeners = codeListeners; + this.statusListeners = statusListeners; + this.port = port; + } + + /** + * Attempts to close the the socket of the recorder client. This is + * necessary to interrupt the thread while the thread is stuck in reading + * the next line of the input stream. + * + * If the socket is null or already closed, do nothing and return. + */ + public void closeSocket() { + if (socket == null || socket.isClosed()) { + return; + } + + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void run() { + socket = connect("localhost", port); + + if (socket == null) { + for (RecorderClientStatusListener listener : statusListeners) { + listener.connectionEnded(); + } + throw new RuntimeException("Could not create socket for client"); + } + + for (RecorderClientStatusListener listener : statusListeners) { + listener.connectionStarted(); + } + + BufferedReader in; + try { + in = new BufferedReader( + new InputStreamReader(socket.getInputStream())); + String line; + + while ((line = in.readLine()) != null && !interrupted()) { + for (RecorderClientCodeListener listener : codeListeners) { + listener.codeGenerated(line); + } + } + + for (RecorderClientStatusListener listener : statusListeners) { + listener.connectionEnded(); + } + + socket.close(); + } catch (IOException e) { + // Client was shut down through closeSocket() and readLine threw + // an exception. + return; + } + } + + /** + * Attempts to connect to host. If the host is not found, wait + * <code>REFRESH_TIME</code> milliseconds and try again. Once connected, + * return. + * + * @param host + * The host name to connect to. + * @param port + * The port to connect to. + * @return The host socket. + */ + private Socket connect(String host, int port) { + Socket socket; + + while (true) { + try { + socket = new Socket(host, port); + return socket; + } catch (ConnectException e) { + try { + Thread.sleep(REFRESH_TIME); + } catch (InterruptedException e1) { + return null; + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public int getPort() { + return port; + } +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClientCodeListener.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClientCodeListener.java new file mode 100644 index 00000000..c52e1395 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClientCodeListener.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client; + +/** + * An interface for objects interested in receiving generated code as it is + * received. + */ +public interface RecorderClientCodeListener { + /** + * Called when new code has been received. + * + * @param code + * The generated code. + */ + public void codeGenerated(String code); +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClientStatusListener.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClientStatusListener.java new file mode 100644 index 00000000..91f8a6f5 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/RecorderClientStatusListener.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client; + +/** + * An interface for objects interested in connection status between recorder + * server and client. + */ +public interface RecorderClientStatusListener { + + /** + * Called when a connection between server and client has started. + */ + public void connectionStarted(); + + /** + * Called when a connection between server and client has ended. + */ + public void connectionEnded(); +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/SWTBotRecorderClientPlugin.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/SWTBotRecorderClientPlugin.java new file mode 100644 index 00000000..76391d58 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/SWTBotRecorderClientPlugin.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle + */ +public class SWTBotRecorderClientPlugin extends AbstractUIPlugin { + + // The plug-in ID + public static final String PLUGIN_ID = "org.eclipse.swtbot.generator.client"; //$NON-NLS-1$ + + // The shared instance + private static SWTBotRecorderClientPlugin plugin; + + /** + * The constructor + */ + public SWTBotRecorderClientPlugin() { + + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework. + * BundleContext) + */ + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework. + * BundleContext) + */ + public void stop(BundleContext context) throws Exception { + Recorder.INSTANCE.interruptRecorderClient(); + + plugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static SWTBotRecorderClientPlugin getDefault() { + return plugin; + } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + * + * @param path + * the path + * @return the image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/commands/ImportClientCodeHandler.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/commands/ImportClientCodeHandler.java new file mode 100644 index 00000000..8787b028 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/commands/ImportClientCodeHandler.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client.commands; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swtbot.generator.client.Recorder; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.AbstractTextEditor; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; + +/** + * ImportClientViewCodeHandler moves generated code to the cursor position of + * the active editor and then clears the generated code buffer. + */ +public class ImportClientCodeHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + addTextToEditorAtCursor(Recorder.INSTANCE.getDocumentText()); + + Recorder.INSTANCE.clearDocument(); + + return null; + } + + /** + * Adds a string to the currently selected location within a text editor. + * + * @param text + * The text to insert. + */ + private void addTextToEditorAtCursor(String text) throws ExecutionException { + IWorkbenchWindow activeWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + IWorkbenchPage page = activeWindow.getActivePage(); + IEditorPart editorPart = page.getActiveEditor(); + + if (editorPart instanceof AbstractTextEditor == false) { + throw new ExecutionException("Code can only be inserted within a text editor"); + } + + ITextEditor editor = (ITextEditor) editorPart; + IDocumentProvider documentProvider = editor.getDocumentProvider(); + IDocument document = documentProvider.getDocument(editor.getEditorInput()); + + ISelection selection = editor.getSelectionProvider().getSelection(); + if (selection instanceof TextSelection == false) { + throw new ExecutionException("Could not find text selection"); + } + + int offset = ((TextSelection) selection).getOffset(); + try { + document.replace(offset, 0, text); + } catch (BadLocationException e) { + throw new ExecutionException("Could not insert code: " + e.getMessage()); + } + } +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/launcher/RecorderServerLaunchConfiguration.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/launcher/RecorderServerLaunchConfiguration.java new file mode 100644 index 00000000..05ded2e7 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/launcher/RecorderServerLaunchConfiguration.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client.launcher; + +import javax.print.attribute.standard.Severity; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.pde.launching.EclipseApplicationLaunchConfiguration; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swtbot.generator.client.Recorder; +import org.eclipse.swtbot.generator.client.SWTBotRecorderClientPlugin; +import org.eclipse.swtbot.generator.client.views.RecorderClientView; +import org.eclipse.swtbot.generator.server.StartupRecorderServer; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; + +/** + * Launch configuration for starting a new Eclipse application with the SWTBot + * recorder running in the background as a server. + */ +public class RecorderServerLaunchConfiguration extends EclipseApplicationLaunchConfiguration { + + /** + * Launches an Eclipse application and then starts connecting with the + * client recorder. + */ + @Override + public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) + throws CoreException { + + super.launch(configuration, mode, launch, monitor); + + int port = getServerPort(configuration); + + if (port == -1) { + Status status = new Status(Severity.ERROR.getValue(), SWTBotRecorderClientPlugin.PLUGIN_ID, + "Could not find a port number in the launch arguments"); + throw new CoreException(status); + } + + startClientRecorder(port); + } + + /** + * Returns the port that the server was launched with. + * + * @param configuration + * The launch configuration used when launching the Eclipse + * application. + * @return The port number or '-1' if port could not be parsed correctly. + * @throws CoreException + * If attribute from configuration cannot be parsed. + */ + private int getServerPort(ILaunchConfiguration configuration) throws CoreException { + String launchArguments = configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, ""); + String[] vmArguments = launchArguments.split(" "); + for (int i = 0; i < vmArguments.length; i++) { + String vmArgument = vmArguments[i]; + if (vmArgument.contains(StartupRecorderServer.ENABLED_WITH_PORT)) { + String[] portArgumentKeyValue = vmArgument.split("="); + if (portArgumentKeyValue.length == 2) { + return Integer.parseInt(portArgumentKeyValue[1]); + } + } + } + + return -1; + } + + /** + * Starts the recorder client on a certain port asynchronously on UI thread. + * + * @param port + * The port to start the recorder client on. + */ + private void startClientRecorder(final int port) { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + RecorderClientView view = getRecorderClientView(); + if (view == null || !(view instanceof RecorderClientView)) { + // Cannot find RecorderClientView, try to open it. + try { + openRecorderClientView(); + view = getRecorderClientView(); + } catch (PartInitException e) { + throw new RuntimeException("Could not open RecorderClientView: " + e.getMessage()); + } + } + + Recorder.INSTANCE.startRecorderClient(port); + view.updateUI(); + } + }); + } + + /** + * Finds the RecorderClientView. Should be called on the UI thread. + * + * @return The RecorderClientView + */ + private RecorderClientView getRecorderClientView() { + return (RecorderClientView) PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage() + .findView(RecorderClientView.ID); + } + + /** + * Opens the RecorderClientView. Should be called on the UI thread. + * + * @throws PartInitException + * If the view could not be opened. + */ + private void openRecorderClientView() throws PartInitException { + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().showView(RecorderClientView.ID); + } +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/launcher/RecorderServerLauncherTabGroup.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/launcher/RecorderServerLauncherTabGroup.java new file mode 100644 index 00000000..e68feaa1 --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/launcher/RecorderServerLauncherTabGroup.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client.launcher; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.pde.ui.launcher.EclipseLauncherTabGroup; +import org.eclipse.swtbot.generator.server.StartupRecorderServer; + +/** + * Appends <code>StartupRecorderServer.ENABLED_WITH_PORT</code> to the VM + * arguments. + */ +public class RecorderServerLauncherTabGroup extends EclipseLauncherTabGroup { + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy configuration) { + super.setDefaults(configuration); + + try { + String launchArguments = configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, + ""); + String launchArgument = "\n-D" + StartupRecorderServer.ENABLED_WITH_PORT + "=" + 8000; + configuration.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, + launchArguments + " " + launchArgument); + + configuration.doSave(); + } catch (CoreException e1) { + e1.printStackTrace(); + } + } +} diff --git a/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/views/RecorderClientView.java b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/views/RecorderClientView.java new file mode 100644 index 00000000..30284fbf --- /dev/null +++ b/org.eclipse.swtbot.generator.client/src/org/eclipse/swtbot/generator/client/views/RecorderClientView.java @@ -0,0 +1,477 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.client.views; + +import org.eclipse.jdt.core.ElementChangedEvent; +import org.eclipse.jdt.core.IElementChangedListener; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; +import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.source.VerticalRuler; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ComboViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swtbot.generator.client.Recorder; +import org.eclipse.swtbot.generator.client.Recorder.ConnectionState; +import org.eclipse.swtbot.generator.client.RecorderClientStatusListener; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; +import org.eclipse.ui.plugin.AbstractUIPlugin; + +/** + * RecorderClientView shows SWTBot recorder output as it is received from a + * RecorderServer. + */ +public class RecorderClientView extends ViewPart implements RecorderClientStatusListener, IElementChangedListener { + + /** + * The ID of the view as specified by the extension. + */ + public static final String ID = "org.eclipse.swtbot.generator.client.view.recorder.client"; + + private static int DEFAULT_PORT_NUMBER = 8000; + + private static String IS_CONNECTED_LABEL = "Connected"; + private static String TRYING_CONNECT_LABEL = "Connecting..."; + private static String NOT_CONNECTED_LABEL = "No connection"; + + private static String CONNECT_LABEL = " Connect "; + private static String CANCEL_CONNECT_LABEL = "Cancel"; + private static String DISCONNECT_LABEL = "Disconnect"; + + private static String ADD_TO_METHOD_TOGGLE_LABEL = "Add to method: "; + + private static String START_RECORDING_LABEL = "Record"; + private static String STOP_RECORDING_LABEL = "Stop recording"; + + private Button connectButton; + private Text portText; + private Label statusLabel; + + private ComboViewer availableMethodsDropDown; + private Button addToMethodToggle; + private Button refreshAvailableMethodsButton; + + private Button recordingButton; + private SourceViewer viewer; + + /** + * Initialize the Recorder singleton. If it already is initialized, reset + * it. + */ + public RecorderClientView() { + if (Recorder.INSTANCE.isInitialized() == false) { + Recorder.INSTANCE.initialize(); + } else { + Recorder.INSTANCE.reset(); + } + + Recorder.INSTANCE.addStatusListener(this); + } + + @Override + public void dispose() { + Recorder.INSTANCE.removeStatusListener(this); + super.dispose(); + } + + /** + * Updates the UI state for any connection state change. + */ + public void updateUI() { + updateUIForConnectionState(Recorder.INSTANCE.getConnectionState()); + portText.setText(String.valueOf(Recorder.INSTANCE.getPort())); + } + + /** + * This is a callback that will allow us to create the viewer and initialize + * it. + */ + public void createPartControl(Composite parent) { + Group group = new Group(parent, SWT.NONE); + group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + group.setLayout(new GridLayout(1, false)); + + Composite launchContainer = new Composite(group, SWT.NONE); + launchContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + launchContainer.setLayout(new GridLayout(2, false)); + + connectButton = new Button(launchContainer, SWT.PUSH); + connectButton.setText(CONNECT_LABEL); + connectButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + connectButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (Recorder.INSTANCE.getConnectionState() == ConnectionState.CONNECTED) { + Recorder.INSTANCE.interruptRecorderClient(); + } else if (Recorder.INSTANCE.getConnectionState() == ConnectionState.CONNECTING) { + Recorder.INSTANCE.interruptRecorderClient(); + } else { + Recorder.INSTANCE.startRecorderClient(getPort()); + } + updateUIForConnectionState(Recorder.INSTANCE.getConnectionState()); + } + }); + + Composite portLaunchContainer = new Composite(launchContainer, SWT.NONE); + portLaunchContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + portLaunchContainer.setLayout(new GridLayout(2, false)); + + Label portLabel = new Label(portLaunchContainer, SWT.NONE); + portLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + portLabel.setText("port:"); + + portText = new Text(portLaunchContainer, SWT.RIGHT); + portText.setText(String.valueOf(DEFAULT_PORT_NUMBER)); + portText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + portText.addVerifyListener(new VerifyListener() { + @Override + public void verifyText(VerifyEvent e) { + String port = portText.getText() + e.text; + + try { + Integer.parseInt(port); + } catch (NumberFormatException e2) { + if (port.length() > 0) { + e.doit = false; + } + } + } + }); + + statusLabel = new Label(group, SWT.SHADOW_IN); + statusLabel.setText("No connection"); + + Label horizontalSeparator = new Label(group, SWT.SEPARATOR | SWT.HORIZONTAL); + horizontalSeparator.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + + Composite methodSelectionContainer = new Composite(group, SWT.NONE); + methodSelectionContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + methodSelectionContainer.setLayout(new GridLayout(3, false)); + + addToMethodToggle = new Button(methodSelectionContainer, SWT.CHECK); + addToMethodToggle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + addToMethodToggle.setText(ADD_TO_METHOD_TOGGLE_LABEL); + addToMethodToggle.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (Recorder.INSTANCE.isInsertingDirectlyInEditor()) { + Recorder.INSTANCE.setInsertingDirectlyInEditor(false); + availableMethodsDropDown.getControl().setEnabled(false); + refreshAvailableMethodsButton.setEnabled(false); + } else { + Recorder.INSTANCE.setInsertingDirectlyInEditor(true); + availableMethodsDropDown.getControl().setEnabled(true); + refreshAvailableMethodsButton.setEnabled(true); + + // If the list is empty we refresh. + if (availableMethodsDropDown.getInput() == null + || ((IMethod[]) availableMethodsDropDown.getInput()).length == 0) { + updateMethodSelectionDropDown(); + } + } + } + }); + addToMethodToggle.setEnabled(false); + + availableMethodsDropDown = new ComboViewer(methodSelectionContainer); + availableMethodsDropDown.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + availableMethodsDropDown.getControl().setEnabled(false); + availableMethodsDropDown.setContentProvider(ArrayContentProvider.getInstance()); + availableMethodsDropDown.setLabelProvider(new LabelProvider() { + @Override + public String getText(Object element) { + IMethod method = (IMethod) element; + + String parameterNamesString = ""; + try { + String[] parameterNames = method.getParameterNames(); + for (int i = 0; i < parameterNames.length; i++) { + if (i == 0) { + parameterNamesString += parameterNames[i]; + } else { + parameterNamesString += ", " + parameterNames[i]; + } + } + + } catch (JavaModelException e) { + e.printStackTrace(); + } + + return method.getElementName() + "(" + parameterNamesString + ")"; + } + }); + availableMethodsDropDown.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + Recorder.INSTANCE.setSelectedMethod((IMethod) selection.getFirstElement()); + } + }); + + refreshAvailableMethodsButton = new Button(methodSelectionContainer, SWT.PUSH); + refreshAvailableMethodsButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + refreshAvailableMethodsButton.setEnabled(false); + refreshAvailableMethodsButton.setImage(AbstractUIPlugin + .imageDescriptorFromPlugin("org.eclipse.swtbot.generator.client", "icons/refresh.gif").createImage()); + refreshAvailableMethodsButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateMethodSelectionDropDown(); + } + }); + + recordingButton = new Button(group, SWT.PUSH); + recordingButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + recordingButton.setText(START_RECORDING_LABEL); + recordingButton.setEnabled(false); + recordingButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (Recorder.INSTANCE.isRecording()) { + recordingButton.setText(START_RECORDING_LABEL); + Recorder.INSTANCE.setRecording(false); + } else { + recordingButton.setText(STOP_RECORDING_LABEL); + Recorder.INSTANCE.setRecording(true); + } + } + }); + + viewer = new SourceViewer(group, new VerticalRuler(0), SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER); + viewer.setDocument(Recorder.INSTANCE.getDocument()); + viewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + getSite().setSelectionProvider(viewer); + + JavaCore.addElementChangedListener(this); + updateUIForConnectionState(Recorder.INSTANCE.getConnectionState()); + updateMethodSelectionDropDown(); + } + + /** + * Passing the focus request to the viewer's control. + */ + public void setFocus() { + viewer.getControl().setFocus(); + } + + /** + * @return The currently chosen port. + */ + private int getPort() { + return Integer.parseInt(portText.getText()); + } + + @Override + public void connectionStarted() { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + updateMethodSelectionDropDown(); + updateUIForConnectionState(ConnectionState.CONNECTED); + } + }); + } + + @Override + public void connectionEnded() { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + updateUIForConnectionState(ConnectionState.DISCONNECTED); + } + }); + } + + /** + * Updates all UI elements to match a new connection state. + * + * @param state + * The new connection state. + */ + private void updateUIForConnectionState(ConnectionState state) { + switch (state) { + case CONNECTED: + statusLabel.setText(IS_CONNECTED_LABEL); + recordingButton.setText(START_RECORDING_LABEL); + connectButton.setText(DISCONNECT_LABEL); + addToMethodToggle.setSelection(false); + + recordingButton.setEnabled(true); + connectButton.setEnabled(true); + portText.setEnabled(false); + addToMethodToggle.setEnabled(true); + availableMethodsDropDown.getControl().setEnabled(false); + refreshAvailableMethodsButton.setEnabled(false); + + break; + case CONNECTING: + statusLabel.setText(TRYING_CONNECT_LABEL); + recordingButton.setText(START_RECORDING_LABEL); + connectButton.setText(CANCEL_CONNECT_LABEL); + addToMethodToggle.setSelection(false); + + recordingButton.setEnabled(false); + connectButton.setEnabled(true); + portText.setEnabled(false); + addToMethodToggle.setEnabled(false); + availableMethodsDropDown.getControl().setEnabled(false); + refreshAvailableMethodsButton.setEnabled(false); + + break; + case DISCONNECTED: + statusLabel.setText(NOT_CONNECTED_LABEL); + recordingButton.setText(START_RECORDING_LABEL); + connectButton.setText(CONNECT_LABEL); + addToMethodToggle.setSelection(false); + + recordingButton.setEnabled(false); + connectButton.setEnabled(true); + portText.setEnabled(true); + addToMethodToggle.setEnabled(false); + availableMethodsDropDown.getControl().setEnabled(false); + refreshAvailableMethodsButton.setEnabled(false); + + break; + } + } + + /** + * Updates the method selection drop down if the change event is likely to + * affect the currently available methods in the method selection drop down. + */ + @Override + public void elementChanged(ElementChangedEvent event) { + if (Recorder.INSTANCE.shouldUpdateMethodSelectionViewer(event)) { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + updateMethodSelectionDropDown(); + } + }); + } + } + + /** + * Returns the active editor or null if it fails. + * + * @return The active editor. + */ + private IEditorPart getActiveEditor() { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return null; + } + IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow(); + if (workbenchWindow == null) { + return null; + } + IWorkbenchPage workbenchPage = workbenchWindow.getActivePage(); + if (workbenchPage == null) { + return null; + } + return workbenchPage.getActiveEditor(); + } + + /** + * Updates the method selection drop down. + */ + @SuppressWarnings("restriction") + private void updateMethodSelectionDropDown() { + IEditorPart activeEditor = getActiveEditor(); + + if (activeEditor == null) { + Recorder.INSTANCE.setSelectedMethod(null); + Recorder.INSTANCE.setSelectedMethodDocument(null); + + availableMethodsDropDown.setInput(null); + } else if (activeEditor instanceof JavaEditor) { + ITypeRoot root = EditorUtility.getEditorInputJavaElement(activeEditor, false); + IType type = root.findPrimaryType(); + final IMethod[] methods; + try { + methods = type.getMethods(); + + IMethod methodToSelect = null; + if (Recorder.INSTANCE.getSelectedMethod() != null) { + methodToSelect = findSimilarMethod(Recorder.INSTANCE.getSelectedMethod(), methods); + } + IDocument currentActiveDocument = ((JavaEditor) activeEditor).getDocumentProvider() + .getDocument(activeEditor.getEditorInput()); + + Recorder.INSTANCE.setSelectedMethod(methodToSelect); + Recorder.INSTANCE.setSelectedMethodDocument(currentActiveDocument); + availableMethodsDropDown.setInput(methods); + + // If methodToSelect is non-null we want to select it to + // maintain the user's previous selection. + if (methodToSelect != null) { + ISelection selection = new StructuredSelection(methodToSelect); + availableMethodsDropDown.setSelection(selection); + } + } catch (JavaModelException e) { + e.printStackTrace(); + } + } + } + + /** + * Tries to find a similar method in an array of IMethod instances using + * <code>IMethod.isSimilar</code> + * + * @param method + * The method to search for. + * @param methods + * The array to search through. + * @return The first similar method found or null if no similar method was + * found. + */ + private IMethod findSimilarMethod(IMethod method, IMethod[] methods) { + IMethod similarMethod = null; + for (IMethod candidate : methods) { + if (method.isSimilar(candidate)) { + similarMethod = candidate; + break; + } + } + + return similarMethod; + } +} diff --git a/org.eclipse.swtbot.generator.feature/feature.xml b/org.eclipse.swtbot.generator.feature/feature.xml index 9ad7569f..63ac17cb 100644 --- a/org.eclipse.swtbot.generator.feature/feature.xml +++ b/org.eclipse.swtbot.generator.feature/feature.xml @@ -48,4 +48,11 @@ version="0.0.0" unpack="false"/> + <plugin + id="org.eclipse.swtbot.generator.client" + download-size="0" + install-size="0" + version="0.0.0" + unpack="false"/> + </feature> diff --git a/org.eclipse.swtbot.generator/META-INF/MANIFEST.MF b/org.eclipse.swtbot.generator/META-INF/MANIFEST.MF index b5738dcc..88dda16a 100644 --- a/org.eclipse.swtbot.generator/META-INF/MANIFEST.MF +++ b/org.eclipse.swtbot.generator/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Bundle-ClassPath: . Export-Package: org.eclipse.swtbot.generator, org.eclipse.swtbot.generator.framework, org.eclipse.swtbot.generator.listener, + org.eclipse.swtbot.generator.server, org.eclipse.swtbot.generator.ui Require-Bundle: org.eclipse.ui, org.eclipse.core.runtime, diff --git a/org.eclipse.swtbot.generator/plugin.xml b/org.eclipse.swtbot.generator/plugin.xml index 6092b795..873206b0 100644 --- a/org.eclipse.swtbot.generator/plugin.xml +++ b/org.eclipse.swtbot.generator/plugin.xml @@ -9,6 +9,9 @@ <startup class="org.eclipse.swtbot.generator.ui.StartupRecorder"> </startup> + <startup + class="org.eclipse.swtbot.generator.server.StartupRecorderServer"> + </startup> </extension> <extension point="org.eclipse.swtbot.generator.botGeneratorSupport"> diff --git a/org.eclipse.swtbot.generator/src/org/eclipse/swtbot/generator/server/RecorderServer.java b/org.eclipse.swtbot.generator/src/org/eclipse/swtbot/generator/server/RecorderServer.java new file mode 100644 index 00000000..c1f9d93b --- /dev/null +++ b/org.eclipse.swtbot.generator/src/org/eclipse/swtbot/generator/server/RecorderServer.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: Isaac Arvestad (Ericsson) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swtbot.generator.server; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; + +import org.eclipse.swtbot.generator.framework.GenerationRule; +import org.eclipse.swtbot.generator.ui.BotGeneratorEventDispatcher; +import org.eclipse.swtbot.generator.ui.BotGeneratorEventDispatcher.CodeGenerationListener; + +/** + * RecorderServer is a server which prints out SWTBot recorder events to all + * connected clients. + */ +public class RecorderServer { + + private BotGeneratorEventDispatcher recorder; + + /** + * Creates a new RecorderServer with a SWTBot recorder. + * + * @param recorder + * The recorder. + */ + public RecorderServer(BotGeneratorEventDispatcher recorder) { + this.recorder = recorder; + } + + /** + * Starts the server. + * + * @param port + * The port the server listens for connections on. + */ + public void start(int port) { + recorder.setRecording(true); + + ConnectionListener connectionListener = new ConnectionListener(port); + connectionListener.start(); + } + + /** + * ConnectionListener listens for collections on a separate thread. + */ + private class ConnectionListener extends Thread { + + private int port; + + /** + * Creates a ConnectionListener with a certain port. + * + * @param port + * The port to listen on. + */ + public ConnectionListener(int port) { + this.port = port; + } + + @Override + public void run() { + try { + ServerSocket serverSocket = new ServerSocket(port); + + while (!interrupted()) { + ConnectionHandler connectionHandler = new ConnectionHandler(serverSocket.accept()); + recorder.addListener(connectionHandler); + } + + serverSocket.close(); + } catch (Exception e) { + throw new RuntimeException( + "Could not start server - There was a problem starting the recorder server. Try restarting using a different port number."); + } + } + } + + /** + * ConnectionHandler handles a connection which ConnectionListener has + * accepted. + */ + private class ConnectionHandler implements CodeGenerationListener { + + private PrintWriter output; + + /** + * Creates a new ConnectionHandler. + * + * @param socket + * The connecting socket. + */ + public ConnectionHandler(Socket socket) { + try { + output = new PrintWriter(socket.getOutputStream(), true); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Sends any generated code to the client. + * + * @param code + * The generated code. + */ + public void handleCodeGenerated(GenerationRule code) { + for (String text : code.getActions()) { + output.println(text); + } + } + } +} diff --git a/org.eclipse.swtbot.generator/src/org/eclipse/swtbot/generator/server/StartupRecorderServer.java b/org.eclipse.swtbot.generator/src/org/eclipse/swtbot/generator/server/StartupRecorderServer.java new file mode 100644 index 00000000..601bb214 --- /dev/null +++ b/org.eclipse.swtbot.generator/src/org/eclipse/swtbot/generator/server/StartupRecorderServer.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2016 Ericsson + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Isaac Arvestad (Ericsson) - initial API and implementation (Based on: org.eclipse.swtbot.generator.ui.StartupRecorder) + *******************************************************************************/ +package org.eclipse.swtbot.generator.server; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swtbot.generator.framework.Generator; +import org.eclipse.swtbot.generator.listener.WorkbenchListener; +import org.eclipse.swtbot.generator.ui.BotGeneratorEventDispatcher; +import org.eclipse.swtbot.generator.ui.GeneratorExtensionPointManager; +import org.eclipse.ui.IStartup; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; + +/** + * StartupRecorderServer handles starting a SWTBot recorder and a + * RecorderServer. + */ +public class StartupRecorderServer implements IStartup { + + public static final String ENABLED_WITH_PORT = "org.eclipse.swtbot.generator.server.enable"; + + private static final int[] monitoredEvents = new int[] { SWT.Activate, SWT.Close, SWT.Selection, SWT.Expand, + SWT.Modify, SWT.MouseDown, SWT.MouseDoubleClick, SWT.KeyDown, SWT.Close }; + + /** + * StartRecorderServerRunnable starts the SWTBot recorder and the + * RecorderServer on a new thread. + */ + private static final class StartRecorderServerRunnable implements Runnable { + private final Display display; + private int port; + + /** + * Creates a new StartRecorderServerRunnable. + * + * @param display + * The display used. + * @param port + * The port used for the RecorderServer. + */ + public StartRecorderServerRunnable(Display display, int port) { + this.display = display; + this.port = port; + } + + public void run() { + final List<Generator> availableGenerators = GeneratorExtensionPointManager.loadGenerators(); + Generator generator = availableGenerators.get(0); + final BotGeneratorEventDispatcher dispatcher = new BotGeneratorEventDispatcher(); + dispatcher.setGenerator(generator); + + List<Shell> ignoreList = new ArrayList<Shell>(); + dispatcher.ignoreShells(ignoreList); + + for (int monitoredEvent : monitoredEvents) { + this.display.addFilter(monitoredEvent, dispatcher); + } + if (PlatformUI.isWorkbenchRunning()) { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + if (page != null) { + page.addPartListener(new WorkbenchListener(dispatcher)); + } + } + + RecorderServer recorderServer = new RecorderServer(dispatcher); + recorderServer.start(port); + } + } + + /** + * Start the SWTBot recorder and the RecorderServer on a new thread. + * + * @param port + * The port to use for the RecorderServer. + */ + public void start(int port) { + final Display display = Display.getDefault(); + StartRecorderServerRunnable serverRunnable = new StartRecorderServerRunnable(display, port); + + display.syncExec(serverRunnable); + } + + /** + * Starts recorder server immediately once Eclipse has launched if the + * argument ENABLED_WITH_PORT can be found and it is equal to an integer + * which is used as the port number. + */ + public void earlyStartup() { + if (System.getProperty(ENABLED_WITH_PORT) == null) { + return; + } + try { + int port = Integer.parseInt(System.getProperty(ENABLED_WITH_PORT)); + start(port); + } catch (NumberFormatException e) { + System.out.println("SWTBot recorder server launch aborted. " + ENABLED_WITH_PORT + + " argument must be assigned an integer as a port number"); + return; + } + } +} @@ -50,6 +50,7 @@ Authors: <module>org.eclipse.swtbot.eclipse.ui</module> <module>org.eclipse.swtbot.forms.finder</module> <module>org.eclipse.swtbot.generator</module> + <module>org.eclipse.swtbot.generator.client</module> <module>org.eclipse.swtbot.generator.rules.workbench</module> <module>org.eclipse.swtbot.generator.jdt</module> <module>org.eclipse.swtbot.generator.ui</module> |