/* * Copyright (C) 2015 Jan Pokorsky * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package cz.cas.lib.proarc.common.process; import java.io.File; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.configuration.Configuration; import org.apache.commons.io.FilenameUtils; /** * The experimental implementation of an external process that can be customized * in more details including input and output parameters. * * @author Jan Pokorsky */ public class GenericExternalProcess extends ExternalProcess { public static final String DST_PARENT = "output.folder"; public static final String DST_PATH = "output.file"; public static final String SRC_EXT = "input.file.ext"; public static final String SRC_NAME = "input.file.name"; public static final String SRC_NAME_EXT = "input.file.nameExt"; public static final String SRC_PATH = "input.file"; public static final String SRC_PARENT = "input.folder"; private File inputFile; private File outputFile; private final ParamHandler parameters; private final Configuration conf; private static final Pattern REPLACE_PARAM_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); private boolean skippedProcess; private ProcessResult processResult; public GenericExternalProcess(Configuration conf) { super(conf); this.conf = conf; this.parameters = new ParamHandler(); } public GenericExternalProcess addInputFile(File file) { this.inputFile = file; parameters.addInputFile(file); return this; } public File getInputFile() { return inputFile; } public GenericExternalProcess addOutputFile(File file) { this.outputFile = file; parameters.addOutput(file); return this; } public File getOutputFile() { return processResult == null ? outputFile : getResult().getOutputFile(); } public ParamHandler getParameters() { return parameters; } public Map<String, String> getResultParameters() { return getResult().getParameters(); } public ProcessResult getResult() { if (processResult == null) { throw new IllegalStateException("The process has not run yet!"); } return processResult; } @Override public void run() { Configuration run = conf.subset("run"); String[] ifNotExists = run.getStringArray("if.notExists"); for (String ifNotExist : ifNotExists) { String path = interpolateParameters(ifNotExist, parameters.getMap()); File file = new File(path); if (file.exists()) { skippedProcess = true; break; } } if (!skippedProcess) { super.run(); } processResult = ProcessResult.getResultParameters(conf, parameters.getMap(), isSkippedProcess(), getExitCode(), getFullOutput()); } public boolean isSkippedProcess() { return skippedProcess; } @Override protected List<String> buildCmdLine(Configuration conf) { List<String> cmdLine = super.buildCmdLine(conf); interpolateParameters(cmdLine); return cmdLine; } /** * Interpolate parameter values. The replace pattern is {@code ${name}}. * @param s a string to search for placeholders * @return the resolved string * @see #addParameter(java.lang.String, java.lang.String) */ static String interpolateParameters(String s, Map<String, String> parameters) { if (s == null || s.length() < 4 || parameters.isEmpty()) { // minimal replaceable value ${x} return s; } // finds ${name} patterns Matcher m = REPLACE_PARAM_PATTERN.matcher(s); StringBuffer sb = null; while(m.find()) { if (m.groupCount() == 1) { String param = m.group(1); String replacement = parameters.get(param); if (replacement != null) { sb = sb != null ? sb : new StringBuffer(); m.appendReplacement(sb, Matcher.quoteReplacement(replacement)); } } } if (sb == null) { return s; } m.appendTail(sb); return sb.toString(); } void interpolateParameters(List<String> cmdLine) { if (parameters.isEmpty()) { return ; } for (ListIterator<String> it = cmdLine.listIterator(); it.hasNext();) { it.set(interpolateParameters(it.next(), parameters.getMap())); } } public static class ParamHandler { private final Map<String, String> parameters = new HashMap<String, String>(); public ParamHandler add(Map<String, String> parameters) { this.parameters.putAll(parameters); return this; } public ParamHandler add(String name, String value) { parameters.put(name, value); return this; } public ParamHandler addInputFile(File file) { String nameExt = file.getName(); return add(SRC_PATH, file.getAbsolutePath()) .add(SRC_EXT, FilenameUtils.getExtension(nameExt)) .add(SRC_NAME, FilenameUtils.getBaseName(nameExt)) .add(SRC_NAME_EXT, nameExt) .add(SRC_PARENT, file.getParentFile().getPath()); } public ParamHandler addOutput(File file) { return add(DST_PATH, file.getAbsolutePath()) .add(DST_PARENT, file.getParentFile().getPath()); } public Map<String, String> getMap() { return parameters; } public boolean isEmpty() { return parameters.isEmpty(); } } public static class ProcessResult { private final Map<String, String> parameters; private final ExitStrategy exit; private final String processorId; public ProcessResult(String processorId, Map<String, String> params, ExitStrategy exit) { this.parameters = params; this.exit = exit; this.processorId = processorId; } public Map<String, String> getParameters() { return parameters; } public File getOutputFile() { String path = parameters.get(processorId + ".param." + DST_PATH); if (path == null) { path = parameters.get(DST_PATH); } return path == null || path.isEmpty() ? null : new File(path); } public ExitStrategy getExit() { return exit; } /** * Merges result parameters. {@code onExit} and {@code onSkip} are experimental features. * * @param conf * @param inputParameters * @param skippedProcess * @param exitCode * @param log * @return */ public static ProcessResult getResultParameters(Configuration conf, Map<String, String> inputParameters, boolean skippedProcess, int exitCode, String log) { // gets <processorId>.param.* parameters with interpolated values // it allows to share properties among process and read helper values from process declaration (mime, output file, ...) String processorId = conf.getString("id"); Map<String, String> hm = new HashMap<String, String>(inputParameters); addResultParamaters(conf, processorId, hm); ExitStrategy exit = new ExitStrategy(); if (skippedProcess) { Configuration onSkip = conf.subset("onSkip"); addResultParamaters(onSkip, processorId, hm); exit.setSkip(true); exit.setContinueWithProcessIds(Arrays.asList(onSkip.getStringArray("next"))); return new ProcessResult(processorId, hm, exit); } exit.setExitCode(exitCode); String[] onExitIds = conf.getStringArray("onExits"); Configuration onExitConf = conf.subset("onExit"); boolean defaultExit = true; for (String onExitId : onExitIds) { if (isExitId(onExitId, exitCode)) { Configuration onExitIdConf = onExitConf.subset(onExitId); addResultParamaters(onExitIdConf, processorId, hm); exit.setErrorMessage(onExitIdConf.getString("message")); exit.setStop(onExitIdConf.getBoolean("stop", exitCode != 0)); exit.setContinueWithProcessIds(Arrays.asList(onExitIdConf.getStringArray("next"))); defaultExit = false; break; } } if (defaultExit) { exit.setStop(exitCode != 0); exit.setErrorMessage(log); } return new ProcessResult(processorId, hm, exit); } private static Map<String, String> addResultParamaters(Configuration conf, String processorId, Map<String, String> result) { for (Iterator<String> it = conf.getKeys("param"); it.hasNext();) { String param = it.next(); String value = interpolateParameters(conf.getString(param), result); String resultName = processorId == null ? param : processorId + '.' + param; result.put(resultName, value); } return result; } /** * Resolves whether the {@code exitExpression} matches to {@code exitCode}. * @param exitExpression syntax: * {@code '*' | '>' exitCode | '<' exitCode | exitCode [',' exitCode]*} * @param exitCode * @return */ private static boolean isExitId(String exitExpression, int exitCode) { boolean match = false; if ("*".equals(exitExpression)) { match = true; } else if (exitExpression.charAt(0) == '>') { try { int code = Integer.parseInt(exitExpression.substring(1)); if (exitCode > code) { match = true; } } catch (NumberFormatException ex) { } } else if (exitExpression.charAt(0) == '<') { try { int code = Integer.parseInt(exitExpression.substring(1)); if (exitCode < code) { match = true; } } catch (NumberFormatException ex) { } } else { String[] codes = exitExpression.split(","); for (String codeStr : codes) { try { int code = Integer.parseInt(codeStr.trim()); if (exitCode == code) { match = true; } } catch (NumberFormatException ex) { } } } return match; } } public static class ExitStrategy { private Integer exitCode; private boolean stop; private boolean skip; private List<String> continueWithProcessIds; private String errorMessage; public Integer getExitCode() { return exitCode; } public void setExitCode(Integer exitCode) { this.exitCode = exitCode; } public boolean isStop() { return stop; } public void setStop(boolean stop) { this.stop = stop; } public boolean isSkip() { return skip; } public void setSkip(boolean skip) { this.skip = skip; } public List<String> getContinueWithProcessIds() { return continueWithProcessIds; } public void setContinueWithProcessIds(List<String> continueWithProcessIds) { this.continueWithProcessIds = continueWithProcessIds; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } } }