View Javadoc

1   /*
2    * Copyright 2016-2016 Hippo B.V. (http://www.onehippo.com)
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.onehippo.forge.gallerymagick.core.command;
17  
18  import java.io.ByteArrayOutputStream;
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.io.UnsupportedEncodingException;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.List;
26  
27  import org.apache.commons.exec.CommandLine;
28  import org.apache.commons.exec.DefaultExecuteResultHandler;
29  import org.apache.commons.exec.DefaultExecutor;
30  import org.apache.commons.exec.ExecuteException;
31  import org.apache.commons.exec.ExecuteStreamHandler;
32  import org.apache.commons.exec.ExecuteWatchdog;
33  import org.apache.commons.exec.PumpStreamHandler;
34  import org.apache.commons.io.IOUtils;
35  import org.apache.commons.lang.StringUtils;
36  import org.apache.commons.lang.math.NumberUtils;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * Abstract *Magick Command.
42   */
43  abstract public class AbstractMagickCommand {
44  
45      private static final Logger log = LoggerFactory.getLogger(AbstractMagickCommand.class);
46  
47      /**
48       * Default Graphics Magick command executable.
49       */
50      public static final String DEFAULT_SUBCOMMAND_CONVERT = "convert";
51  
52      /**
53       * System property name for Graphics Magick command executable.
54       */
55      public static final String PROP_TIMEOUT = "org.onehippo.forge.gallerymagick.core.command.timeout";
56  
57      /**
58       * The default command execution timeout.
59       */
60      private static final long DEFAULT_COMMAND_TIMEOUT = 3000L;
61  
62      /**
63       * Working directory of a command execution.
64       */
65      private File workingDirectory;
66  
67      /**
68       * Command executable. e.g, <code>gm</code>, <code>/usr/bin/gm</code> or <code>/usr/local/bin/gm</code>.
69       */
70      private final String executable;
71  
72      /**
73       * Sub-command of <code>gm</code> command. e.g, <code>convert</code>.
74       */
75      private final String subCommand;
76  
77      /**
78       * Command line arguments after the sub-command.
79       */
80      private List<String> arguments;
81  
82      /**
83       * Constructor with a sub-command.
84       * @param executable executable of Magick command
85       * @param subCommand sub-command
86       */
87      public AbstractMagickCommand(final String executable, final String subCommand) {
88          this.executable = executable;
89          this.subCommand = subCommand;
90      }
91  
92      /**
93       * Returns working directory.
94       * @return working directory
95       */
96      public File getWorkingDirectory() {
97          return workingDirectory;
98      }
99  
100     /**
101      * Sets working directory
102      * @param workingDirectory working directory
103      */
104     public void setWorkingDirectory(File workingDirectory) {
105         this.workingDirectory = workingDirectory;
106     }
107 
108     /**
109      * Returns the executable of Magick command.
110      * @return the executable of Magick command
111      */
112     public String getExecutable() {
113         return executable;
114     }
115 
116     /**
117      * Returns the sub-command of Graphics Magick command.
118      * @return the sub-command of Graphics Magick command
119      */
120     public String getSubCommand() {
121         return subCommand;
122     }
123 
124     /**
125      * Returns the command line arguments after the sub-command.
126      * @return the command line arguments after the sub-command
127      */
128     public List<String> getArguments() {
129         if (arguments == null) {
130             return Collections.emptyList();
131         }
132 
133         return Collections.unmodifiableList(arguments);
134     }
135 
136     /**
137      * Add a command line argument to Magick command.
138      * @param argument a command line argument to Magick command
139      */
140     public void addArgument(final String argument) {
141         if (StringUtils.isBlank(argument)) {
142             throw new IllegalArgumentException("Blank argument.");
143         }
144 
145         if (arguments == null) {
146             arguments = new ArrayList<>();
147         }
148 
149         arguments.add(argument);
150     }
151 
152     /**
153      * Remove all the Magick command line arguments.
154      */
155     public void clearArguments() {
156         if (arguments != null) {
157             arguments.clear();
158         }
159     }
160 
161     /**
162      * Execute the Magick command with the sub-command and arguments.
163      * @throws MagickExecuteException if an execution exception occurs
164      * @throws IOException if IO exception occurs
165      */
166     public void execute() throws IOException {
167         execute(null);
168     }
169 
170     /**
171      * Execute the Magick command with the sub-command and arguments.
172      * @param stdOut standard output stream
173      * @throws MagickExecuteException if an execution exception occurs
174      * @throws IOException if IO exception occurs
175      */
176     public void execute(final OutputStream stdOut) throws IOException {
177         CommandLine cmdLine = createCommandLine();
178         ByteArrayOutputStream errStream = null;
179         int exitValue = 0;
180         DefaultExecuteResultHandler resultHandler = null;
181 
182         try {
183             errStream = new ByteArrayOutputStream(512);
184 
185             final DefaultExecutor executor = new DefaultExecutor();
186             ExecuteStreamHandler streamHandler;
187 
188             if (stdOut != null) {
189                 streamHandler = new PumpStreamHandler(stdOut, errStream);
190             } else {
191                 streamHandler = new PumpStreamHandler(System.out, errStream);
192             }
193 
194             executor.setStreamHandler(streamHandler);
195 
196             if (getWorkingDirectory() != null) {
197                 executor.setWorkingDirectory(getWorkingDirectory());
198             }
199 
200             long timeout = NumberUtils.toLong(System.getProperty(PROP_TIMEOUT), DEFAULT_COMMAND_TIMEOUT);
201 
202             if (timeout > 0) {
203                 ExecuteWatchdog watchdog = new ExecuteWatchdog(DEFAULT_COMMAND_TIMEOUT);
204                 executor.setWatchdog(watchdog);
205                 resultHandler = new DefaultExecuteResultHandler();
206                 executor.execute(cmdLine, resultHandler);
207                 log.debug("Executed with watchdog: {}", cmdLine);
208                 resultHandler.waitFor();
209             } else {
210                 exitValue = executor.execute(cmdLine);
211                 log.debug("Executed without watchdog: {}", cmdLine);
212             }
213         } catch (ExecuteException | InterruptedException e) {
214             if (resultHandler != null) {
215                 exitValue = resultHandler.getExitValue();
216             }
217             if (e.getCause() == null) {
218                 throw new MagickExecuteException(getExecutionErrorMessage(cmdLine, errStream, e), exitValue);
219             } else {
220                 throw new MagickExecuteException(getExecutionErrorMessage(cmdLine, errStream, e), exitValue, e.getCause());
221             }
222         } finally {
223             IOUtils.closeQuietly(errStream);
224         }
225     }
226 
227     private String getExecutionErrorMessage(final CommandLine cmdLine, final ByteArrayOutputStream errStream,
228             final Exception e) throws UnsupportedEncodingException {
229         StringBuilder sbMsg = new StringBuilder(256);
230         sbMsg.append(StringUtils.trim(errStream.toString("UTF-8")));
231         sbMsg.append(' ').append(cmdLine.toString());
232         sbMsg.append(". ").append(e.getMessage());
233         return sbMsg.toString();
234     }
235 
236     /**
237      * Create a {@link CommandLine} from executable and arguments.
238      * @return a {@link CommandLine} from executable and arguments
239      */
240     abstract protected CommandLine createCommandLine();
241 
242 }