001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.utils;
014
015import static org.apache.juneau.internal.IOUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.io.*;
019import java.lang.reflect.*;
020import java.util.*;
021import java.util.logging.*;
022
023import org.apache.juneau.internal.*;
024import org.apache.juneau.utils.IOPipe.*;
025
026/**
027 * Utility class for running operating system processes.
028 *
029 * <p>
030 * Similar to {@link java.lang.ProcessBuilder} but with additional features.
031 */
032public class ProcBuilder {
033
034   private java.lang.ProcessBuilder pb = new java.lang.ProcessBuilder();
035   private TeeWriter outWriters = new TeeWriter(), logWriters = new TeeWriter();
036   private LineProcessor lp;
037   private Process p;
038   private int maxExitStatus = 0;
039   private boolean byLines;
040   private String divider = "--------------------------------------------------------------------------------";
041
042   /**
043    * Creates a process builder with the specified arguments.
044    *
045    * <p>
046    * Equivalent to calling <code>ProcessBuilder.create().command(args);</code>
047    *
048    * @param args The command-line arguments.
049    * @return A new process builder.
050    */
051   public static ProcBuilder create(Object...args) {
052      return new ProcBuilder().command(args);
053   }
054
055   /**
056    * Creates an empty process builder.
057    *
058    * @return A new process builder.
059    */
060   public static ProcBuilder create() {
061      return new ProcBuilder().command();
062   }
063
064   /**
065    * Command arguments.
066    *
067    * <p>
068    * Arguments can be collections or arrays and will be automatically expanded.
069    *
070    * @param args The command-line arguments.
071    * @return This object (for method chaining).
072    */
073   public ProcBuilder command(Object...args) {
074      return commandIf(ANY, args);
075   }
076
077   /**
078    * Command arguments if the specified matcher matches.
079    *
080    * <p>
081    * Can be used for specifying OS-specific commands.
082    *
083    * <h5 class='section'>Example:</h5>
084    * <p class='bcode w800'>
085    *    ProcBuilder pb = ProcBuilder
086    *       .create()
087    *       .commandIf(<jsf>WINDOWS</jsf>, <js>"cmd /c dir"</js>)
088    *       .commandIf(<jsf>UNIX</jsf>, <js>"bash -c ls"</js>)
089    *       .merge()
090    *       .execute();
091    * </p>
092    *
093    * @param m The matcher.
094    * @param args The command line arguments if matcher matches.
095    * @return This object (for method chaining).
096    */
097   public ProcBuilder commandIf(Matcher m, Object...args) {
098      if (m.matches())
099         pb.command(toList(args));
100      return this;
101   }
102
103   /**
104    * Append to the command arguments.
105    *
106    * <p>
107    * Arguments can be collections or arrays and will be automatically expanded.
108    *
109    * @param args The command-line arguments.
110    * @return This object (for method chaining).
111    */
112   public ProcBuilder append(Object...args) {
113      return appendIf(ANY, args);
114   }
115
116   /**
117    * Append to the command arguments if the specified matcher matches.
118    *
119    * <p>
120    * Arguments can be collections or arrays and will be automatically expanded.
121    *
122    * @param m The matcher.
123    * @param args The command line arguments if matcher matches.
124    * @return This object (for method chaining).
125    */
126   public ProcBuilder appendIf(Matcher m, Object...args) {
127      if (m.matches())
128         pb.command().addAll(toList(args));
129      return this;
130   }
131
132   /**
133    * Merge STDOUT and STDERR into a single stream.
134    *
135    * @return This object (for method chaining).
136    */
137   public ProcBuilder merge() {
138      pb.redirectErrorStream(true);
139      return this;
140   }
141
142   /**
143    * Use by-lines mode.
144    *
145    * <p>
146    * Flushes output after every line of input.
147    *
148    * @return This object (for method chaining).
149    */
150   public ProcBuilder byLines() {
151      this.byLines = true;
152      return this;
153   }
154
155   /**
156    * Pipe output to the specified writer.
157    *
158    * <p>
159    * The method can be called multiple times to write to multiple writers.
160    *
161    * @param w The writer to pipe to.
162    * @param close Close the writer afterwards.
163    * @return This object (for method chaining).
164    */
165   public ProcBuilder pipeTo(Writer w, boolean close) {
166      this.outWriters.add(w, close);
167      return this;
168   }
169
170   /**
171    * Pipe output to the specified writer, but don't close the writer.
172    *
173    * @param w The writer to pipe to.
174    * @return This object (for method chaining).
175    */
176   public ProcBuilder pipeTo(Writer w) {
177      return pipeTo(w, false);
178   }
179
180   /**
181    * Pipe output to the specified writer, including the command and return code.
182    *
183    * <p>
184    * The method can be called multiple times to write to multiple writers.
185    *
186    * @param w The writer to pipe to.
187    * @param close Close the writer afterwards.
188    * @return This object (for method chaining).
189    */
190   public ProcBuilder logTo(Writer w, boolean close) {
191      this.logWriters.add(w, close);
192      this.outWriters.add(w, close);
193      return this;
194   }
195
196   /**
197    * Pipe output to the specified writer, including the command and return code.
198    *
199    * <p>
200    * The method can be called multiple times to write to multiple writers.
201    * Don't close the writer afterwards.
202    *
203    * @param w The writer to pipe to.
204    * @return This object (for method chaining).
205    */
206   public ProcBuilder logTo(Writer w) {
207      return logTo(w, false);
208   }
209
210   /**
211    * Pipe output to the specified writer, including the command and return code.
212    * The method can be called multiple times to write to multiple writers.
213    *
214    * @param level The log level.
215    * @param logger The logger to log to.
216    * @return This object (for method chaining).
217    */
218   public ProcBuilder logTo(final Level level, final Logger logger) {
219      if (logger.isLoggable(level)) {
220         logTo(new StringWriter() {
221            private boolean isClosed;  // Prevents messages from being written twice.
222            @Override /* Writer */
223            public void close() {
224               if (! isClosed)
225                  logger.log(level, this.toString());
226               isClosed = true;
227            }
228         }, true);
229      }
230      return this;
231   }
232
233   /**
234    * Line processor to use to process/convert lines of output returned by the process.
235    *
236    * @param lp The new line processor.
237    * @return This object (for method chaining).
238    */
239   public ProcBuilder lp(LineProcessor lp) {
240      this.lp = lp;
241      return this;
242   }
243
244   /**
245    * Append the specified environment variables to the process.
246    *
247    * @param env The new set of environment variables.
248    * @return This object (for method chaining).
249    */
250   @SuppressWarnings({"rawtypes"})
251   public ProcBuilder env(Map env) {
252      if (env != null)
253         for (Map.Entry e : (Set<Map.Entry>)env.entrySet())
254            environment(e.getKey().toString(), e.getValue() == null ? null : e.getValue().toString());
255      return this;
256   }
257
258   /**
259    * Append the specified environment variable.
260    *
261    * @param key The environment variable name.
262    * @param val The environment variable value.
263    * @return This object (for method chaining).
264    */
265   public ProcBuilder environment(String key, String val) {
266      pb.environment().put(key, val);
267      return this;
268   }
269
270   /**
271    * Sets the directory where the command will be executed.
272    *
273    * @param directory The directory.
274    * @return This object (for method chaining).
275    */
276   public ProcBuilder directory(File directory) {
277      pb.directory(directory);
278      return this;
279   }
280
281   /**
282    * Sets the maximum allowed return code on the process call.
283    *
284    * <p>
285    * If the return code exceeds this value, an IOException is returned on the {@link #run()} command.
286    * The default value is '0'.
287    *
288    * @param maxExitStatus The maximum exit status.
289    * @return This object (for method chaining).
290    */
291   public ProcBuilder maxExitStatus(int maxExitStatus) {
292      this.maxExitStatus = maxExitStatus;
293      return this;
294   }
295
296   /**
297    * Run this command and pipes the output to the specified writer or output stream.
298    *
299    * @return The exit code from the process.
300    * @throws IOException
301    * @throws InterruptedException
302    */
303   public int run() throws IOException, InterruptedException {
304      if (pb.command().size() == 0)
305         throw new IOException("No command specified in ProcBuilder.");
306      try {
307         logWriters.append(divider).append('\n').flush();
308         logWriters.append(join(pb.command(), " ")).append('\n').flush();
309         p = pb.start();
310         IOPipe.create(p.getInputStream(), outWriters).lineProcessor(lp).byLines(byLines).run();
311         int rc = p.waitFor();
312         logWriters.append("Exit: ").append(String.valueOf(p.exitValue())).append('\n').flush();
313         if (rc > maxExitStatus)
314            throw new IOException("Return code "+rc+" from command " + join(pb.command(), " "));
315         return rc;
316      } finally {
317         close();
318      }
319   }
320
321   /**
322    * Run this command and returns the output as a simple string.
323    *
324    * @return The output from the command.
325    * @throws IOException
326    * @throws InterruptedException
327    */
328   public String getOutput() throws IOException, InterruptedException {
329      StringWriter sw = new StringWriter();
330      pipeTo(sw).run();
331      return sw.toString();
332   }
333
334   /**
335    * Returns the output from this process as a {@link Scanner}.
336    *
337    * @return The output from the process as a Scanner object.
338    * @throws IOException
339    * @throws InterruptedException
340    */
341   public Scanner getScanner() throws IOException, InterruptedException {
342      StringWriter sw = new StringWriter();
343      pipeTo(sw, true);
344      run();
345      return new Scanner(sw.toString());
346   }
347
348   /**
349    * Destroys the underlying process.
350    *
351    * <p>
352    * This method is only needed if the {@link #getScanner()} method was used.
353    */
354   private void close() {
355      closeQuietly(logWriters, outWriters);
356      if (p != null)
357         p.destroy();
358   }
359
360   /**
361    * Specifies interface for defining OS-specific commands.
362    */
363   public abstract static class Matcher {
364      abstract boolean matches();
365   }
366
367   static final String OS = System.getProperty("os.name").toLowerCase();
368
369   /** Operating system matcher: Any operating system. */
370   public static final Matcher ANY = new Matcher() {
371      @Override boolean matches() {
372         return true;
373      }
374   };
375
376   /** Operating system matcher: Any Windows system. */
377   public static final Matcher WINDOWS = new Matcher() {
378      @Override boolean matches() {
379         return OS.indexOf("win") >= 0;
380      }
381   };
382
383   /** Operating system matcher: Any Mac system. */
384   public static final Matcher MAC = new Matcher() {
385      @Override boolean matches() {
386         return OS.indexOf("mac") >= 0;
387      }
388   };
389
390   /** Operating system matcher: Any Unix or Linux system. */
391   public static final Matcher UNIX = new Matcher() {
392      @Override boolean matches() {
393         return OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0;
394      }
395   };
396
397   private static List<String> toList(Object...args) {
398      List<String> l = new LinkedList<>();
399      for (Object o : args) {
400         if (o.getClass().isArray())
401            for (int i = 0; i < Array.getLength(o); i++)
402               l.add(Array.get(o, i).toString());
403         else if (o instanceof Collection)
404            for (Object o2 : (Collection<?>)o)
405               l.add(o2.toString());
406         else
407            l.add(o.toString());
408      }
409      return l;
410   }
411}