diff --git a/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java b/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java
index 4c9775b14..289072cad 100644
--- a/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java
+++ b/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java
@@ -21,6 +21,8 @@ import java.nio.file.Paths;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.ide.vscode.boot.properties.util.FuzzyMap;
+import org.springframework.ide.vscode.util.ExternalCommand;
+import org.springframework.ide.vscode.util.ExternalProcess;
/**
* Sanity test the boot properties index
@@ -39,8 +41,8 @@ public class PropertiesIndexTest {
Path testProjectPath = Paths.get(PropertiesIndexTest.class.getResource("/demo-1").toURI());
if (!Files.exists(testProjectPath.resolve("classpath.txt"))) {
testProjectPath.resolve("mvnw").toFile().setExecutable(true);
- int result = Runtime.getRuntime().exec("./mvnw clean package", null, testProjectPath.toFile()).waitFor();
- if (result != 0) {
+ ExternalProcess process = new ExternalProcess(testProjectPath.toFile(), new ExternalCommand("./mvnw", "clean", "package"), true);
+ if (process.getExitValue() != 0) {
throw new RuntimeException("Failed to build test project");
}
}
diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalCommand.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalCommand.java
new file mode 100644
index 000000000..b693ef566
--- /dev/null
+++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalCommand.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2016 Pivotal Software, Inc.
+ * 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:
+ * Pivotal Software, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Encapsulates information about an 'external' command that can be run through the OS.
+ *
+ * This is a simplistic implementation. A more sophisticate implementation should allow for
+ * different OS's (commands may return different information depending on the OS).
+ *
+ * @author Kris De Volder
+ */
+public class ExternalCommand {
+
+ private final String[] command;
+
+ public ExternalCommand(String... command) {
+ ArrayList pieces = new ArrayList(command.length);
+ for (String piece : command) {
+ if (piece!=null) {
+ pieces.add(piece);
+ }
+ }
+ this.command = pieces.toArray(new String[pieces.size()]);
+ }
+
+ public String[] getProgramAndArgs() {
+ return command;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ boolean first = true;
+ for (String piece : command) {
+ if (!first) {
+ buf.append(" ");
+ }
+ buf.append(piece);
+ first = false;
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Just before executing the command with a ProcessBuilder instance, this method is called,
+ * giving the command a chance to apply some extra configuration (e.g. set some environment
+ * parameters).
+ */
+ public void configure(ProcessBuilder processBuilder) {
+ //Default implementation does nothing. Subclasses may override.
+ }
+
+ /**
+ * A convenient way to execute commands suitable for use in tests. The output and
+ * result of commands are logged to the console and if the command returns non
+ * 0 exit value an exception is thrown.
+ */
+ public void exec(File workdir) throws IOException, InterruptedException {
+ System.out.println(">>> exec: "+this);
+ ExternalProcess process = new ExternalProcess(workdir, this);
+ System.out.println(process);
+// org.junit.Assert.assertEquals(0, process.getExitValue());
+ }
+
+}
diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalProcess.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalProcess.java
new file mode 100644
index 000000000..68c0b6030
--- /dev/null
+++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalProcess.java
@@ -0,0 +1,180 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2016 Pivotal Software, Inc.
+ * 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:
+ * Pivotal Software, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+
+/**
+ * Convenient wrapper around a {@link Process}. Simplifies the synchronous execution of external
+ * commands by handling reading from out and error streams and either buffering the result output
+ * for later retrieval, or forwarding the to output to designated streams.
+ *
+ * @author Kris De Volder
+ */
+public class ExternalProcess {
+
+ /**
+ * A thread that keeps reading input from a Stream until the end is reached
+ * or there's some error reading the Stream.
+ */
+ public static class StreamGobler extends Thread {
+
+ private final OutputStream echo;
+ private InputStream toRead; //Stream to read. This is nulled after all input has been consumed.
+
+ /**
+ * Creates a StreamGobler that reads input from an input stream
+ * and buffers up all input it has read for later retrieval via
+ * the getOut() method.
+ */
+ public StreamGobler(InputStream toRead) {
+ this(toRead, new ByteArrayOutputStream());
+ }
+
+ /**
+ * Creates a StreamGobler that reads input from an input stream
+ * and writes it out to an outputstream.
+ */
+ public StreamGobler(InputStream toRead, OutputStream forwardTo) {
+ this.toRead = toRead;
+ this.echo = forwardTo;
+ start();
+ }
+
+ @Override
+ public void run() {
+ byte[] buf = new byte[256];
+ while (toRead!=null) {
+ try {
+ int i = toRead.read(buf);
+ if (i==-1) {
+ //EOF
+ toRead = null; //Done!
+ } else {
+ append(buf, i);
+ }
+ } catch (IOException e) {
+ toRead = null;
+ ByteArrayOutputStream errMsg = new ByteArrayOutputStream();
+ e.printStackTrace(new PrintStream(errMsg));
+ append(errMsg.toByteArray());
+ }
+ }
+ }
+
+ private void append(byte[] buf) {
+ append(buf, buf.length);
+ }
+
+ private void append(byte[] buf, int len) {
+ if (echo!=null) {
+ try {
+ echo.write(buf, 0, len);
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ public String getContents() throws InterruptedException {
+ try {
+ this.join();
+ if (echo instanceof ByteArrayOutputStream) {
+ return ((ByteArrayOutputStream)echo).toString();
+ } else {
+ return null;
+ }
+ } finally {
+ toRead = null;
+ }
+ }
+ }
+
+ private Process process;
+ private StreamGobler err; // Standard error is to be read from here
+ private StreamGobler out; // Standard out is to be read from here
+ private int exitValue = -9999;
+ private ExternalCommand cmd;
+
+ /**
+ * Creates an external process and waits for it to terminate. The output and error streams
+ * will be read and forwarded to System.out and System.err
+ */
+ public ExternalProcess(File workingDir, ExternalCommand cmd) throws IOException, InterruptedException {
+ this(workingDir, cmd, false);
+ }
+
+ private void init(File workingDir, ExternalCommand cmd,
+ OutputStream outStream, OutputStream errStream) throws IOException,
+ InterruptedException {
+ this.cmd = cmd;
+ ProcessBuilder processBuilder = new ProcessBuilder(cmd.getProgramAndArgs());
+ processBuilder.directory(workingDir);
+ cmd.configure(processBuilder);
+ process = processBuilder.start();
+ err = new StreamGobler(process.getErrorStream(), errStream);
+ out = new StreamGobler(process.getInputStream(), outStream);
+ exitValue = process.waitFor();
+ }
+
+ public ExternalProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException, InterruptedException {
+ if (captureStreams) {
+ init(workingDir, cmd, new ByteArrayOutputStream(), new ByteArrayOutputStream());
+ } else {
+ init(workingDir, cmd, System.out, System.err);
+ }
+ }
+
+ public String getOut() throws InterruptedException {
+ return out.getContents();
+ }
+
+ public String getErr() throws InterruptedException {
+ return err.getContents();
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer result = new StringBuffer();
+ try {
+ process.exitValue();
+ result.append(">>>> ExternalProcess: ");
+ result.append(cmd+"\n");
+ result.append("exitValue = "+exitValue+"\n");
+ String strOut = getOut();
+ if (strOut!=null) {
+ result.append("\n------- System.out -------\n");
+ result.append(strOut);
+ }
+ String strErr = getErr();
+ if (strErr!=null) {
+ result.append("\n------- System.err -------\n");
+ result.append(getOut());
+ }
+ result.append("<<<< ExternalProcess");
+ return result.toString();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return result.toString();
+ } catch (IllegalThreadStateException e) {
+ return "ExternalProcess(RUNNING, "+cmd+")";
+ }
+ }
+
+ public int getExitValue() {
+ return exitValue;
+ }
+}