/*******************************************************************************
* (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.SlangSource;
import io.cloudslang.lang.compiler.SlangTextualKeys;
import io.cloudslang.lang.compiler.modeller.model.Executable;
import io.cloudslang.lang.compiler.modeller.model.Flow;
import io.cloudslang.lang.compiler.modeller.model.Step;
import io.cloudslang.lang.entities.ScoreLangConstants;
import io.cloudslang.lang.entities.bindings.Argument;
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.ArgumentUtils;
import io.cloudslang.lang.entities.utils.InputUtils;
import io.cloudslang.lang.entities.utils.ListUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.python.google.common.collect.Lists;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CompileValidatorImpl extends AbstractValidator implements CompileValidator {
public static final String DUPLICATE_EXECUTABLE_FOUND = "Duplicate executable found: '%s'";
@Override
public List<RuntimeException> validateModelWithDependencies(
Executable executable,
Map<String, Executable> filteredDependencies) {
Map<String, Executable> dependencies = new HashMap<>(filteredDependencies);
dependencies.put(executable.getId(), executable);
Set<Executable> verifiedExecutables = new HashSet<>();
return validateModelWithDependencies(executable, dependencies, verifiedExecutables,
new ArrayList<RuntimeException>(), true);
}
private List<RuntimeException> validateModelWithDependencies(
Executable executable,
Map<String, Executable> dependencies,
Set<Executable> verifiedExecutables,
List<RuntimeException> errors,
boolean recursive) {
//validate that all required & non private parameters with no default value of a reference are provided
if (!SlangTextualKeys.FLOW_TYPE.equals(executable.getType()) || verifiedExecutables.contains(executable)) {
return errors;
}
verifiedExecutables.add(executable);
Flow flow = (Flow) executable;
Collection<Step> steps = flow.getWorkflow().getSteps();
Set<Executable> flowReferences = new HashSet<>();
for (Step step : steps) {
Executable reference = dependencies.get(step.getRefId());
errors.addAll(validateStepAgainstItsDependency(flow, step, dependencies));
flowReferences.add(reference);
}
if (recursive) {
for (Executable reference : flowReferences) {
validateModelWithDependencies(reference, dependencies, verifiedExecutables, errors, true);
}
}
return errors;
}
@Override
public List<RuntimeException> validateModelWithDirectDependencies(Executable executable,
Map<String, Executable> directDependencies) {
List<RuntimeException> errors = new ArrayList<>();
Set<Executable> verifiedExecutables = new HashSet<>();
return validateModelWithDependencies(executable, directDependencies, verifiedExecutables, errors, false);
}
@Override
public List<RuntimeException> validateNoDuplicateExecutables(
Executable currentExecutable,
SlangSource currentSource,
Map<Executable, SlangSource> allAvailableExecutables) {
List<RuntimeException> errors = new ArrayList<>();
for (Map.Entry<Executable, SlangSource> entry : allAvailableExecutables.entrySet()) {
Executable executable = entry.getKey();
if (currentExecutable.getId().equalsIgnoreCase(executable.getId()) &&
!currentSource.equals(entry.getValue())) {
errors.add(new RuntimeException(String.format(DUPLICATE_EXECUTABLE_FOUND, currentExecutable.getId())));
}
}
return errors;
}
private List<RuntimeException> validateStepAgainstItsDependency(Flow flow, Step step,
Map<String, Executable> dependencies) {
List<RuntimeException> errors = new ArrayList<>();
String refId = step.getRefId();
Executable reference = dependencies.get(refId);
if (reference == null) {
throw new RuntimeException("Dependency " + step.getRefId() + " used by step: " + step.getName() +
" must be supplied for validation");
}
errors.addAll(validateMandatoryInputsAreWired(flow, step, reference));
errors.addAll(validateStepInputNamesDifferentFromDependencyOutputNames(flow, step, reference));
errors.addAll(validateNavigationSectionAgainstDependencyResults(flow, step, reference));
errors.addAll(validateBreakSection(flow, step, reference));
return errors;
}
private List<RuntimeException> validateBreakSection(Flow parentFlow, Step step, Executable reference) {
List<RuntimeException> errors = new ArrayList<>();
@SuppressWarnings("unchecked") // from BreakTransformer
List<String> breakValues = (List<String>) step.getPostStepActionData().get(SlangTextualKeys.BREAK_KEY);
if (isForLoop(step, breakValues)) {
List<String> referenceResultNames = getResultNames(reference);
Collection<String> nonExistingResults = ListUtils.subtract(breakValues, referenceResultNames);
if (CollectionUtils.isNotEmpty(nonExistingResults)) {
errors.add(new IllegalArgumentException("Cannot compile flow '" + parentFlow.getId() +
"' since in step '" + step.getName() + "' the results " +
nonExistingResults + " declared in '" + SlangTextualKeys.BREAK_KEY +
"' section are not declared in the dependency '" + reference.getId() + "' result section."));
}
}
return errors;
}
private boolean isForLoop(Step step, List<String> breakValuesList) {
Serializable forData = step.getPreStepActionData().get(SlangTextualKeys.FOR_KEY);
return (forData != null) && CollectionUtils.isNotEmpty(breakValuesList);
}
private List<RuntimeException> validateNavigationSectionAgainstDependencyResults(Flow flow, Step step,
Executable reference) {
List<RuntimeException> errors = new ArrayList<>();
String refId = step.getRefId();
if (reference == null) {
errors.add(new IllegalArgumentException(
getErrorMessagePrefix(flow, step) + " the dependency '" + refId + "' is missing."
));
} else {
if (!step.isOnFailureStep()) { // on_failure step cannot have navigation section
validateResultNamesAndNavigationSection(flow, step, refId, reference, errors);
}
}
return errors;
}
private void validateResultNamesAndNavigationSection(Flow flow, Step step, String refId,
Executable reference, List<RuntimeException> errors) {
List<String> stepNavigationKeys = getMapKeyList(step.getNavigationStrings());
List<String> refResults = mapResultsToNames(reference.getResults());
List<String> possibleResults;
possibleResults = getPossibleResults(step, refResults);
List<String> stepNavigationKeysWithoutMatchingResult = ListUtils.subtract(stepNavigationKeys, possibleResults);
List<String> refResultsWithoutMatchingNavigation = ListUtils.subtract(possibleResults, stepNavigationKeys);
if (CollectionUtils.isNotEmpty(refResultsWithoutMatchingNavigation)) {
if (step.isParallelLoop()) {
errors.add(new IllegalArgumentException(
getErrorMessagePrefix(flow, step) + " the parallel loop results " +
refResultsWithoutMatchingNavigation + " have no matching navigation."
));
} else {
errors.add(new IllegalArgumentException(
getErrorMessagePrefix(flow, step) + " the results " + refResultsWithoutMatchingNavigation +
" of its dependency '" + refId + "' have no matching navigation."
));
}
}
if (CollectionUtils.isNotEmpty(stepNavigationKeysWithoutMatchingResult)) {
if (step.isParallelLoop()) {
errors.add(new IllegalArgumentException(
getErrorMessagePrefix(flow, step) + " the navigation keys " +
stepNavigationKeysWithoutMatchingResult + " have no matching results." +
" The parallel loop depending on '" + refId +
"' can have the following results: " + possibleResults + "."
));
} else {
errors.add(new IllegalArgumentException(
getErrorMessagePrefix(flow, step) + " the navigation keys " +
stepNavigationKeysWithoutMatchingResult +
" have no matching results in its dependency '" +
refId + "'."
));
}
}
}
private List<String> getPossibleResults(Step step, List<String> refResults) {
List<String> possibleResults;
if (step.isParallelLoop()) {
possibleResults = Lists.newArrayList(ScoreLangConstants.SUCCESS_RESULT);
if (refResults.contains(ScoreLangConstants.FAILURE_RESULT)) {
possibleResults.add(ScoreLangConstants.FAILURE_RESULT);
}
} else {
possibleResults = refResults;
}
return possibleResults;
}
private String getErrorMessagePrefix(Flow flow, Step step) {
return "Cannot compile flow '" + flow.getName() +
"' since for step '" + step.getName() + "'";
}
private List<String> getMapKeyList(List<Map<String, String>> collection) {
List<String> result = new ArrayList<>();
for (Map<String, String> element : collection) {
result.add(element.keySet().iterator().next());
}
return result;
}
private List<String> mapResultsToNames(List<Result> results) {
List<String> resultNames = new ArrayList<>();
for (Result element : results) {
resultNames.add(element.getName());
}
return resultNames;
}
private List<RuntimeException> validateStepInputNamesDifferentFromDependencyOutputNames(Flow flow, Step step,
Executable reference) {
List<RuntimeException> errors = new ArrayList<>();
List<Argument> stepArguments = step.getArguments();
List<Output> outputs = reference.getOutputs();
String errorMessage = "Cannot compile flow '" + flow.getId() +
"'. Step '" + step.getName() +
"' has input '" + NAME_PLACEHOLDER +
"' with the same name as the one of the outputs of '" + reference.getId() + "'.";
try {
validateListsHaveMutuallyExclusiveNames(stepArguments, outputs, errorMessage);
} catch (RuntimeException e) {
errors.add(e);
}
return errors;
}
private List<String> getMandatoryInputNames(Executable executable) {
List<String> inputNames = new ArrayList<>();
for (Input input : executable.getInputs()) {
if (InputUtils.isMandatory(input)) {
inputNames.add(input.getName());
}
}
return inputNames;
}
private List<String> getStepInputNamesWithNonEmptyValue(Step step) {
List<String> inputNames = new ArrayList<>();
for (Argument argument : step.getArguments()) {
if (ArgumentUtils.isDefined(argument)) {
inputNames.add(argument.getName());
}
}
return inputNames;
}
private List<String> getInputsNotWired(List<String> mandatoryInputNames, List<String> stepInputNames) {
List<String> inputsNotWired = new ArrayList<>(mandatoryInputNames);
inputsNotWired.removeAll(stepInputNames);
return inputsNotWired;
}
private List<RuntimeException> validateMandatoryInputsAreWired(Flow flow, Step step, Executable reference) {
List<RuntimeException> errors = new ArrayList<>();
List<String> mandatoryInputNames = getMandatoryInputNames(reference);
List<String> stepInputNames = getStepInputNamesWithNonEmptyValue(step);
List<String> inputsNotWired = getInputsNotWired(mandatoryInputNames, stepInputNames);
if (!CollectionUtils.isEmpty(inputsNotWired)) {
errors.add(new IllegalArgumentException(
prepareErrorMessageValidateInputNamesEmpty(inputsNotWired, flow, step, reference))
);
}
return errors;
}
private String prepareErrorMessageValidateInputNamesEmpty(List<String> inputsNotWired, Flow flow,
Step step, Executable reference) {
StringBuilder inputsNotWiredBuilder = new StringBuilder();
for (String inputName : inputsNotWired) {
inputsNotWiredBuilder.append(inputName);
inputsNotWiredBuilder.append(", ");
}
String inputsNotWiredAsString = inputsNotWiredBuilder.toString();
if (StringUtils.isNotEmpty(inputsNotWiredAsString)) {
inputsNotWiredAsString = inputsNotWiredAsString.substring(0, inputsNotWiredAsString.length() - 2);
}
return "Cannot compile flow '" + flow.getId() +
"'. Step '" + step.getName() +
"' does not declare all the mandatory inputs of its reference." +
" The following inputs of '" + reference.getId() +
"' are not private, required and with no default value: " +
inputsNotWiredAsString + ".";
}
}