/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.components.script.execution.validator; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.components.script.common.ScriptComponentConstants; import de.rcenvironment.components.script.execution.Messages; import de.rcenvironment.core.component.executor.SshExecutorConstants; import de.rcenvironment.core.component.model.api.ComponentDescription; import de.rcenvironment.core.component.validation.api.ComponentValidationMessage; import de.rcenvironment.core.component.validation.spi.AbstractComponentValidator; import de.rcenvironment.core.scripting.python.PythonComponentConstants; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.textstream.TextStreamWatcher; import de.rcenvironment.core.utils.common.textstream.receivers.CapturingTextOutReceiver; import de.rcenvironment.core.utils.executor.LocalApacheCommandLineExecutor; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Validator for script component. * * @author Sascha Zur * @author Jascha Riedel * @author Martin Misiak * @author David Scholz */ public class ScriptComponentValidator extends AbstractComponentValidator { private static final String PYTHON_VALIDATION_ERROR = "Validation of python path failed."; private static final Long PYTHON_TEST_TIMEOUT = 1L; private static final int MINUS_ONE = -1; private static final String COLON = ": "; private Log logger = LogFactory.getLog(ScriptComponentValidator.class); @Override public String getIdentifier() { return ScriptComponentConstants.COMPONENT_ID; } @Override protected List<ComponentValidationMessage> validateComponentSpecific(ComponentDescription componentDescription) { final List<ComponentValidationMessage> messages = new ArrayList<ComponentValidationMessage>(); String script = getProperty(componentDescription, SshExecutorConstants.CONFIG_KEY_SCRIPT); if (script == null || script.isEmpty()) { final ComponentValidationMessage noScriptMessage = new ComponentValidationMessage( ComponentValidationMessage.Type.ERROR, SshExecutorConstants.CONFIG_KEY_SCRIPT, Messages.noScript, Messages.noScript + " defined"); messages.add(noScriptMessage); } else if (script.endsWith(ScriptComponentConstants.DEFAULT_SCRIPT_LAST_LINE)) { final ComponentValidationMessage defaultScriptMessage = new ComponentValidationMessage( ComponentValidationMessage.Type.WARNING, SshExecutorConstants.CONFIG_KEY_SCRIPT, Messages.defaultScriptMessage, Messages.defaultScriptMessage); messages.add(defaultScriptMessage); } else if (!checkScriptIndentationConsistency(script)) { final ComponentValidationMessage inconsistentScriptMessage = new ComponentValidationMessage( ComponentValidationMessage.Type.WARNING, SshExecutorConstants.CONFIG_KEY_SCRIPT, Messages.scriptInconsistentIndentation, Messages.scriptInconsistentIndentation + COLON + SshExecutorConstants.CONFIG_KEY_SCRIPT); messages.add(inconsistentScriptMessage); } return messages; } /** * The method returns true if either whitespaces or tabs are exclusively used for the indentation of a script. False otherwise. * * @param script String containing the script * @return */ private boolean checkScriptIndentationConsistency(String script) { String regexWs = "^ +([\\S].*)$"; String regexWsOnly = "^( +)$"; String regexTab = "^\\t+([\\S].*)$"; String regexTabOnly = "^(\\t+)$"; String eol = System.getProperty("line.separator"); String[] scriptLines = script.split(eol); boolean ws = false; boolean tab = false; for (int i = 0; i < scriptLines.length; i++) { if (!ws) { ws = scriptLines[i].matches(regexWs) || scriptLines[i].matches(regexWsOnly); } if (!tab) { tab = scriptLines[i].matches(regexTab) || scriptLines[i].matches(regexTabOnly); } if (scriptLines[i].matches("^(( +\\t+)|(\\t+ +)).*")) { return false; } } if (ws && tab) { return false; } return true; } @Override protected List<ComponentValidationMessage> validateOnWorkflowStartComponentSpecific( ComponentDescription componentDescription) { // TODO Auto-generated method stub return null; } @Override public List<ComponentValidationMessage> validateOnWorkflowStart(ComponentDescription componentDescription) { final List<ComponentValidationMessage> messages = new ArrayList<ComponentValidationMessage>(); if (getProperty(componentDescription, ScriptComponentConstants.SCRIPT_LANGUAGE).equals("Python")) { String pythonInstallation = getProperty(componentDescription, PythonComponentConstants.PYTHON_INSTALLATION); if (!pythonInstallation.isEmpty()) { final LocalApacheCommandLineExecutor executor; TextStreamWatcher stdOutTextStreamWatcher; TextStreamWatcher stdErrTextStreamWatcher; final PythonVersionRegexValidator validator; String command = "\"" + pythonInstallation + "\"" + " --version"; try { validator = new PythonVersionRegexValidator(); executor = new LocalApacheCommandLineExecutor(new File("/")); executor.start(command); stdOutTextStreamWatcher = new TextStreamWatcher(executor.getStdout(), ConcurrencyUtils.getAsyncTaskService(), new CapturingTextOutReceiver("") { @Override public synchronized void addOutput(String line) { super.addOutput(line); validator.validatePythonVersion(getBufferedOutput().toString()); } }); stdErrTextStreamWatcher = new TextStreamWatcher(executor.getStderr(), ConcurrencyUtils.getAsyncTaskService(), new CapturingTextOutReceiver("") { @Override public synchronized void addOutput(String line) { super.addOutput(line); validator.validatePythonVersion(getBufferedOutput().toString()); } }); stdOutTextStreamWatcher.start(); stdErrTextStreamWatcher.start(); final AsyncTaskService asyncTaskService = ConcurrencyUtils.getAsyncTaskService(); Future<?> task = asyncTaskService.submit(new Runnable() { @TaskDescription("Waits for python validation to finish") @Override public void run() { try { executor.waitForTermination(); } catch (IOException e) { logger.error(PYTHON_VALIDATION_ERROR, e); } catch (InterruptedException e) { logger.error(PYTHON_VALIDATION_ERROR, e); } } }); try { task.get(PYTHON_TEST_TIMEOUT, TimeUnit.SECONDS); } catch (TimeoutException e) { executor.cancel(); } catch (ExecutionException e) { logger.error(PYTHON_VALIDATION_ERROR, e); } finally { executor.cancel(); } processPythonValidationResult(validator, messages, pythonInstallation); } catch (IOException | InterruptedException e) { logger.error(PYTHON_VALIDATION_ERROR, e); } } } return messages; } private void createPythonExecutionSuccessfulMessage(PythonVersionRegexValidator validator) { LogFactory.getLog(this.getClass()) .debug("Python Version Used: " + validator.getMajorPythonVersion() + "." + validator.getMinorPythonVersion() + "." + validator.getMicroPythonVersion()); } private void processPythonValidationResult(PythonVersionRegexValidator validator, List<ComponentValidationMessage> messages, String path) { if (!validator.isPythonExecutionSuccessful() && validator.getMajorPythonVersion() == MINUS_ONE) { final ComponentValidationMessage message = new ComponentValidationMessage( ComponentValidationMessage.Type.ERROR, PythonComponentConstants.PYTHON_INSTALLATION, Messages.pythonExecutionTestErrorRelative, StringUtils.format(Messages.pythonExecutionTestErrorRelative, path)); messages.add(message); } else if (!validator.isPythonExecutionSuccessful() && (validator.getMinorPythonVersion() < 6 && validator.getMajorPythonVersion() >= 2)) { final ComponentValidationMessage message = new ComponentValidationMessage( ComponentValidationMessage.Type.ERROR, PythonComponentConstants.PYTHON_INSTALLATION, Messages.pythonExecutionUnsupportedVersionRelative, Messages.pythonExecutionUnsupportedVersionRelative); messages.add(message); } else if (validator.isPythonExecutionSuccessful()) { createPythonExecutionSuccessfulMessage(validator); } } /** * * @return the {@link PythonVersionRegexValidator}. Intended for unit testing. */ public PythonVersionRegexValidator createPythonVersionRegexValidator() { return new PythonVersionRegexValidator(); } /** * * @author David Scholz * */ public class PythonVersionRegexValidator { private static final int MINUS_ONE = -1; private boolean pythonExecutionSuccessful = false; private final Pattern matchPattern; private int majorPythonVersion = MINUS_ONE; private int minorPythonVersion = MINUS_ONE; private int microPythonVersion = MINUS_ONE; PythonVersionRegexValidator() { this.matchPattern = Pattern.compile("^Python\\s([0-9]+)\\.([0-9]+)\\.([0-9]+)"); } /** * * @param bufferedOutput stdout or stderr of the python exe. */ public synchronized void validatePythonVersion(String bufferedOutput) { Matcher matcher = matchPattern.matcher(bufferedOutput); if (matcher.find()) { majorPythonVersion = Integer.parseInt(matcher.group(1)); minorPythonVersion = Integer.parseInt(matcher.group(2)); microPythonVersion = Integer.parseInt(matcher.group(3)); } if (majorPythonVersion == 2 && minorPythonVersion >= 6) { pythonExecutionSuccessful = true; } else if (majorPythonVersion == 3) { pythonExecutionSuccessful = true; } } public boolean isPythonExecutionSuccessful() { return pythonExecutionSuccessful; } public int getMajorPythonVersion() { return this.majorPythonVersion; } public int getMinorPythonVersion() { return this.minorPythonVersion; } public int getMicroPythonVersion() { return this.microPythonVersion; } } }