/* (c) 2015 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.importer.transform; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteWatchdog; import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.io.FileUtils; import org.apache.commons.io.output.CountingOutputStream; import org.apache.commons.lang.SystemUtils; import org.geoserver.data.util.IOUtils; import org.geoserver.importer.ImportData; import org.geoserver.importer.ImportTask; /** * Generic file translator getting a set of options, an input file, and an output file * * @author Andrea Aime - GeoSolutions */ public abstract class AbstractCommandLineTransform extends AbstractTransform implements PreTransform { private static final long serialVersionUID = 5998049960852782644L; static final long DEFAULT_TIMEOUT = 60 * 60 * 1000; // one hour List<String> options; public AbstractCommandLineTransform(List<String> options) { this.options = options; } /** * @return the options */ public List<String> getOptions() { return options; } /** * @param options the options to set */ public void setOptions(List<String> options) { this.options = options; } @Override public boolean stopOnError(Exception e) { return true; } @Override public void apply(ImportTask task, ImportData data) throws Exception { boolean inline = isInline(); File executable = getExecutable(); File inputFile = getInputFile(data); Map<String, File> substitutions = new HashMap<>(); substitutions.put("input", inputFile); File outputDirectory = null; File outputFile = null; if (!inline) { outputDirectory = getOutputDirectory(data); outputFile = new File(outputDirectory, inputFile.getName()); substitutions.put("output", outputFile); } // setup the options CommandLine cmd = new CommandLine(executable); cmd.setSubstitutionMap(substitutions); setupCommandLine(inline, cmd); // prepare to run DefaultExecutor executor = new DefaultExecutor(); // make sure we don't try to execute for too much time executor.setWatchdog(new ExecuteWatchdog(DEFAULT_TIMEOUT)); // grab at least some part of the outputs int limit = 16 * 1024; try { try (OutputStream os = new BoundedOutputStream(new ByteArrayOutputStream(), limit); OutputStream es = new BoundedOutputStream(new ByteArrayOutputStream(), limit)) { PumpStreamHandler streamHandler = new PumpStreamHandler(os, es); executor.setStreamHandler(streamHandler); try { int result = executor.execute(cmd); if (executor.isFailure(result)) { // toString call is routed to ByteArrayOutputStream, which does the right string // conversion throw new IOException("Failed to execute command " + cmd.toString() + "\nStandard output is:\n" + os.toString() + "\nStandard error is:\n" + es.toString()); } } catch (Exception e) { throw new IOException("Failure to execute command " + cmd.toString() + "\nStandard output is:\n" + os.toString() + "\nStandard error is:\n" + es.toString(), e); } } // if not inline, replace inputs with output if (!inline) { List<String> names = getReplacementTargetNames(data); File inputParent = inputFile.getParentFile(); for (String name : names) { File output = new File(outputDirectory, name); File input = new File(inputParent, name); if (output.exists()) { // uses atomic rename on *nix, delete and copy on Windows IOUtils.rename(output, input); } else if (input.exists()) { input.delete(); } } } } finally { if (outputDirectory != null) { FileUtils.deleteQuietly(outputDirectory); } } } protected boolean checkAvailable() throws IOException { try { CommandLine cmd = new CommandLine(getExecutable()); for (String option : getAvailabilityTestOptions()) { cmd.addArgument(option); } // prepare to run DefaultExecutor executor = new DefaultExecutor(); // grab at least some part of the outputs int limit = 16 * 1024; try (OutputStream os = new BoundedOutputStream(new ByteArrayOutputStream(), limit); OutputStream es = new BoundedOutputStream(new ByteArrayOutputStream(), limit)) { PumpStreamHandler streamHandler = new PumpStreamHandler(os, es); executor.setStreamHandler(streamHandler); int result = executor.execute(cmd); if (result != 0) { LOGGER.log(Level.SEVERE, "Failed to execute command " + cmd.toString() + "\nStandard output is:\n" + os.toString() + "\nStandard error is:\n" + es.toString()); return false; } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failure to execute command " + cmd.toString(), e); return false; } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failure to locate executable for class " + this.getClass(), e); return false; } return true; } /** * Returns the list of options to be passed the executable to test its availability and ability * to run. e.g. "--help" * * */ protected abstract List<String> getAvailabilityTestOptions(); protected void setupCommandLine(boolean inline, CommandLine cmd) { for (String option : options) { cmd.addArgument(option, false); } // setup input and output files if (inline) { cmd.addArgument("${input}", false); } else { if (isOutputAfterInput()) { cmd.addArgument("${input}", false); cmd.addArgument("${output}", false); } else { cmd.addArgument("${output}", false); cmd.addArgument("${input}", false); } } } /** * Returns the name of all the files that should be transferred from input to output (sometimes * the output is made of several files) * * @param data * * @throws IOException */ protected abstract List<String> getReplacementTargetNames(ImportData data) throws IOException; /** * Returns true if the command line manipulates the input file directly * * */ protected boolean isInline() { return false; } /** * Returns true if in the command line the output file comes after the input one. The default * implementation returns true * * */ protected boolean isOutputAfterInput() { return true; } /** * The command input file * * @param data * * @throws IOException */ protected abstract File getInputFile(ImportData data) throws IOException; /** * The directory used for outputs, by default, a subdirectory of the input file parent * * @param data * * @throws IOException */ protected File getOutputDirectory(ImportData data) throws IOException { File input = getInputFile(data); File parent = input.getParentFile(); File tempFile = File.createTempFile("tmp", null, parent); tempFile.delete(); if (!tempFile.mkdir()) { throw new IOException("Could not create work directory " + tempFile.getAbsolutePath()); } return tempFile; } /** * Implementors must provide the executable to be run * * */ protected abstract File getExecutable() throws IOException; /** * Locates and executable in the system path. On windows it will automatically append .exe to * the searched file name * * @param name * * @throws IOException */ protected File getExecutableFromPath(String name) throws IOException { if (SystemUtils.IS_OS_WINDOWS) { name = name + ".exe"; } String systemPath = System.getenv("PATH"); if (systemPath == null) { systemPath = System.getenv("path"); } if (systemPath == null) { throw new IOException("Path is not set, cannot locate " + name); } String[] paths = systemPath.split(File.pathSeparator); for (String pathDir : paths) { File file = new File(pathDir, name); if (file.exists() && file.isFile() && file.canExecute()) { return file; } } throw new IOException( "Could not locate executable (or could locate, but does not have execution rights): " + name); } /** * Output stream wrapper with a soft limit * * @author Andrea Aime - GeoSolutions */ static final class BoundedOutputStream extends CountingOutputStream { private long maxSize; private OutputStream delegate; public BoundedOutputStream(OutputStream delegate, long maxSize) { super(delegate); this.delegate = delegate; this.maxSize = maxSize; } @Override public void write(byte[] bts) throws IOException { if (getByteCount() > maxSize) { return; } super.write(bts); } @Override public void write(byte[] bts, int st, int end) throws IOException { if (getByteCount() > maxSize) { return; } super.write(bts, st, end); } @Override public void write(int idx) throws IOException { if (getByteCount() > maxSize) { return; } super.write(idx); } @Override public String toString() { return delegate.toString(); } } }