package com.twasyl.slideshowfx.snippet.executor.groovy;
import com.sun.javafx.PlatformUtil;
import com.twasyl.slideshowfx.global.configuration.GlobalConfiguration;
import com.twasyl.slideshowfx.snippet.executor.AbstractSnippetExecutor;
import com.twasyl.slideshowfx.snippet.executor.CodeSnippet;
import com.twasyl.slideshowfx.utils.beans.converter.FileStringConverter;
import com.twasyl.slideshowfx.utils.io.DefaultCharsetReader;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import java.io.*;
import java.util.StringJoiner;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An implementation of {@link com.twasyl.slideshowfx.snippet.executor.AbstractSnippetExecutor} that allows to execute
* Java code snippets.
* This implementation is identified with the code {@code JAVA}.
*
* @author Thierry Wasyczenko
* @version 1.0
* @since SlideshowFX 1.0
*/
public class GroovySnippetExecutor extends AbstractSnippetExecutor<GroovySnippetExecutorOptions> {
private static final Logger LOGGER = Logger.getLogger(GroovySnippetExecutor.class.getName());
private static final String GROOVY_HOME_PROPERTY_SUFFIX = ".home";
/**
* Indicates if the code should be wrapped in a main or run method (depending it is a Groovy Script or Class)
*/
protected static final String WRAP_IN_METHOD_RUNNER = "wrapInMethodRunner";
protected static final String IMPORTS_PROPERTY = "imports";
protected static final String CLASS_NAME_PROPERTY = "class";
protected static final String MAKE_SCRIPT = "makeScript";
public GroovySnippetExecutor() {
super("GROOVY", "Groovy", "language-groovy");
this.setOptions(new GroovySnippetExecutorOptions());
final String groovyHome = GlobalConfiguration.getProperty(this.getConfigurationBaseName().concat(GROOVY_HOME_PROPERTY_SUFFIX));
if(groovyHome != null) {
try {
this.getOptions().setGroovyHome(new File(groovyHome));
} catch (FileNotFoundException e) {
LOGGER.log(Level.SEVERE, "Can not set the GROOVY_HOME", e);
}
}
}
@Override
public Parent getUI(final CodeSnippet codeSnippet) {
final TextField classTextField = new TextField();
classTextField.setPromptText("Class name");
classTextField.setPrefColumnCount(10);
classTextField.setTooltip(new Tooltip("The class name of this code snippet"));
classTextField.textProperty().addListener((textValue, oldText, newText) -> {
if (newText == null || newText.isEmpty()) codeSnippet.putProperty(CLASS_NAME_PROPERTY, null);
else codeSnippet.putProperty(CLASS_NAME_PROPERTY, newText);
});
final StringProperty codeEncapsulationType = new SimpleStringProperty("main");
final Tooltip wrapInTooltip = new Tooltip();
wrapInTooltip.textProperty().bind(new SimpleStringProperty("Wrap the provided code snippet in a Groovy ").concat(codeEncapsulationType).concat(" method"));
final CheckBox wrapInMethodRunner = new CheckBox();
wrapInMethodRunner.textProperty().bind(new SimpleStringProperty("Wrap code snippet in ").concat(codeEncapsulationType));
wrapInMethodRunner.setTooltip(wrapInTooltip);
wrapInMethodRunner.selectedProperty().addListener((selectedValue, oldSelected, newSelected) -> {
if(newSelected != null) codeSnippet.putProperty(WRAP_IN_METHOD_RUNNER, newSelected.toString());
});
final CheckBox makeScript = new CheckBox("Make Groovy Script");
makeScript.setTooltip(new Tooltip("Create a Groovy Script instead of a Groovy class"));
makeScript.selectedProperty().addListener((selectedValue, oldSelected, newSelected) -> {
if(newSelected != null) codeSnippet.putProperty(MAKE_SCRIPT, newSelected.toString());
if(newSelected != null && newSelected) codeEncapsulationType.set("script");
else codeEncapsulationType.set("main");
});
final TextArea imports = new TextArea();
imports.setPromptText("Imports");
imports.setPrefColumnCount(15);
imports.setPrefRowCount(15);
imports.setWrapText(true);
imports.textProperty().addListener((textValue, oldText, newText) -> {
if(newText.isEmpty()) codeSnippet.putProperty(IMPORTS_PROPERTY, null);
else codeSnippet.putProperty(IMPORTS_PROPERTY, newText);
});
final VBox ui = new VBox(5);
ui.getChildren().addAll(classTextField, wrapInMethodRunner, makeScript, imports);
return ui;
}
@Override
public Node getConfigurationUI() {
this.newOptions = new GroovySnippetExecutorOptions();
try {
this.newOptions.setGroovyHome(this.getOptions().getGroovyHome());
} catch (FileNotFoundException | NullPointerException e) {
LOGGER.log(Level.FINE, "Can not duplicate GROOVY_HOME", e);
}
final Label label = new Label(this.getLanguage().concat(":"));
final TextField javaHomeField = new TextField();
javaHomeField.textProperty().bindBidirectional(this.newOptions.groovyHomeProperty(), new FileStringConverter());
javaHomeField.setPrefColumnCount(20);
final Button browse = new Button("...");
browse.setOnAction(event -> {
final DirectoryChooser chooser = new DirectoryChooser();
final File sdkHomeDir = chooser.showDialog(null);
if(sdkHomeDir != null) {
javaHomeField.setText(sdkHomeDir.getAbsolutePath());
}
});
final HBox box = new HBox(5);
box.getChildren().addAll(label, javaHomeField, browse);
return box;
}
@Override
public void saveNewOptions() {
if(this.getNewOptions() != null) {
this.setOptions(this.getNewOptions());
if(this.getOptions().getGroovyHome() != null) {
GlobalConfiguration.setProperty(this.getConfigurationBaseName().concat(GROOVY_HOME_PROPERTY_SUFFIX),
this.getOptions().getGroovyHome().getAbsolutePath().replaceAll("\\\\", "/"));
}
}
}
@Override
public ObservableList<String> execute(final CodeSnippet codeSnippet) {
final ObservableList<String> consoleOutput = FXCollections.observableArrayList();
final Thread snippetThread = new Thread(() -> {
File codeFile = null;
try {
codeFile = createSourceCodeFile(codeSnippet);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not write code to snippet file", e);
consoleOutput.add("ERROR: ".concat(e.getMessage()));
}
// Execute the class
final File groovyExecutable = PlatformUtil.isWindows() ?
new File(this.getOptions().getGroovyHome(), "bin/groovy.bat") :
new File(this.getOptions().getGroovyHome(), "bin/groovy");
final String[] executionCommand = {groovyExecutable.getAbsolutePath(), codeFile.getName()};
Process process = null;
try {
process = new ProcessBuilder()
.redirectErrorStream(true)
.command(executionCommand)
.directory(this.getTemporaryDirectory())
.start();
try (final BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
inputStream.lines().forEach(line -> consoleOutput.add(line));
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not execute code snippet", e);
consoleOutput.add("ERROR: ".concat(e.getMessage()));
} finally {
if(process != null) {
try {
process.waitFor();
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "Can not wait for process to end", e);
}
}
}
codeFile.delete();
});
snippetThread.start();
return consoleOutput;
}
/**
* Create the source code file for the given code snippet.
* @param codeSnippet The code snippet.
* @return The file created and containing the source code.
*/
protected File createSourceCodeFile(final CodeSnippet codeSnippet) throws IOException {
final File codeFile = new File(this.getTemporaryDirectory(), determineClassName(codeSnippet).concat(".groovy"));
try (final FileWriter codeFileWriter = new FileWriter(codeFile)) {
codeFileWriter.write(buildSourceCode(codeSnippet));
codeFileWriter.flush();
}
return codeFile;
}
/**
*
* Build code file content according properties. The source code can then be written properly inside a file in order
* to be compiled and then executed.
* @param codeSnippet The code snippet to build the source code for.
* @return The content of the source code file.
*/
protected String buildSourceCode(final CodeSnippet codeSnippet) throws IOException {
final StringBuilder sourceCode = new StringBuilder();
boolean someImportsPresent = false;
if(makeScript(codeSnippet)) {
sourceCode.append(getScriptImport()).append("\n");
someImportsPresent = true;
}
if(hasImports(codeSnippet)) {
sourceCode.append(getImports(codeSnippet)).append("\n");
someImportsPresent = true;
}
if(someImportsPresent) sourceCode.append("\n");
sourceCode.append(getStartClassDefinition(codeSnippet)).append("\n");
if(mustBeWrappedInMethodRunner(codeSnippet)) {
sourceCode.append("\t").append(getStartMainMethod(codeSnippet)).append("\n")
.append(codeSnippet.getCode())
.append("\n\t").append(getEndMainMethod());
} else {
sourceCode.append(codeSnippet.getCode());
}
sourceCode.append("\n").append(getEndClassDefinition(codeSnippet));
return sourceCode.toString();
}
/**
* Determine if the code snippet must make a groovy scriptr. It is determined by the presence and value of
* the {@link #MAKE_SCRIPT} property.
* @param codeSnippet The code snippet.
* @return {@code true} if the snippet must be created as a groovy script, {@code false} otherwise.
*/
protected boolean makeScript(final CodeSnippet codeSnippet) {
final Boolean makeScript = codeSnippet.getProperties().containsKey(MAKE_SCRIPT) ?
Boolean.parseBoolean(codeSnippet.getProperties().get(MAKE_SCRIPT)) :
false;
return makeScript;
}
/**
* Get the necessary import to create a groovy script.
* @return The well formatted import to create a groovy script.
*/
protected String getScriptImport() {
return formatImportLine("org.codehaus.groovy.runtime.InvokerHelper");
}
/**
* Get the imports to be added to the source code.
* @param codeSnippet The code snippet.
*/
protected boolean hasImports(final CodeSnippet codeSnippet) {
final String imports = codeSnippet.getProperties().get(IMPORTS_PROPERTY);
return imports != null && !imports.isEmpty();
}
/**
* Get the imports for the code snippets. If some lines of the imports don't contain the {@code import} keyword, it
* will be added properly.
*
* @param codeSnippet The code snippet.
* @return A well formatted string containing all imports.
*/
protected String getImports(final CodeSnippet codeSnippet) throws IOException {
final StringJoiner imports = new StringJoiner("\n");
try (final StringReader stringReader = new StringReader(codeSnippet.getProperties().get(IMPORTS_PROPERTY));
final BufferedReader reader = new DefaultCharsetReader(stringReader)) {
reader.lines()
.filter(line -> !line.trim().isEmpty())
.forEach(line -> imports.add(formatImportLine(line)));
}
return imports.toString();
}
/**
* Format an import line by make sure it starts with the {@code import} keyword.
* @param importLine The import line to format.
* @return A well formatted import line.
*/
protected String formatImportLine(final String importLine) {
final String importLineBeginning = "import ";
String formattedImportLine;
if(importLine.startsWith(importLineBeginning)) {
formattedImportLine = importLine;
} else {
formattedImportLine = importLineBeginning.concat(importLine);
}
return formattedImportLine;
}
/**
* Get the definition of the class.
* @param codeSnippet The code snippet.
*/
protected String getStartClassDefinition(final CodeSnippet codeSnippet) {
final StringBuilder startClassDefinition = new StringBuilder("class ")
.append(determineClassName(codeSnippet));
if(makeScript(codeSnippet)) {
startClassDefinition.append(" extends Script");
}
startClassDefinition.append(" {");
return startClassDefinition.toString();
}
/**
* Determine the class name of the code snippet. It looks inside the code snippet's properties and check the value
* of the {@link #CLASS_NAME_PROPERTY} property. If {@code null} or empty, {@code Snippet} will be returned.
* @param codeSnippet The code snippet.
* @return The class name of the code snippet.
*/
protected String determineClassName(final CodeSnippet codeSnippet) {
String className = codeSnippet.getProperties().get(CLASS_NAME_PROPERTY);
if(className == null || className.isEmpty()) className = "Snippet";
return className;
}
/**
* Determine if the code snippet must be wrapped inside a method runner. It is determined by the presence and value of
* the {@link #WRAP_IN_METHOD_RUNNER} property.
* @param codeSnippet The code snippet.
* @return {@code true} if the snippet must be wrapped in main, {@code false} otherwise.
*/
protected boolean mustBeWrappedInMethodRunner(final CodeSnippet codeSnippet) {
final Boolean wrapInMain = codeSnippet.getProperties().containsKey(WRAP_IN_METHOD_RUNNER) ?
Boolean.parseBoolean(codeSnippet.getProperties().get(WRAP_IN_METHOD_RUNNER)) :
false;
return wrapInMain;
}
/**
* Get the start of the declaration of the main method.
* @return The start of the main method.
*/
protected String getStartMainMethod(final CodeSnippet codeSnippet) {
if(makeScript(codeSnippet)) return "def run() {";
else return "def static main(String ... args) {";
}
/**
* Get the end of the declaration of the main method.
* @return The end of the main method.
*/
protected String getEndMainMethod() {
return "}";
}
/**
* Get the end of the definition of the class.
* @param codeSnippet The code snippet.
*/
protected String getEndClassDefinition(final CodeSnippet codeSnippet) {
return "}";
}
}