/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.integration; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.Semaphore; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptException; import org.apache.commons.exec.OS; import org.apache.commons.io.FileDeleteStrategy; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.core.runtime.Platform; import de.rcenvironment.core.component.api.ComponentConstants; import de.rcenvironment.core.component.api.ComponentException; import de.rcenvironment.core.component.api.ComponentUtils; import de.rcenvironment.core.component.datamanagement.api.ComponentDataManagementService; import de.rcenvironment.core.component.execution.api.ComponentContext; import de.rcenvironment.core.component.execution.api.ComponentEventAnnouncement; import de.rcenvironment.core.component.execution.api.ComponentEventAnnouncement.WorkflowEventType; import de.rcenvironment.core.component.execution.api.ComponentEventAnnouncementDispatcher; import de.rcenvironment.core.component.execution.api.ComponentLog; import de.rcenvironment.core.component.execution.api.ConsoleRow; import de.rcenvironment.core.component.execution.api.ConsoleRowUtils; import de.rcenvironment.core.component.execution.api.ThreadHandler; import de.rcenvironment.core.component.model.spi.DefaultComponent; import de.rcenvironment.core.component.scripting.WorkflowConsoleForwardingWriter; import de.rcenvironment.core.datamodel.api.DataType; import de.rcenvironment.core.datamodel.api.TypedDatum; import de.rcenvironment.core.datamodel.api.TypedDatumFactory; import de.rcenvironment.core.datamodel.api.TypedDatumService; import de.rcenvironment.core.datamodel.types.api.DirectoryReferenceTD; import de.rcenvironment.core.datamodel.types.api.FileReferenceTD; import de.rcenvironment.core.datamodel.types.api.FloatTD; import de.rcenvironment.core.datamodel.types.api.MatrixTD; import de.rcenvironment.core.datamodel.types.api.VectorTD; import de.rcenvironment.core.scripting.ScriptDataTypeHelper; import de.rcenvironment.core.scripting.ScriptingService; import de.rcenvironment.core.scripting.ScriptingUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.TempFileServiceAccess; import de.rcenvironment.core.utils.common.security.StringSubstitutionSecurityUtils; import de.rcenvironment.core.utils.common.security.StringSubstitutionSecurityUtils.SubstitutionContext; import de.rcenvironment.core.utils.common.textstream.TextStreamWatcher; import de.rcenvironment.core.utils.executor.LocalApacheCommandLineExecutor; import de.rcenvironment.core.utils.scripting.ScriptLanguage; import de.rcenvironment.toolkit.utils.text.TextLinesReceiver; /** * Main class for the generic tool integration. * * @author Sascha Zur * @author Jascha Riedel (#14029) * @author Doreen Seider (tool run imitation, verification token handling) */ public class CommonToolIntegratorComponent extends DefaultComponent { private static final Object PLATFORM_ACCESS_LOCK = new Object(); private static final Object VERIFICATION_TOKEN_WRITE_LOCK = new Object(); private static final String CURRENT_DIR = "."; private static final String KEEP_ON_FAILURE_ERROR_MSG = "\"Keep working directory(ies) in case of failure\" was active but is not supported by the tool, so it was deactivated."; private static final String DELETION_BEHAVIOR_ERROR_WARNING_MSG = "Chosen working directory deletion behavior not supported for tool %s. Valid one is automatically chosen: %s"; // adjust if necessary/not reasonable for productive environments private static final int MAX_TOOLS_COPIED_IN_PARALLEL = 1; /** Lock to restrict how many tools can be copied at the same time. */ private static final Semaphore COPY_TOOL_SEMAPHORE = new Semaphore(MAX_TOOLS_COPIED_IN_PARALLEL, true); private static final Log LOG = LogFactory.getLog(CommonToolIntegratorComponent.class); private static final String SLASH = "/"; private static final String ESCAPESLASH = "\\\\"; private static final String PROPERTY_PLACEHOLDER = "${prop:%s}"; private static final String ADD_PROPERTY_PLACEHOLDER = "${addProp:%s}"; private static final String OUTPUT_PLACEHOLDER = "${out:%s}"; private static final String INPUT_PLACEHOLDER = "${in:%s}"; private static final String SCRIPT_LANGUAGE = "Jython"; private static final String DIRECTORY_PLACEHOLDER_TEMPLATE = "${dir:%s}"; private static final String QUOTE = "\""; private static final String SUBSTITUTION_ERROR_MESSAGE_PREFIX = " can not be substituted in the script, because it contains" + " at least one unsecure character. See log message above to see which characters are affected"; protected ComponentContext componentContext; protected ComponentLog componentLog; protected ComponentDataManagementService datamanagementService; protected ComponentEventAnnouncementDispatcher compEventAnnouncementDispatcher; protected File executionToolDirectory; protected File inputDirectory; protected File outputDirectory; protected IntegrationHistoryDataItem historyDataItem; protected Map<String, TypedDatum> lastRunStaticInputValues = null; protected Map<String, TypedDatum> lastRunStaticOutputValues = null; protected boolean needsToRun = true; protected String copyToolBehaviour; private ScriptingService scriptingService; private TypedDatumFactory typedDatumFactory; private File baseWorkingDirectory; private File iterationDirectory; private File configDirectory; private File sourceToolDirectory; private String rootWDPath; private int runCount; private File currentWorkingDirectory; private String deleteToolBehaviour; private boolean useIterationDirectories; private boolean dontCrashOnNonZeroExitCodes; private String jythonPath; private String workingPath; private boolean setToolDirectoryAsWorkingDirectory; private TextStreamWatcher stdoutWatcher; private TextStreamWatcher stderrWatcher; private Writer stdoutWriter; private Writer stderrWriter; private Map<String, Object> stateMap; private boolean keepOnFailure; private Map<String, String> outputMapping; private LocalApacheCommandLineExecutor executor; private Set<String> outputsWithNotAValueWritten = new HashSet<>(); private volatile boolean canceled; @Override public void setComponentContext(ComponentContext componentContext) { this.componentContext = componentContext; componentLog = componentContext.getLog(); compEventAnnouncementDispatcher = componentContext.getService(ComponentEventAnnouncementDispatcher.class); } @Override public boolean treatStartAsComponentRun() { return componentContext.getInputs().isEmpty(); } @Override public void start() throws ComponentException { canceled = false; datamanagementService = componentContext.getService(ComponentDataManagementService.class); scriptingService = componentContext.getService(ScriptingService.class); typedDatumFactory = componentContext.getService(TypedDatumService.class).getFactory(); // Create basic folder structure and prepare sandbox String toolName = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_NAME); rootWDPath = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_ROOT_WORKING_DIRECTORY); String toolDirPath = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_DIRECTORY); URL platformUrl = null; // Avoid concurrent access as there is a known bug in Eclipse 3.7 when invoking Platform.getInstallLocation() from multiple threads synchronized (PLATFORM_ACCESS_LOCK) { platformUrl = Platform.getInstallLocation().getURL(); } // Should not happen if (platformUrl == null) { throw new ComponentException( "Unable to access the platform installation location. " + "This points to an error in the underlying eclipse platform. Please try to execute this component again."); } if (toolDirPath.equals(CURRENT_DIR) || toolDirPath.startsWith("./")) { try { toolDirPath.replaceFirst(CURRENT_DIR, platformUrl.toURI().toString()); } catch (URISyntaxException e) { LOG.debug("Could not get installation dir with URI, trying URL. ", e); toolDirPath.replaceFirst(CURRENT_DIR, platformUrl.getPath().toString()); } } sourceToolDirectory = new File(toolDirPath); if (!sourceToolDirectory.isAbsolute()) { try { sourceToolDirectory = new File(new File(platformUrl.toURI()), toolDirPath); } catch (URISyntaxException e) { sourceToolDirectory = new File(new File(platformUrl.getPath()), toolDirPath); } } useIterationDirectories = Boolean.parseBoolean(componentContext.getConfigurationValue( ToolIntegrationConstants.KEY_TOOL_USE_ITERATION_DIRECTORIES)); copyToolBehaviour = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_COPY_TOOL_BEHAVIOUR); dontCrashOnNonZeroExitCodes = componentContext .getConfigurationValue(ToolIntegrationConstants.DONT_CRASH_ON_NON_ZERO_EXIT_CODES) != null && Boolean.parseBoolean(componentContext.getConfigurationValue(ToolIntegrationConstants.DONT_CRASH_ON_NON_ZERO_EXIT_CODES)); setToolDirectoryAsWorkingDirectory = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_SET_TOOL_DIR_AS_WORKING_DIR) != null && Boolean.parseBoolean(componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_SET_TOOL_DIR_AS_WORKING_DIR)); if (copyToolBehaviour == null) { copyToolBehaviour = ToolIntegrationConstants.VALUE_COPY_TOOL_BEHAVIOUR_NEVER; } getToolDeleteBehaviour(); try { if (rootWDPath == null || rootWDPath.isEmpty()) { baseWorkingDirectory = TempFileServiceAccess.getInstance().createManagedTempDir(toolName); } else { baseWorkingDirectory = new File(rootWDPath + File.separator + toolName + "_" + UUID.randomUUID().toString() + File.separator); baseWorkingDirectory.mkdirs(); } FileUtils.write(new File(baseWorkingDirectory, "rce-workflow-info.txt"), "Workflow name: " + componentContext.getWorkflowInstanceName()); if (!useIterationDirectories) { iterationDirectory = baseWorkingDirectory; currentWorkingDirectory = baseWorkingDirectory; createFolderStructure(baseWorkingDirectory); } if (copyToolBehaviour.equals(ToolIntegrationConstants.VALUE_COPY_TOOL_BEHAVIOUR_ONCE)) { copySandboxToolConsideringRestriction(baseWorkingDirectory); } if (copyToolBehaviour.equals(ToolIntegrationConstants.VALUE_COPY_TOOL_BEHAVIOUR_NEVER)) { executionToolDirectory = sourceToolDirectory; } } catch (IOException e) { throw new ComponentException("Failed to create working directory", e); } prepareJythonForUsingModules(); initializeNewHistoryDataItem(); stateMap = new HashMap<>(); if (treatStartAsComponentRun()) { processInputs(); if (historyDataItem != null) { historyDataItem.setWorkingDirectory(currentWorkingDirectory.getAbsolutePath()); } } if ((copyToolBehaviour.equals(ToolIntegrationConstants.VALUE_COPY_TOOL_BEHAVIOUR_ALWAYS) || deleteToolBehaviour.equals(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS)) && !useIterationDirectories) { throw new ComponentException( "Tool shall be copied always but working directory is not new for each run. " + "Please check tool configuration \"Launch Settings\"."); } } protected boolean isMockMode() { boolean isMockMode = false; if (Boolean.valueOf(componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_MOCK_MODE_SUPPORTED)) && componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_IS_MOCK_MODE) != null) { isMockMode = Boolean.valueOf(componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_IS_MOCK_MODE)); } return isMockMode; } private void getToolDeleteBehaviour() { boolean deleteAlwaysActive = Boolean.parseBoolean(componentContext .getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS)); boolean deleteNeverActive = Boolean.parseBoolean(componentContext .getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_NEVER)); boolean deleteOnceActive = Boolean.parseBoolean(componentContext .getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ONCE)); if (componentContext.getConfigurationValue(ToolIntegrationConstants.CHOSEN_DELETE_TEMP_DIR_BEHAVIOR) != null) { deleteToolBehaviour = componentContext.getConfigurationValue(ToolIntegrationConstants.CHOSEN_DELETE_TEMP_DIR_BEHAVIOR); } else { determineDeletionBehaviour(deleteNeverActive, deleteOnceActive); } if ((ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS.equals(deleteToolBehaviour) && !deleteAlwaysActive) || (ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ONCE.equals(deleteToolBehaviour) && !deleteOnceActive) || (ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_NEVER.equals(deleteToolBehaviour) && !deleteNeverActive)) { String displayname = determineDeletionBehaviour(deleteNeverActive, deleteOnceActive); componentLog.componentWarn( StringUtils.format(DELETION_BEHAVIOR_ERROR_WARNING_MSG, componentContext.getInstanceName(), displayname)); } keepOnFailure = false; if (checkIfKeepOnFailureCanBeActive()) { keepOnFailure = Boolean.parseBoolean(componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_KEEP_ON_FAILURE)); } else { keepOnFailure = Boolean.parseBoolean(componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_KEEP_ON_FAILURE)); if (keepOnFailure) { keepOnFailure = false; componentLog.componentWarn(StringUtils.format(KEEP_ON_FAILURE_ERROR_MSG)); } } } private boolean checkIfKeepOnFailureCanBeActive() { if (componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_KEEP_ON_FAILURE) != null && !(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_NEVER.equals(deleteToolBehaviour))) { if (ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS.equals(deleteToolBehaviour)) { return Boolean.parseBoolean(componentContext .getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_KEEP_ON_ERROR_ITERATION)); } else if (ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ONCE.equals(deleteToolBehaviour)) { return Boolean.parseBoolean(componentContext .getConfigurationValue(ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_KEEP_ON_ERROR_ONCE)); } } return false; } private String determineDeletionBehaviour(boolean deleteNeverActive, boolean deleteOnceActive) { String chosenDisplayName = "\"Delete after every run.\""; deleteToolBehaviour = ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS; if (deleteOnceActive) { deleteToolBehaviour = ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ONCE; chosenDisplayName = "\"Delete after workflow execution.\""; } else if (deleteNeverActive) { deleteToolBehaviour = ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_NEVER; chosenDisplayName = "\"Do not delete\""; } return chosenDisplayName; } @Override public void processInputs() throws ComponentException { Map<String, TypedDatum> inputValues = new HashMap<>(); if (componentContext != null && componentContext.getInputsWithDatum() != null) { for (String inputName : componentContext.getInputsWithDatum()) { inputValues.put(inputName, componentContext.readInput(inputName)); } } // create iteration directory and prepare it if (useIterationDirectories) { iterationDirectory = new File(baseWorkingDirectory, "" + runCount++); currentWorkingDirectory = iterationDirectory; createFolderStructure(iterationDirectory); if (copyToolBehaviour.equals(ToolIntegrationConstants.VALUE_COPY_TOOL_BEHAVIOUR_ALWAYS)) { copySandboxToolConsideringRestriction(iterationDirectory); } } // create a list with used input values to delete them afterwards Map<String, String> inputNamesToLocalFile = new HashMap<>(); for (String inputName : inputValues.keySet()) { if (componentContext.getInputDataType(inputName) == DataType.FileReference) { inputNamesToLocalFile.put(inputName, copyInputFileToInputFolder(inputName, inputValues)); } else if (componentContext.getInputDataType(inputName) == DataType.DirectoryReference) { inputNamesToLocalFile.put(inputName, copyInputFileToInputFolder(inputName, inputValues)); } } // Create Conf files Set<String> configFileNames = new HashSet<>(); for (String configKey : componentContext.getConfigurationKeys()) { String configFilename = componentContext.getConfigurationMetaDataValue(configKey, ToolIntegrationConstants.KEY_PROPERTY_CONFIG_FILENAME); if (configFilename != null && !configFilename.isEmpty()) { configFileNames.add(configFilename); } } for (String filename : configFileNames) { File f = new File(configDirectory, filename); try { if (f.exists()) { f.delete(); } f.createNewFile(); for (String configKey : componentContext.getConfigurationKeys()) { String configFilename = componentContext.getConfigurationMetaDataValue(configKey, ToolIntegrationConstants.KEY_PROPERTY_CONFIG_FILENAME); if (configFilename != null && !configFilename.isEmpty() && configFilename.equals(filename)) { List<String> lines = FileUtils.readLines(f); lines.add(configKey + "=" + componentContext.getConfigurationValue(configKey)); FileUtils.writeLines(f, lines); } } } catch (IOException e) { throw new ComponentException("Failed to write configuration file: " + f.getAbsolutePath(), e); } } String preScript = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_PRE_SCRIPT); beforePreScriptExecution(inputValues, inputNamesToLocalFile); needsToRun = needToRun(inputValues, inputNamesToLocalFile); lastRunStaticInputValues = new HashMap<>(); lastRunStaticOutputValues = new HashMap<>(); if (needsToRun) { for (String inputName : inputValues.keySet()) { if (componentContext.isStaticInput(inputName) && inputValues.containsKey(inputName)) { lastRunStaticInputValues.put(inputName, inputValues.get(inputName)); } } if (isMockMode()) { performRunInMockMode(inputValues, inputNamesToLocalFile); } else { performRunInNormalMode(preScript, inputValues, inputNamesToLocalFile); } } else { for (String outputName : lastRunStaticOutputValues.keySet()) { componentContext.writeOutput(outputName, lastRunStaticOutputValues.get(outputName)); } } afterPostScriptExecution(inputValues, inputNamesToLocalFile); // Not that nice to look for a certain value as this is more workflow engine than component knowledge if (!Boolean.valueOf(componentContext.getConfigurationValue(ComponentConstants.COMPONENT_CONFIG_KEY_REQUIRES_OUTPUT_APPROVAL))) { deleteCurrentWorkingDirectoryIfRequired(); } if (needsToRun) { try { closeConsoleWriters(); } catch (IOException e) { LOG.error("Failed to close console writers", e); } } else { componentLog.componentInfo("Skipped tool execution as input(s) didn't change - output(s) from previous run sent"); } storeHistoryDataItem(); } private void deleteCurrentWorkingDirectoryIfRequired() { if (useIterationDirectories && ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS.equals(deleteToolBehaviour)) { try { FileUtils.deleteDirectory(currentWorkingDirectory); } catch (IOException e) { LOG.error(StringUtils.format("Failed to delete current working directory: %s", currentWorkingDirectory.getAbsolutePath()), e); } } } private void performRunInNormalMode(String preScript, Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { runScript(preScript, inputValues, inputNamesToLocalFile, "Pre"); beforeCommandExecution(inputValues, inputNamesToLocalFile); componentContext.announceExternalProgramStart(); int exitCode; try { exitCode = runCommand(inputValues, inputNamesToLocalFile); } finally { componentContext.announceExternalProgramTermination(); } afterCommandExecution(inputValues, inputNamesToLocalFile); // do not execute the post script if the execution was canceled prior if (canceled) { return; } String postScript = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_POST_SCRIPT); if (postScript != null) { postScript = ComponentUtils.replaceVariable(postScript, String.valueOf(exitCode), ToolIntegrationConstants.PLACEHOLDER_EXIT_CODE, ADD_PROPERTY_PLACEHOLDER); } runScript(postScript, inputValues, inputNamesToLocalFile, "Post"); } private void performRunInMockMode(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { beforeCommandExecution(inputValues, inputNamesToLocalFile); afterCommandExecution(inputValues, inputNamesToLocalFile); String postScript = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_MOCK_SCRIPT); runScript(postScript, inputValues, inputNamesToLocalFile, "Tool run imitation"); } // The before* and after* methods are for implementing own code in sub classes protected void afterPostScriptExecution(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { // LOG.debug("after postscript execution"); } protected void beforePreScriptExecution(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { // LOG.debug("Before prescript execution"); } protected void beforeCommandExecution(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { // LOG.debug("before command execution"); } protected void afterCommandExecution(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { // LOG.debug("after command execution"); } // The needToRun() method gives the ability to control if the integrated tool should run on some // conditions or not. protected boolean needToRun(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { // Standard case --> run always return true; } @Override public void completeStartOrProcessInputsAfterFailure() throws ComponentException { storeHistoryDataItem(); } @Override public void tearDown(FinalComponentState state) { super.tearDown(state); switch (state) { case FAILED: case CANCELLED: deleteBaseWorkingDirectory(false); break; case FINISHED: deleteBaseWorkingDirectory(true); break; default: break; } } private int runCommand(Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile) throws ComponentException { String commScript = null; int exitCode = 0; if (OS.isFamilyWindows() && (Boolean.parseBoolean(componentContext.getConfigurationValue( ToolIntegrationConstants.KEY_COMMAND_SCRIPT_WINDOWS_ENABLED)))) { commScript = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_COMMAND_SCRIPT_WINDOWS); commScript = replacePlaceholder(commScript, inputValues, inputNamesToLocalFile, SubstitutionContext.WINDOWS_BATCH); } else if (OS.isFamilyUnix() && (Boolean.parseBoolean(componentContext.getConfigurationValue( ToolIntegrationConstants.KEY_COMMAND_SCRIPT_LINUX_ENABLED)))) { commScript = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_COMMAND_SCRIPT_LINUX); commScript = replacePlaceholder(commScript, inputValues, inputNamesToLocalFile, SubstitutionContext.LINUX_BASH); } else { throw new ComponentException(StringUtils.format("No command(s) for operating system %s defined", System.getProperty("os.name"))); } try { componentLog.componentInfo("Executing command(s)..."); synchronized (this) { if (setToolDirectoryAsWorkingDirectory) { executor = new LocalApacheCommandLineExecutor(executionToolDirectory); } else { executor = new LocalApacheCommandLineExecutor(currentWorkingDirectory); } // if cancel was called before the executor was created, we cancel it now before the actual execution if (canceled) { executor.cancel(); } executor.executeScript(commScript, null); stdoutWatcher = ConsoleRowUtils.logToWorkflowConsole(componentLog, executor.getStdout(), ConsoleRow.Type.TOOL_OUT, null, false); stderrWatcher = ConsoleRowUtils.logToWorkflowConsole(componentLog, executor.getStderr(), ConsoleRow.Type.TOOL_ERROR, null, false); if (canceled) { stdoutWatcher.cancel(); stderrWatcher.cancel(); } } try { exitCode = executor.waitForTermination(); stdoutWatcher.waitForTermination(); stderrWatcher.waitForTermination(); } catch (CancellationException e) { LOG.debug("Execution canceled while waiting for termination of TextStreamWatcher."); exitCode = 1; } componentLog.componentInfo("Command(s) executed - exit code: " + exitCode); if (historyDataItem != null) { historyDataItem.setExitCode(exitCode); } // check if tool execution was canceled by the user if (canceled) { return exitCode; } if (!dontCrashOnNonZeroExitCodes && exitCode != 0) { throw new ComponentException(StringUtils.format("Command(s) execution terminated abnormally with exit code: %d", exitCode)); } } catch (IOException | InterruptedException e) { throw new ComponentException("Failed to execute command(s)", e); } return exitCode; } private void runScript(String script, Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile, String scriptPrefix) throws ComponentException { // As the Jython script engine is not thread safe (console outputs of multiple script // executions are mixed), we must ensure that at most one script is executed at the same // time synchronized (ScriptingUtils.SCRIPT_EVAL_LOCK_OBJECT) { Object exitCode = null; if (script != null && !script.isEmpty()) { ScriptLanguage scriptLanguage = ScriptLanguage.getByName(SCRIPT_LANGUAGE); final ScriptEngine engine = scriptingService.createScriptEngine(scriptLanguage); Map<String, Object> scriptExecConfig = null; engine.put("config", scriptExecConfig); prepareScriptOutputForRun(engine); componentLog.componentInfo(StringUtils.format("Executing %s script...", scriptPrefix.toLowerCase())); if (useIterationDirectories) { workingPath = createJythonPath(currentWorkingDirectory); } script = replacePlaceholder(script, inputValues, inputNamesToLocalFile, SubstitutionContext.JYTHON); try { engine.eval("RCE_Bundle_Jython_Path = " + QUOTE + jythonPath + QUOTE); if (!setToolDirectoryAsWorkingDirectory) { engine.eval("RCE_Temp_working_path = " + QUOTE + workingPath + QUOTE); } else { engine.eval("RCE_Temp_working_path = " + QUOTE + createJythonPath(executionToolDirectory) + QUOTE); } String headerScript = ScriptingUtils.prepareHeaderScript(stateMap, componentContext, inputDirectory, new LinkedList<File>()); engine.eval(headerScript); engine.eval(prepareTableInput(inputValues)); exitCode = engine.eval(script); String footerScript = "\nRCE_Dict_OutputChannels = RCE.get_output_internal()\nRCE_CloseOutputChannelsList = " + "RCE.get_closed_outputs_internal()\n" + StringUtils.format("sys.stdout.write('%s')\nsys.stderr.write('%s')\nsys.stdout.flush()\nsys.stderr.flush()", WorkflowConsoleForwardingWriter.CONSOLE_END, WorkflowConsoleForwardingWriter.CONSOLE_END); engine.eval(footerScript); ((WorkflowConsoleForwardingWriter) engine.getContext().getWriter()).awaitPrintingLinesFinished(); ((WorkflowConsoleForwardingWriter) engine.getContext().getErrorWriter()).awaitPrintingLinesFinished(); String message = StringUtils.format("%s script executed", scriptPrefix); if (exitCode != null) { componentLog.componentInfo(message + " - exit code: " + exitCode); } else { componentLog.componentInfo(message); } } catch (ScriptException e) { throw new ComponentException(StringUtils.format("Failed to execute %s script", scriptPrefix.toLowerCase()), e); } catch (InterruptedException e) { LOG.error(StringUtils.format("Failed to wait for stdout or stderr writer of to finish (%s (%s))", componentContext.getInstanceName(), componentContext.getExecutionIdentifier()), e); } if (exitCode != null && (!(exitCode instanceof Integer) || (((Integer) exitCode).intValue() != 0))) { throw new ComponentException(StringUtils.format("Execution of %s script terminated abnormally - exit code: %s", scriptPrefix.toLowerCase(), exitCode)); } writeOutputValues(engine, scriptExecConfig); } } } private String prepareTableInput(Map<String, TypedDatum> inputValues) { String script = ""; for (String inputName : inputValues.keySet()) { if (componentContext.getInputDataType(inputName) == DataType.Vector) { script += inputName + "= ["; for (FloatTD floatEntry : ((VectorTD) inputValues.get(inputName)).toArray()) { script += floatEntry.getFloatValue() + ","; } script = script.substring(0, script.length() - 1) + "]\n"; } if (componentContext.getInputDataType(inputName) == DataType.Matrix) { script += inputName + "= ["; MatrixTD matrix = ((MatrixTD) componentContext.readInput(inputName)); for (int i = 0; i < matrix.getRowDimension(); i++) { script += "["; for (int j = 0; j < matrix.getColumnDimension(); j++) { script += matrix.getFloatTDOfElement(i, j).getFloatValue() + ","; } script = script.substring(0, script.length() - 1) + "],"; } script = script.substring(0, script.length() - 1) + "]\n"; } } return script; } @SuppressWarnings("unchecked") private void writeOutputValues(ScriptEngine engine, Map<String, Object> scriptExecConfig) throws ComponentException { final Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); /* * Extract all values calculated/set in the script to a custom scope so the calculated/set values are accessible via the current * Context. */ for (final String key : bindings.keySet()) { Object value = bindings.get(key); if (value != null && value.getClass().getSimpleName().equals("NativeJavaObject")) { try { value = value.getClass().getMethod("unwrap").invoke(value); } catch (IllegalArgumentException | SecurityException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new ComponentException("Failed to extract output values from post script", e); } } if (scriptExecConfig != null) { scriptExecConfig.put(key, value); } if (value != null && outputMapping.containsKey(key)) { if (componentContext.getOutputDataType(outputMapping.get(key)) == DataType.FileReference || componentContext.getOutputDataType(outputMapping.get(key)) == DataType.DirectoryReference) { File file = new File(value.toString()); if (!file.isAbsolute()) { file = new File(currentWorkingDirectory, value.toString()); } if (!file.exists()) { throw new ComponentException(StringUtils.format("File for output '%s' doesn't exist: %s", outputMapping.get(key), file.getAbsolutePath())); } try { if (componentContext.getOutputDataType(outputMapping.get(key)) == DataType.FileReference) { String metafilename = componentContext.getOutputMetaDataValue(outputMapping.get(key), ToolIntegrationConstants.KEY_ENDPOINT_FILENAME); String filename = file.getName(); if (metafilename != null && !metafilename.isEmpty()) { filename = metafilename; } FileReferenceTD uuid = datamanagementService.createFileReferenceTDFromLocalFile(componentContext, file, filename); componentContext.writeOutput(outputMapping.get(key), uuid); lastRunStaticOutputValues.put(outputMapping.get(key), uuid); } else { String metafilename = componentContext.getOutputMetaDataValue(outputMapping.get(key), ToolIntegrationConstants.KEY_ENDPOINT_FILENAME); String filename = file.getName(); if (metafilename != null && !metafilename.isEmpty()) { filename = metafilename; } DirectoryReferenceTD uuid = datamanagementService.createDirectoryReferenceTDFromLocalDirectory( componentContext, file, filename); componentContext.writeOutput(outputMapping.get(key), uuid); lastRunStaticOutputValues.put(outputMapping.get(key), uuid); } } catch (IOException e) { throw new ComponentException(StringUtils.format("Failed to store file/directory '%s' into the data management" + " - if it is not stored in the data management it can not be sent as value for output '%s'", file.getAbsolutePath(), outputMapping.get(key)), e); } } else { TypedDatum valueTD = ScriptDataTypeHelper.getTypedDatum(value, typedDatumFactory); componentContext.writeOutput(outputMapping.get(key), valueTD); lastRunStaticOutputValues.put(outputMapping.get(key), valueTD); } } } ScriptingUtils.writeAPIOutput(stateMap, componentContext, engine, workingPath, historyDataItem); outputsWithNotAValueWritten.addAll(ScriptingUtils.getOutputsSendingNotAValue(engine, componentContext)); for (String outputName : (List<String>) engine.get("RCE_CloseOutputChannelsList")) { componentContext.closeOutput(outputName); } Map<String, Object> stateMapOutput = (Map<String, Object>) engine.get("RCE_STATE_VARIABLES"); for (String key : stateMapOutput.keySet()) { stateMap.put(key, stateMapOutput.get(key)); } } private String replacePlaceholder(String script, Map<String, TypedDatum> inputValues, Map<String, String> inputNamesToLocalFile, SubstitutionContext context) throws ComponentException { if (inputValues != null) { for (String inputName : inputValues.keySet()) { if (inputValues.containsKey(inputName) && script.contains(StringUtils.format(INPUT_PLACEHOLDER, inputName))) { if (componentContext.getInputDataType(inputName) == DataType.FileReference) { script = script.replace(StringUtils.format(INPUT_PLACEHOLDER, inputName), inputNamesToLocalFile.get(inputName).replaceAll(ESCAPESLASH, SLASH)); } else if (componentContext.getInputDataType(inputName) == DataType.DirectoryReference) { script = script.replace(StringUtils.format(INPUT_PLACEHOLDER, inputName), inputNamesToLocalFile.get(inputName).replaceAll(ESCAPESLASH, SLASH)); } else if (componentContext.getInputDataType(inputName) == DataType.Vector) { script = script.replace(StringUtils.format(INPUT_PLACEHOLDER, inputName), validate(inputName, context, StringUtils.format("Name of Vector '%s'" + SUBSTITUTION_ERROR_MESSAGE_PREFIX, inputName))); } else if (componentContext.getInputDataType(inputName) == DataType.Matrix) { script = script.replace(StringUtils.format(INPUT_PLACEHOLDER, inputName), validate(inputName, context, StringUtils.format("Name of Vector '%s'" + SUBSTITUTION_ERROR_MESSAGE_PREFIX, inputName))); } else { String value = inputValues.get(inputName).toString(); if (context == SubstitutionContext.JYTHON && componentContext.getInputDataType(inputName) == DataType.Boolean) { value = value.substring(0, 1).toUpperCase() + value.substring(1); } script = script.replace(StringUtils.format(INPUT_PLACEHOLDER, inputName), validate(value, context, StringUtils.format("Value '%s' from input '%s'" + SUBSTITUTION_ERROR_MESSAGE_PREFIX, value, inputName))); } } } } script = replaceOutputVariables(script, componentContext.getOutputs(), OUTPUT_PLACEHOLDER); Map<String, String> properties = new HashMap<>(); // get properties! for (String configKey : componentContext.getConfigurationKeys()) { String value = componentContext.getConfigurationValue(configKey); validate(value, context, StringUtils.format("Value '%s' of property '%s'" + SUBSTITUTION_ERROR_MESSAGE_PREFIX, value, configKey)); properties.put(configKey, componentContext.getConfigurationValue(configKey)); } script = ComponentUtils.replacePropertyVariables(script, properties, PROPERTY_PLACEHOLDER); script = ComponentUtils.replaceVariable(script, configDirectory.getAbsolutePath(), ToolIntegrationConstants.DIRECTORIES_PLACEHOLDER[0], DIRECTORY_PLACEHOLDER_TEMPLATE); script = ComponentUtils.replaceVariable(script, currentWorkingDirectory.getAbsolutePath(), ToolIntegrationConstants.DIRECTORIES_PLACEHOLDER[1], DIRECTORY_PLACEHOLDER_TEMPLATE); script = ComponentUtils.replaceVariable(script, currentWorkingDirectory.getAbsolutePath(), ToolIntegrationConstants.DIRECTORIES_PLACEHOLDER[1] + "Dir", DIRECTORY_PLACEHOLDER_TEMPLATE); script = ComponentUtils.replaceVariable(script, inputDirectory.getAbsolutePath(), ToolIntegrationConstants.DIRECTORIES_PLACEHOLDER[2], DIRECTORY_PLACEHOLDER_TEMPLATE); script = ComponentUtils.replaceVariable(script, outputDirectory.getAbsolutePath(), ToolIntegrationConstants.DIRECTORIES_PLACEHOLDER[4], DIRECTORY_PLACEHOLDER_TEMPLATE); script = ComponentUtils.replaceVariable(script, executionToolDirectory.getAbsolutePath(), ToolIntegrationConstants.DIRECTORIES_PLACEHOLDER[3], DIRECTORY_PLACEHOLDER_TEMPLATE); return script; } private String replaceOutputVariables(String script, Set<String> outputs, String outputPlaceholder) { outputMapping = new HashMap<>(); for (String outputName : outputs) { String outputID = "_RCE_OUTPUT_" + UUID.randomUUID().toString().replaceAll("-", "_"); script = script.replace(StringUtils.format(outputPlaceholder, outputName), outputID); outputMapping.put(outputID, outputName); } return script; } private String validate(String key, SubstitutionContext context, String errorMsg) throws ComponentException { if (!StringSubstitutionSecurityUtils.isSafeForSubstitutionInsideDoubleQuotes(key, context)) { throw new ComponentException(errorMsg); } return key; } private String copyInputFileToInputFolder(String inputName, Map<String, TypedDatum> inputValues) throws ComponentException { File targetFile = null; TypedDatum fileReference = inputValues.get(inputName); try { if (componentContext.getInputDataType(inputName) == DataType.FileReference) { String fileName = ((FileReferenceTD) fileReference).getFileName(); /** * Commented out because of bug with renaming file / dir */ // if (!componentContext.getInputMetaDataValue(inputName, // ToolIntegrationConstants.KEY_ENDPOINT_FILENAME).isEmpty()) { // fileName = componentContext.getInputMetaDataValue(inputName, // ToolIntegrationConstants.KEY_ENDPOINT_FILENAME); // } targetFile = new File(inputDirectory.getAbsolutePath(), inputName + File.separator + fileName); if (targetFile.exists()) { FileUtils.forceDelete(targetFile); } datamanagementService.copyFileReferenceTDToLocalFile(componentContext, (FileReferenceTD) fileReference, targetFile); } else { // String fileName = ((DirectoryReferenceTD) fileReference).getDirectoryName(); targetFile = new File(inputDirectory.getAbsolutePath(), inputName); if (targetFile.exists()) { FileUtils.forceDelete(targetFile); } datamanagementService.copyDirectoryReferenceTDToLocalDirectory(componentContext, (DirectoryReferenceTD) fileReference, targetFile); /** * Commented out because of bug with renaming file / dir */ // if (componentContext.getInputMetaDataValue(inputName, // ToolIntegrationConstants.KEY_ENDPOINT_FILENAME) != null // && !componentContext.getInputMetaDataValue(inputName, // ToolIntegrationConstants.KEY_ENDPOINT_FILENAME).isEmpty()) { // fileName = componentContext.getInputMetaDataValue(inputName, // ToolIntegrationConstants.KEY_ENDPOINT_FILENAME); // File newTarget = new File(new File(inputDirectory.getAbsolutePath(), inputName), // fileName); // FileUtils.moveDirectory(new File(targetFile, ((DirectoryReferenceTD) // fileReference).getDirectoryName()), newTarget); // targetFile = newTarget; // } targetFile = new File(targetFile, ((DirectoryReferenceTD) fileReference).getDirectoryName()); } } catch (IOException e) { throw new ComponentException(StringUtils.format("Failed to write incoming file of input '%s' into working directory: %s", inputName, targetFile.getAbsolutePath()), e); } return targetFile.getAbsolutePath(); } private void copySandboxTool(File directory) throws ComponentException { File targetToolDir = new File(directory + File.separator + sourceToolDirectory.getName()); boolean copiedToolDir = false; try { FileUtils.copyDirectory(sourceToolDirectory, targetToolDir); componentLog.componentInfo("Copied tool directory '" + sourceToolDirectory.getName() + "' to working directory"); copiedToolDir = true; } catch (IOException e) { throw new ComponentException(StringUtils.format("Failed to copy tool directory: %s", sourceToolDirectory.getAbsolutePath()), e); } if (copiedToolDir) { executionToolDirectory = targetToolDir; Iterator<File> it = FileUtils.iterateFiles(targetToolDir, null, true); while (it.hasNext()) { File f = it.next(); f.setExecutable(true, false); } } } private void copySandboxToolConsideringRestriction(File directory) throws ComponentException { try { COPY_TOOL_SEMAPHORE.acquire(); } catch (InterruptedException e) { throw new ComponentException("Internal error: Interrupted while waiting for the release to copy the tool", e); } try { copySandboxTool(directory); } finally { COPY_TOOL_SEMAPHORE.release(); } } private void createFolderStructure(File directory) { inputDirectory = new File(directory.getAbsolutePath() + File.separator + ToolIntegrationConstants.COMPONENT_INPUT_FOLDER_NAME + File.separator); inputDirectory.mkdirs(); outputDirectory = new File(directory.getAbsolutePath() + File.separator + ToolIntegrationConstants.COMPONENT_OUTPUT_FOLDER_NAME + File.separator); outputDirectory.mkdirs(); configDirectory = new File(directory.getAbsolutePath() + File.separator + ToolIntegrationConstants.COMPONENT_CONFIG_FOLDER_NAME + File.separator); configDirectory.mkdirs(); componentLog.componentInfo("Created working directory: " + directory.getAbsolutePath()); } private void deleteBaseWorkingDirectory(boolean workflowSuccess) { if ((ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ONCE.equals(deleteToolBehaviour) || ToolIntegrationConstants.KEY_TOOL_DELETE_WORKING_DIRECTORIES_ALWAYS.equals(deleteToolBehaviour)) && !(keepOnFailure && !workflowSuccess) && baseWorkingDirectory != null && baseWorkingDirectory.exists()) { try { if (rootWDPath == null || rootWDPath.isEmpty()) { TempFileServiceAccess.getInstance().disposeManagedTempDirOrFile(baseWorkingDirectory); } else { FileDeleteStrategy.FORCE.delete(baseWorkingDirectory); } componentLog.componentInfo("Deleted working directory: " + baseWorkingDirectory.getAbsolutePath()); } catch (IOException e) { baseWorkingDirectory.deleteOnExit(); LOG.error(StringUtils.format("Failed to delete working directory: %s", baseWorkingDirectory.getAbsolutePath()), e); } } } protected void bindScriptingService(final ScriptingService service) { scriptingService = service; } protected void bindComponentDataManagementService(ComponentDataManagementService compDataManagementService) { datamanagementService = compDataManagementService; } private void prepareScriptOutputForRun(ScriptEngine scriptEngine) { final int buffer = 1024; StringWriter out = new StringWriter(buffer); StringWriter err = new StringWriter(buffer); stdoutWriter = new WorkflowConsoleForwardingWriter(out, componentLog, ConsoleRow.Type.TOOL_OUT); stderrWriter = new WorkflowConsoleForwardingWriter(err, componentLog, ConsoleRow.Type.TOOL_ERROR); scriptEngine.getContext().setWriter(stdoutWriter); scriptEngine.getContext().setErrorWriter(stderrWriter); } private void prepareJythonForUsingModules() throws ComponentException { try { jythonPath = ScriptingUtils.getJythonPath(); } catch (IOException e) { throw new ComponentException("Internal error: Failed to initialize Jython", e); } if (jythonPath == null) { throw new ComponentException("Internal error: Failed to initialize Jython"); } File file = new File(baseWorkingDirectory.getAbsolutePath(), "jython-import-" + UUID.randomUUID().toString() + ".tmp"); workingPath = createJythonPath(file); } private String createJythonPath(File file) { String path = file.getAbsolutePath().toString(); path = path.replaceAll(ESCAPESLASH, SLASH); String[] splitted = path.split(SLASH); path = ""; int lastEntry = splitted.length; if (!file.isDirectory()) { lastEntry--; } for (int i = 0; i < lastEntry; i++) { path += splitted[i] + SLASH; } return path; } protected void closeConsoleWriters() throws IOException { if (stdoutWriter != null) { stdoutWriter.flush(); stdoutWriter.close(); } if (stderrWriter != null) { stderrWriter.flush(); stderrWriter.close(); } } protected void initializeNewHistoryDataItem() { if (Boolean.valueOf(componentContext.getConfigurationValue(ComponentConstants.CONFIG_KEY_STORE_DATA_ITEM))) { historyDataItem = new IntegrationHistoryDataItem(componentContext.getComponentIdentifier()); } } private void storeHistoryDataItem() { if (historyDataItem != null) { historyDataItem.setWorkingDirectory(currentWorkingDirectory.getAbsolutePath()); } if (historyDataItem != null && Boolean.valueOf(componentContext.getConfigurationValue(ComponentConstants.CONFIG_KEY_STORE_DATA_ITEM))) { componentContext.writeFinalHistoryDataItem(historyDataItem); } } @Override public synchronized void onStartInterrupted(ThreadHandler executingThreadHandler) { canceled = true; // the command might not be started yet if (executor != null) { executor.cancel(); } } @Override public synchronized void onProcessInputsInterrupted(ThreadHandler executingThreadHandler) { canceled = true; // the command might not be started yet if (executor != null) { executor.cancel(); } } @Override public void handleVerificationToken(String verificationToken) throws ComponentException { String tokenLocation = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_VERIFICATION_TOKEN_LOCATION); if (tokenLocation == null) { tokenLocation = currentWorkingDirectory.getAbsolutePath(); } boolean verificationTokenAnnounced = false; String verificationTokenFileContent = createVerificationFileContent(verificationToken); File verificationTokenFile = writeVerificationTokenToFile(tokenLocation, verificationTokenFileContent); if (verificationTokenFile != null) { verificationTokenAnnounced = true; } String[] recipients = getEmailRecipientsForApprovalRequestAnnouncement(); if (recipients.length > 0) { String verificationTokenFilePath; if (verificationTokenFile == null) { verificationTokenFilePath = "n/a"; } else { verificationTokenFilePath = verificationTokenFile.getAbsolutePath(); } if (announceRequestForOutputApprovalViaMail(verificationTokenFileContent, verificationTokenFilePath, recipients)) { verificationTokenAnnounced = true; } } if (!verificationTokenAnnounced) { throw new ComponentException("Failed to announce verification key; neither file was created nor an email was sent"); } componentLog.componentInfo("Waiting for approval..."); } private String createVerificationFileContent(String verificationToken) { String contentTemplate; try { contentTemplate = IOUtils.toString(getClass().getResourceAsStream("/file_template_result_verification.txt")); } catch (IOException e) { throw new RuntimeException("Failed to load file template for verification key file", e); } return StringUtils.format(contentTemplate, verificationToken, componentContext.getComponentName(), componentContext.getExecutionCount(), currentWorkingDirectory, componentContext.getWorkflowInstanceName()); } private File writeVerificationTokenToFile(String tokenLocation, String verificationTokenFileContent) { File verificationTokenFile; String baseVerificationFileName = "verification-key"; String verificationFileName = baseVerificationFileName; synchronized (VERIFICATION_TOKEN_WRITE_LOCK) { int i = 1; while (new File(new File(tokenLocation), verificationFileName).exists()) { verificationFileName = baseVerificationFileName + " (" + i++ + ")"; } verificationTokenFile = new File(new File(tokenLocation), verificationFileName); try { FileUtils.write(verificationTokenFile, verificationTokenFileContent); } catch (IOException e) { String message = "Failed to create file with verification key"; LOG.error(message, e); componentLog.componentError(message + "; " + e.getMessage()); return null; } } componentLog.componentInfo("File with verification key created"); return verificationTokenFile; } private String[] getEmailRecipientsForApprovalRequestAnnouncement() { String recipientsString = componentContext.getConfigurationValue(ToolIntegrationConstants.KEY_VERIFICATION_TOKEN_RECIPIENTS); if (recipientsString == null) { return new String[0]; } return recipientsString.trim().split(ToolIntegrationConstants.VERIFICATION_TOKEN_RECIPIENTS_SEPARATOR); } private boolean announceRequestForOutputApprovalViaMail(String verificationTokenFileContent, String verificationTokenFilePath, String[] recipients) { for (int i = 0; i < recipients.length; i++) { recipients[i] = recipients[i].trim(); } String subject = StringUtils.format("Request for result approval for tool '%s'", componentContext.getComponentName()); String contentTemplate; try { contentTemplate = IOUtils.toString(getClass().getResourceAsStream("/mail_template_result_verification.txt")); } catch (IOException e) { throw new RuntimeException("Failed to load file template for verification key email", e); } final String verificationTokenMailContent = StringUtils.format(contentTemplate, componentContext.getComponentName(), verificationTokenFileContent, verificationTokenFilePath); ComponentEventAnnouncement compEventAnnouncement = ComponentEventAnnouncement.createAnnouncement(WorkflowEventType.REQUEST_FOR_OUTPUT_APPROVAL, subject, verificationTokenMailContent); if (compEventAnnouncementDispatcher.dispatchWorkflowEventAnnouncementViaMail(recipients, compEventAnnouncement, new TextLinesReceiver() { @Override public void addLines(List<String> lines) { for (String line : lines) { addLine(line); } } @Override public void addLines(String... lines) { for (String line : lines) { componentLog.componentError(line); LOG.error(line); } } @Override public void addLine(String line) { addLines(line); } })) { componentLog.componentInfo("Email with verification key sent"); return true; } else { return false; } } @Override public void completeStartOrProcessInputsAfterVerificationDone() throws ComponentException { deleteCurrentWorkingDirectoryIfRequired(); } protected Set<String> getOutputsWithNotAValueWritten() { return outputsWithNotAValueWritten; } }