io.cloudslang.lang.compiler.validator.PreCompileValidatorImpl.java Source code

Java tutorial

Introduction

Here is the source code for io.cloudslang.lang.compiler.validator.PreCompileValidatorImpl.java

Source

/*******************************************************************************
 * (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 org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

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 static ch.lambdaj.Lambda.exists;
import static io.cloudslang.lang.compiler.SlangTextualKeys.ON_FAILURE_KEY;
import static io.cloudslang.lang.compiler.SlangTextualKeys.SEQ_STEPS_KEY;
import static java.util.stream.Collectors.toSet;
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:";
    public static final String FLOW_RESULTS_NOT_ALLOWED_EXPRESSIONS_MESSAGE = "Valid results are:";

    @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 List<Map<String, Map<String, String>>> validateSeqActionSteps(Object oSeqActionStepsRawData,
            List<RuntimeException> errors) {
        if (oSeqActionStepsRawData == null) {
            oSeqActionStepsRawData = new ArrayList<>();
            errors.add(new RuntimeException(
                    "Error compiling sequential operation: missing '" + SEQ_STEPS_KEY + "' property."));
        }
        List<Map<String, Map<String, String>>> stepsRawData;
        try {
            //noinspection unchecked
            stepsRawData = (List<Map<String, Map<String, String>>>) oSeqActionStepsRawData;
        } catch (ClassCastException ex) {
            stepsRawData = new ArrayList<>();
            errors.add(new RuntimeException("Error compiling sequential operation: syntax is illegal.\n" + "Below '"
                    + SEQ_STEPS_KEY + "' property there should be a list of steps and not a map."));
        }
        if (CollectionUtils.isEmpty(stepsRawData)) {
            errors.add(new RuntimeException(
                    "Error compiling sequential operation: missing '" + SEQ_STEPS_KEY + "' data."));
        }
        for (Map<String, Map<String, String>> step : stepsRawData) {
            if (step.size() > 1) {
                errors.add(new RuntimeException("Error compiling sequential operation: syntax is illegal.\n"
                        + "Found steps with keyword on the same indentation as the step name "
                        + "or there is no space between step name and hyphen."));
            }
        }

        return stepsRawData;
    }

    @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);
        }
        //todo this should be changed, we shouldn't rely on the names of the class
        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 validateResultsWithWhitelist(List<Result> results, List<String> allowedResults, String artifactName,
            List<RuntimeException> errors) {

        final Set<String> artifactResultNames = results.stream().map(Result::getName).collect(toSet());
        if ((artifactResultNames.size() != results.size())
                || !artifactResultNames.equals(new HashSet<>(allowedResults))) {
            errors.add(new RuntimeException("Sequential operation: '" + artifactName + "' syntax is illegal. "
                    + FLOW_RESULTS_NOT_ALLOWED_EXPRESSIONS_MESSAGE + allowedResults.toString() + "."));
        }
    }

    @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 {
            Set<String> reachableStepNames = new HashSet<>();
            Set<String> reachableResultNames = new HashSet<>();
            List<String> resultNames = getResultNames(compiledFlow);
            Deque<Step> steps = compiledFlow.getWorkflow().getSteps();

            List<RuntimeException> validationErrors = new ArrayList<>();

            validateNavigation(compiledFlow.getWorkflow().getSteps().getFirst(), steps, resultNames,
                    reachableStepNames, reachableResultNames, validationErrors);
            validateStepsAreReachable(reachableStepNames, steps, validationErrors);
            validateResultsAreReachable(reachableResultNames, resultNames, validationErrors);

            // convert all the errors for this flow into one which indicates the flow as well
            if (CollectionUtils.isNotEmpty(validationErrors)) {
                StringBuilder stringBuilder = new StringBuilder();

                validationErrors.forEach((err) -> stringBuilder.append('\n').append(err.getMessage()));

                result.getErrors().add(new RuntimeException(
                        "Flow " + compiledFlow.getName() + " has errors:" + stringBuilder.toString()));
            }
        }
    }

    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<>());
    }

    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;
    }
}