/******************************************************************************* * (c) Copyright 2016 Hewlett-Packard Development Company, L.P. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Apache License v2.0 which accompany this distribution. * * The Apache License is available at * http://www.apache.org/licenses/LICENSE-2.0 * *******************************************************************************/ package io.cloudslang.lang.compiler.validator; import io.cloudslang.lang.compiler.Extension; import io.cloudslang.lang.compiler.SlangTextualKeys; import io.cloudslang.lang.compiler.modeller.TransformersHandler; import io.cloudslang.lang.compiler.modeller.model.Flow; import io.cloudslang.lang.compiler.modeller.model.Step; import io.cloudslang.lang.compiler.modeller.result.ExecutableModellingResult; import io.cloudslang.lang.compiler.modeller.transformers.InOutTransformer; import io.cloudslang.lang.compiler.modeller.transformers.Transformer; import io.cloudslang.lang.compiler.parser.model.ParsedSlang; import io.cloudslang.lang.entities.bindings.Argument; import io.cloudslang.lang.entities.bindings.InOutParam; import io.cloudslang.lang.entities.bindings.Input; import io.cloudslang.lang.entities.bindings.Output; import io.cloudslang.lang.entities.bindings.Result; import io.cloudslang.lang.entities.utils.ResultUtils; import io.cloudslang.lang.entities.utils.SetUtils; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import static ch.lambdaj.Lambda.exists; import static io.cloudslang.lang.compiler.SlangTextualKeys.ON_FAILURE_KEY; import static org.hamcrest.Matchers.equalToIgnoringCase; public class PreCompileValidatorImpl extends AbstractValidator implements PreCompileValidator { private ExecutableValidator executableValidator; private static final String MULTIPLE_ON_FAILURE_MESSAGE_SUFFIX = "Multiple 'on_failure' steps found"; private static final String ON_FAILURE_LAST_STEP_MESSAGE_SUFFIX = "'on_failure' should be last step in the workflow"; public static final String FLOW_RESULTS_WITH_EXPRESSIONS_MESSAGE = "Explicit values are not allowed for flow results. Correct format is:"; @Override public String validateExecutableRawData(ParsedSlang parsedSlang, Map<String, Object> executableRawData, List<RuntimeException> errors) { if (executableRawData == null) { errors.add(new IllegalArgumentException("Error compiling " + parsedSlang.getName() + ". Executable data is null")); return ""; } else { String executableName = getExecutableName(executableRawData, errors); if (parsedSlang == null) { errors.add(new IllegalArgumentException("Slang source for: \'" + executableName + "\' is null")); } else { if (executableRawData.size() == 0) { errors.add(new IllegalArgumentException("Error compiling " + parsedSlang.getName() + ". Executable data for: \'" + executableName + "\' is empty")); } } return executableName; } } @Override public List<Map<String, Map<String, Object>>> validateWorkflowRawData(ParsedSlang parsedSlang, Object workflowRawData, String executableName, List<RuntimeException> errors) { if (workflowRawData == null) { workflowRawData = new ArrayList<>(); errors.add(new RuntimeException("Error compiling " + parsedSlang.getName() + ". Flow: " + executableName + " has no workflow property")); } List<Map<String, Map<String, Object>>> workFlowRawData; try { //noinspection unchecked workFlowRawData = (List<Map<String, Map<String, Object>>>) workflowRawData; } catch (ClassCastException ex) { workFlowRawData = new ArrayList<>(); errors.add(new RuntimeException("Flow: '" + executableName + "' syntax is illegal.\nBelow 'workflow' property there should be a list of steps and not a map")); } if (CollectionUtils.isEmpty(workFlowRawData)) { errors.add(new RuntimeException("Error compiling source '" + parsedSlang.getName() + "'. Flow: '" + executableName + "' has no workflow data")); } for (Map<String, Map<String, Object>> step : workFlowRawData) { if (step.size() > 1) { errors.add(new RuntimeException("Error compiling source '" + parsedSlang.getName() + "'.\nFlow: '" + executableName + "' has steps with keyword on the same indentation as the step name " + "or there is no space between step name and hyphen.")); } } return workFlowRawData; } @Override public ExecutableModellingResult validateResult(ParsedSlang parsedSlang, String executableName, ExecutableModellingResult result) { validateFileName(executableName, parsedSlang, result); validateInputNamesDifferentFromOutputNames(result); if (SlangTextualKeys.FLOW_TYPE.equals(result.getExecutable().getType())) { validateFlow((Flow) result.getExecutable(), result); } return result; } @Override public List<RuntimeException> checkKeyWords( String dataLogicalName, String parentProperty, Map<String, Object> rawData, List<Transformer> allRelevantTransformers, List<String> additionalValidKeyWords, List<List<String>> constraintGroups) { Set<String> validKeywords = new HashSet<>(); List<RuntimeException> errors = new ArrayList<>(); if (additionalValidKeyWords != null) { validKeywords.addAll(additionalValidKeyWords); } for (Transformer transformer : allRelevantTransformers) { validKeywords.add(TransformersHandler.keyToTransform(transformer)); } Set<String> rawDataKeySet = rawData.keySet(); for (String key : rawDataKeySet) { if (!(exists(validKeywords, equalToIgnoringCase(key)))) { String additionalParentPropertyMessage = StringUtils.isEmpty(parentProperty) ? "" : " under \'" + parentProperty + "\'"; errors.add(new RuntimeException("Artifact {" + dataLogicalName + "} has unrecognized tag {" + key + "}" + additionalParentPropertyMessage + ". Please take a look at the supported features per versions link")); } } if (constraintGroups != null) { for (List<String> group : constraintGroups) { String lastKeyFound = null; for (String key : group) { if (rawDataKeySet.contains(key)) { if (lastKeyFound != null) { // one key from this group was already found in action data errors.add(new RuntimeException("Conflicting keys[" + lastKeyFound + ", " + key + "] at: " + dataLogicalName)); } else { lastKeyFound = key; } } } } } return errors; } @Override public Map<String, Map<String, Object>> validateOnFailurePosition( List<Map<String, Map<String, Object>>> workFlowRawData, String execName, List<RuntimeException> errors) { Iterator<Map<String, Map<String, Object>>> stepsIterator = workFlowRawData.iterator(); int onFailureCount = 0; String latestStepName = null; List<RuntimeException> onFailureErrors = new ArrayList<>(); Map<String, Map<String, Object>> onFailureData = null; while (stepsIterator.hasNext()) { Map<String, Map<String, Object>> stepData = stepsIterator.next(); latestStepName = stepData.keySet().iterator().next(); if (latestStepName.equals(ON_FAILURE_KEY)) { if (onFailureCount == 1) { onFailureErrors.add(new RuntimeException("Flow: '" + execName + "' syntax is illegal.\n" + MULTIPLE_ON_FAILURE_MESSAGE_SUFFIX)); } ++onFailureCount; onFailureData = stepData; stepsIterator.remove(); } } // exactly one on_failure -> need to be last step if (onFailureCount == 1) { if (!ON_FAILURE_KEY.equals(latestStepName)) { onFailureErrors.add(new RuntimeException("Flow: '" + execName + "' syntax is illegal.\n" + ON_FAILURE_LAST_STEP_MESSAGE_SUFFIX)); } } if (CollectionUtils.isEmpty(onFailureErrors)) { return onFailureData; } else { errors.addAll(onFailureErrors); return null; } } @Override public void validateDecisionResultsSection( Map<String, Object> executableRawData, String artifact, List<RuntimeException> errors) { Object resultsValue = executableRawData.get(SlangTextualKeys.RESULTS_KEY); if (resultsValue == null || (resultsValue instanceof List && ((List) resultsValue).isEmpty())) { errors.add( new RuntimeException( "Artifact {" + artifact + "} syntax is invalid: " + "'" + SlangTextualKeys.RESULTS_KEY + "' section cannot be empty for executable type '" + ParsedSlang.Type.DECISION.key() + "'" ) ); } } @Override public List<RuntimeException> validateNoDuplicateInOutParams(List<? extends InOutParam> inputs, InOutParam element) { List<RuntimeException> errors = new ArrayList<>(); Collection<InOutParam> inOutParams = new ArrayList<>(); inOutParams.addAll(inputs); String message = "Duplicate " + getMessagePart(element.getClass()) + " found: " + element.getName(); validateNotDuplicateInOutParam(inOutParams, element, message, errors); return errors; } @Override public void validateStringValue(String name, Serializable value, InOutTransformer transformer) { String prefix = StringUtils.capitalize(getMessagePart(transformer.getTransformedObjectsClass())) + ": '" + name; validateStringValue(prefix, value); } public static void validateStringValue(String errorMessagePrefix, Serializable value) { if (value != null && !(value instanceof String)) { throw new RuntimeException(errorMessagePrefix + "' should have a String value, but got value '" + value + "' of type " + value.getClass().getSimpleName() + "."); } } @Override public void validateResultsHaveNoExpression(List<Result> results, String artifactName, List<RuntimeException> errors) { for (Result result : results) { if (result.getValue() != null) { errors.add( new RuntimeException( "Flow: '" + artifactName + "' syntax is illegal. Error compiling result: '" + result.getName() + "'. " + FLOW_RESULTS_WITH_EXPRESSIONS_MESSAGE + " '- " + result.getName() + "'." ) ); } } } @Override public void validateResultTypes(List<Result> results, String artifactName, List<RuntimeException> errors) { for (Result result : results) { if (!(result.getValue() == null) && !(result.getValue().get() == null)) { Serializable value = result.getValue().get(); if (!(value instanceof String || Boolean.TRUE.equals(value))) { errors.add( new RuntimeException("Flow: '" + artifactName + "' syntax is illegal. Error compiling result: '" + result.getName() + "'. Value supports only expression or boolean true values." ) ); } } } } @Override public void validateDefaultResult(List<Result> results, String artifactName, List<RuntimeException> errors) { for (int i = 0; i < results.size() - 1; i++) { Result currentResult = results.get(i); if (ResultUtils.isDefaultResult(currentResult)) { errors.add(new RuntimeException( "Flow: '" + artifactName + "' syntax is illegal. Error compiling result: '" + currentResult.getName() + "'. Default result should be on last position." )); } } if (results.size() > 0) { Result lastResult = results.get(results.size() - 1); if (!ResultUtils.isDefaultResult(lastResult)) { errors.add(new RuntimeException( "Flow: '" + artifactName + "' syntax is illegal. Error compiling result: '" + lastResult.getName() + "'. Last result should be default result." )); } } } private String getMessagePart(Class aClass) { String messagePart = ""; if (aClass.equals(Input.class)) { messagePart = "input"; } else if (aClass.equals(Argument.class)) { messagePart = "step input"; } else if (aClass.equals(Output.class)) { messagePart = "output / publish value"; } else if (aClass.equals(Result.class)) { messagePart = "result"; } return messagePart; } private String getExecutableName(Map<String, Object> executableRawData, List<RuntimeException> errors) { String execName = (String) executableRawData.get(SlangTextualKeys.EXECUTABLE_NAME_KEY); if (StringUtils.isBlank(execName)) { errors.add(new RuntimeException("Executable has no name")); } try { executableValidator.validateExecutableName(execName); } catch (RuntimeException rex) { errors.add(rex); } return execName; } private void validateFlow(Flow compiledFlow, ExecutableModellingResult result) { if (CollectionUtils.isEmpty(compiledFlow.getWorkflow().getSteps())) { result.getErrors().add(new RuntimeException("Flow: " + compiledFlow.getName() + " has no steps")); } else { List<RuntimeException> errors = result.getErrors(); Set<String> reachableStepNames = new HashSet<>(); Set<String> reachableResultNames = new HashSet<>(); List<String> resultNames = getResultNames(compiledFlow); Deque<Step> steps = compiledFlow.getWorkflow().getSteps(); validateNavigation( compiledFlow.getWorkflow().getSteps().getFirst(), steps, resultNames, reachableStepNames, reachableResultNames, errors ); validateStepsAreReachable(reachableStepNames, steps, errors); validateResultsAreReachable(reachableResultNames, resultNames, errors); } } private void validateNavigation( Step currentStep, Deque<Step> steps, List<String> resultNames, Set<String> reachableStepNames, Set<String> reachableResultNames, List<RuntimeException> errors) { validateNavigation( currentStep, steps, resultNames, reachableStepNames, reachableResultNames, errors, new ArrayList<String>() ); } private void validateNavigation( Step currentStep, Deque<Step> steps, List<String> resultNames, Set<String> reachableStepNames, Set<String> reachableResultNames, List<RuntimeException> errors, List<String> stepResultCollisionNames) { String currentStepName = currentStep.getName(); reachableStepNames.add(currentStepName); for (Map<String, String> map : currentStep.getNavigationStrings()) { Map.Entry<String, String> entry = map.entrySet().iterator().next(); String navigationTarget = entry.getValue(); boolean isResult = resultNames.contains(navigationTarget); Step nextStepToCompile = selectNextStepToCompile(steps, navigationTarget); boolean isStep = nextStepToCompile != null; if (isStep && isResult && !stepResultCollisionNames.contains(navigationTarget)) { stepResultCollisionNames.add(navigationTarget); errors.add( new RuntimeException( "Navigation target: '" + navigationTarget + "' is declared both as step name and flow result." ) ); } if (isResult) { reachableResultNames.add(navigationTarget); } if (!isProcessed(navigationTarget, isStep, reachableStepNames, reachableResultNames)) { if (isStep) { validateNavigation( nextStepToCompile, steps, resultNames, reachableStepNames, reachableResultNames, errors, stepResultCollisionNames ); } else if (!isResult) { errors.add( new RuntimeException( "Failed to compile step: " + currentStepName + ". The step/result name: " + entry.getValue() + " of navigation: " + entry.getKey() + " -> " + entry.getValue() + " is missing" ) ); } } } } private Step selectNextStepToCompile(Deque<Step> steps, String navigationTarget) { for (Step step : steps) { if (org.apache.commons.lang.StringUtils.equals(step.getName(), navigationTarget)) { return step; } } return null; } private boolean isProcessed( String navigationTarget, boolean pointsToStep, Set<String> reachableStepNames, Set<String> reachableResultNames) { if (pointsToStep) { return reachableStepNames.contains(navigationTarget); } else { return reachableResultNames.contains(navigationTarget); } } private void validateStepsAreReachable( Set<String> reachableStepNames, Deque<Step> steps, List<RuntimeException> errors) { for (Step step : steps) { String stepName = step.getName(); String messagePrefix = step.isOnFailureStep() ? "on_failure step '" : "Step '"; if (!reachableStepNames.contains(stepName)) { errors.add(new RuntimeException(messagePrefix + stepName + "' is unreachable.")); } } } private void validateResultsAreReachable( Set<String> reachableResultNames, List<String> resultNames, List<RuntimeException> errors) { Set<String> unreachableResultNames = new HashSet<>(resultNames); unreachableResultNames.removeAll(reachableResultNames); if (!unreachableResultNames.isEmpty()) { errors.add(new RuntimeException("The following results are not wired: " + unreachableResultNames + ".")); } } private void validateFileName(String executableName, ParsedSlang parsedSlang, ExecutableModellingResult result) { String fileName = parsedSlang.getName(); Extension fileExtension = parsedSlang.getFileExtension(); String fileNameWithoutExtension = Extension.removeExtension(fileName); if (StringUtils.isNotEmpty(executableName) && !executableName.equals(fileNameWithoutExtension)) { if (fileExtension == null) { result.getErrors().add(new IllegalArgumentException("Operation/Flow " + executableName + " is declared in a file named \"" + fileNameWithoutExtension + "\"," + " it should be declared in a file named \"" + executableName + "\" plus a valid " + "extension(" + Extension.getExtensionValuesAsString() + ") separated by \".\"")); } else { result.getErrors().add(new IllegalArgumentException("Operation/Flow " + executableName + " is declared in a file named \"" + fileName + "\"" + ", it should be declared in a file named \"" + executableName + "." + fileExtension.getValue() + "\"")); } } } private void validateInputNamesDifferentFromOutputNames(ExecutableModellingResult result) { List<Input> inputs = result.getExecutable().getInputs(); List<Output> outputs = result.getExecutable().getOutputs(); String errorMessage = "Inputs and outputs names should be different for \"" + result.getExecutable().getId() + "\". " + "Please rename input/output \"" + NAME_PLACEHOLDER + "\""; try { validateListsHaveMutuallyExclusiveNames(inputs, outputs, errorMessage); } catch (RuntimeException e) { result.getErrors().add(e); } } private void validateNotDuplicateInOutParam(Collection<InOutParam> inOutParams, InOutParam element, String message, List<RuntimeException> errors) { if (SetUtils.containsIgnoreCaseBasedOnName(inOutParams, element)) { errors.add(new RuntimeException(message)); } else { inOutParams.add(element); } } public void setExecutableValidator(ExecutableValidator executableValidator) { this.executableValidator = executableValidator; } }