001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.util.processes;
031
032import java.io.File;
033
034import org.apache.tools.ant.BuildEvent;
035import org.apache.tools.ant.BuildException;
036import org.apache.tools.ant.BuildListener;
037import org.apache.tools.ant.Project;
038import org.apache.tools.ant.taskdefs.Java;
039import org.apache.tools.ant.types.Commandline.Argument;
040import org.apache.tools.ant.types.Path;
041import org.openimaj.util.array.ArrayUtils;
042
043/**
044 * Helper methods for launching a new JVM and running code within it. Advanced
045 * configuration can be achieved by constructing a {@link ProcessOptions} and
046 * calling {@link #runProcess(ProcessOptions)}. For most other use-cases the
047 * static methods provide a convenient shortcut.
048 * 
049 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
050 */
051public class JavaProcess {
052        /**
053         * Options describing a JVM environment.
054         * 
055         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
056         * 
057         */
058        public static class ProcessOptions {
059                File workingDir;
060                String classPath;
061                String mainClass;
062                String jvmArgs;
063                File jarFile;
064                String args;
065                boolean cloneVM;
066                boolean outputErr = true;
067                boolean outputOut = true;
068
069                /**
070                 * Construct a new {@link ProcessOptions} that inherits the classpath of
071                 * the current JVM and will runs the main method of the given class.
072                 * 
073                 * @param clz
074                 *            the main class
075                 */
076                public ProcessOptions(Class<?> clz) {
077                        this.workingDir = new File(System.getProperty("user.dir", new File(".").getAbsolutePath()));
078                        this.mainClass = clz.getName();
079                        cloneVM = true;
080                }
081
082                /**
083                 * Construct a new {@link ProcessOptions} that will run the given jar
084                 * file.
085                 * 
086                 * @param jarFile
087                 *            the jar file
088                 */
089                public ProcessOptions(File jarFile) {
090                        this.workingDir = new File(System.getProperty("user.dir", new File(".").getAbsolutePath()));
091                        this.jarFile = jarFile;
092                        cloneVM = false;
093                }
094
095                /**
096                 * Construct a new {@link ProcessOptions} with the given classpath and
097                 * main class.
098                 * 
099                 * @param classPath
100                 *            the classpath the JVM can search
101                 * @param mainClass
102                 *            the main class to run
103                 */
104                public ProcessOptions(String classPath, String mainClass) {
105                        this.workingDir = new File(System.getProperty("user.dir", new File(".").getAbsolutePath()));
106                        this.classPath = classPath;
107                        this.mainClass = mainClass;
108                        cloneVM = false;
109                }
110
111                /**
112                 * Set the working directory for the JVM. By default this is set to the
113                 * working directory of the current JVM. Passing a null value resets to
114                 * the default.
115                 * 
116                 * @param workingDir
117                 *            the working directory
118                 */
119                public void setWorkingDirectory(File workingDir) {
120                        if (workingDir == null)
121                                workingDir = new File(System.getProperty("user.dir", new File(".").getAbsolutePath()));
122
123                        this.workingDir = workingDir;
124                }
125
126                /**
127                 * Set the argument string to pass to the program
128                 * 
129                 * @param args
130                 *            the argument string
131                 */
132                public void setArgs(String args) {
133                        this.args = args;
134                }
135
136                /**
137                 * Set the arguments to pass to the program
138                 * 
139                 * @param args
140                 *            the arguments
141                 */
142                public void setArgs(String[] args) {
143                        this.args = ArrayUtils.toString(args, " ");
144                }
145
146                /**
147                 * Set the JVM arguments to use when launching the program.
148                 * 
149                 * @param jvmArgs
150                 *            the JVM arguments.
151                 */
152                public void setJvmArgs(String jvmArgs) {
153                        this.jvmArgs = jvmArgs;
154                }
155
156                /**
157                 * Set the JVM arguments to use when launching the program.
158                 * 
159                 * @param jvmArgs
160                 *            the JVM arguments.
161                 */
162                public void setJvmArgs(String[] jvmArgs) {
163                        this.jvmArgs = ArrayUtils.toString(jvmArgs, " ");
164                }
165
166                protected void setup(Java java) {
167                        if (jarFile != null) {
168                                java.setJar(jarFile);
169                        } else {
170                                if (classPath == null) {
171                                        java.setClasspath(Path.systemClasspath);
172                                } else {
173                                        java.setClasspath(new Path(java.getProject(), classPath));
174                                }
175                                java.setClassname(mainClass);
176                        }
177
178                        if (jvmArgs != null) {
179                                final Argument jvmArg = java.createJvmarg();
180                                jvmArg.setLine(jvmArgs);
181                        }
182
183                        if (args != null) {
184                                final Argument taskArgs = java.createArg();
185                                taskArgs.setLine(args);
186                        }
187
188                        java.getProject().addBuildListener(new SimpleListener(java, outputOut, outputErr));
189
190                        java.setCloneVm(cloneVM);
191                }
192
193                /**
194                 * Echo the {@link System#err} stream from the running JVM
195                 * 
196                 * @param outputErr
197                 *            true to echo; false to hide
198                 */
199                public void outputErr(boolean outputErr) {
200                        this.outputErr = outputErr;
201                }
202
203                /**
204                 * Echo the {@link System#out} stream from the running JVM
205                 * 
206                 * @param outputOut
207                 *            true to echo; false to hide
208                 */
209                public void outputOut(boolean outputOut) {
210                        this.outputOut = outputOut;
211                }
212        }
213
214        /**
215         * Run the <code>main</code> main method of the given class in a new JVM.
216         * The new JVM has the same classpath as the invoking JVM. No JVM arguments
217         * are set.
218         * 
219         * @param clz
220         *            the class to run
221         * @param args
222         *            the arguments to pass to the main method
223         * 
224         * @throws ProcessException
225         *             if an error occurs during execution or the return code is
226         *             non-zero.
227         */
228        public static void runProcess(Class<?> clz, String[] args) throws ProcessException {
229                final ProcessOptions opts = new ProcessOptions(clz);
230                opts.setArgs(args);
231
232                runProcess(opts);
233        }
234
235        /**
236         * Run the <code>main</code> main method of the given class in a new JVM.
237         * The new JVM has the same classpath as the invoking JVM. No JVM arguments
238         * are set.
239         * 
240         * @param clz
241         *            the class to run
242         * @param argString
243         *            the arguments to pass to the main method
244         * 
245         * @throws ProcessException
246         *             if an error occurs during execution or the return code is
247         *             non-zero.
248         */
249        public static void runProcess(Class<?> clz, String argString) throws ProcessException {
250                final ProcessOptions opts = new ProcessOptions(clz);
251                opts.setArgs(argString);
252
253                runProcess(opts);
254        }
255
256        /**
257         * Run the <code>main</code> main method of the given class in a new JVM.
258         * The new JVM has the same classpath as the invoking JVM.
259         * 
260         * @param clz
261         *            the class to run
262         * @param jvmArgs
263         *            arguments to pass to the JVM
264         * @param args
265         *            the arguments to pass to the main method
266         * 
267         * @throws ProcessException
268         *             if an error occurs during execution or the return code is
269         *             non-zero.
270         */
271        public static void runProcess(Class<?> clz, String[] jvmArgs, String[] args) throws ProcessException {
272                final ProcessOptions opts = new ProcessOptions(clz);
273                opts.setArgs(args);
274                opts.setJvmArgs(jvmArgs);
275
276                runProcess(opts);
277        }
278
279        /**
280         * Run the <code>main</code> main method of the given class in a new JVM.
281         * The new JVM has the same classpath as the invoking JVM.
282         * 
283         * @param clz
284         *            the class to run
285         * @param jvmArgs
286         *            arguments to pass to the JVM
287         * @param argString
288         *            the arguments to pass to the main method
289         * 
290         * @throws ProcessException
291         *             if an error occurs during execution or the return code is
292         *             non-zero.
293         */
294        public static void runProcess(Class<?> clz, String jvmArgs, String argString) throws ProcessException {
295                final ProcessOptions opts = new ProcessOptions(clz);
296                opts.setArgs(argString);
297                opts.setJvmArgs(jvmArgs);
298
299                runProcess(opts);
300        }
301
302        /**
303         * Run the process described by the given {@link ProcessOptions} in a
304         * separate JVM and wait for it to exit.
305         * 
306         * @param op
307         *            the {@link ProcessOptions}.
308         * @throws ProcessException
309         *             if an error occurs during execution or the return code is
310         *             non-zero.
311         */
312        public static void runProcess(ProcessOptions op) throws ProcessException {
313                final Project project = new Project();
314                project.setBaseDir(op.workingDir);
315                project.init();
316                project.fireBuildStarted();
317
318                BuildException caught = null;
319
320                try {
321                        final Java java = new Java();
322                        java.setNewenvironment(true);
323                        java.setTaskName("runjava");
324                        java.setProject(project);
325                        java.setFork(true);
326                        java.setFailonerror(true);
327
328                        op.setup(java);
329
330                        java.init();
331                        java.execute();
332                } catch (final BuildException e) {
333                        caught = e;
334                        throw new ProcessException(caught);
335                } finally {
336                        project.log("finished");
337                        project.fireBuildFinished(caught);
338                }
339        }
340
341        private static class SimpleListener implements BuildListener {
342                Java proc;
343                boolean outputOut;
344                boolean outputErr;
345
346                SimpleListener(Java proc, boolean outputOut, boolean outputErr) {
347                        this.proc = proc;
348                        this.outputOut = outputOut;
349                        this.outputErr = outputErr;
350                }
351
352                @Override
353                public void taskStarted(BuildEvent event) {
354                }
355
356                @Override
357                public void taskFinished(BuildEvent event) {
358                }
359
360                @Override
361                public void targetStarted(BuildEvent event) {
362                }
363
364                @Override
365                public void targetFinished(BuildEvent event) {
366                }
367
368                @Override
369                public void messageLogged(BuildEvent event) {
370                        if (proc != event.getSource())
371                                return;
372
373                        if (outputOut && event.getPriority() == Project.MSG_INFO)
374                                System.out.println(event.getMessage());
375                        else if (outputErr && event.getPriority() == Project.MSG_ERR)
376                                System.err.println(event.getMessage());
377                }
378
379                @Override
380                public void buildStarted(BuildEvent event) {
381                }
382
383                @Override
384                public void buildFinished(BuildEvent event) {
385                }
386        }
387}