All Downloads are FREE. Search and download functionalities are using the official Maven repository.

step.artefacts.handlers.CallFunctionHandler Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (C) 2020, exense GmbH
 *  
 * This file is part of STEP
 *  
 * STEP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * STEP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *  
 * You should have received a copy of the GNU Affero General Public License
 * along with STEP.  If not, see .
 ******************************************************************************/
package step.artefacts.handlers;

import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import jakarta.json.JsonValue.ValueType;
import jakarta.json.stream.JsonParsingException;
import step.artefacts.CallFunction;
import step.artefacts.handlers.FunctionGroupHandler.FunctionGroupContext;
import step.artefacts.handlers.functions.FunctionGroupSession;
import step.artefacts.handlers.functions.TokenSelectionCriteriaMapBuilder;
import step.artefacts.reports.CallFunctionReportNode;
import step.attachments.AttachmentMeta;
import step.common.managedoperations.OperationManager;
import step.core.accessors.AbstractOrganizableObject;
import step.core.artefacts.AbstractArtefact;
import step.core.artefacts.handlers.ArtefactHandler;
import step.core.artefacts.reports.ReportNode;
import step.core.artefacts.reports.ReportNodeStatus;
import step.core.dynamicbeans.DynamicJsonObjectResolver;
import step.core.dynamicbeans.DynamicJsonValueResolver;
import step.core.execution.ExecutionContext;
import step.core.execution.ExecutionContextBindings;
import step.core.execution.ExecutionContextWrapper;
import step.core.execution.OperationMode;
import step.core.json.JsonProviderCache;
import step.core.miscellaneous.ReportNodeAttachmentManager;
import step.core.miscellaneous.ReportNodeAttachmentManager.AttachmentQuotaException;
import step.core.plans.Plan;
import step.core.plugins.ExecutionCallbacks;
import step.core.reports.Error;
import step.core.reports.ErrorType;
import step.core.variables.VariablesManager;
import step.datapool.DataSetHandle;
import step.functions.Function;
import step.functions.accessor.FunctionAccessor;
import step.functions.execution.FunctionExecutionService;
import step.functions.execution.FunctionExecutionServiceException;
import step.functions.handler.AbstractFunctionHandler;
import step.functions.io.FunctionInput;
import step.functions.io.Output;
import step.functions.type.FunctionTypeRegistry;
import step.grid.Token;
import step.grid.TokenWrapper;
import step.grid.agent.tokenpool.TokenReservationSession;
import step.grid.io.Attachment;
import step.grid.io.AttachmentHelper;
import step.grid.tokenpool.Interest;
import step.plugins.functions.types.CompositeFunction;

import java.io.StringReader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import static step.artefacts.handlers.functions.TokenForecastingExecutionPlugin.getTokenForecastingContext;
import static step.core.agents.provisioning.AgentPoolConstants.TOKEN_ATTRIBUTE_PARTITION;

public class CallFunctionHandler extends ArtefactHandler {

	private static final String KEYWORD_OUTPUT_LEGACY_FORMAT = "keywords.output.legacy";
	public static final String OPERATION_KEYWORD_CALL = "Keyword Call";

	protected FunctionExecutionService functionExecutionService;
	protected FunctionAccessor functionAccessor;
	
	protected ReportNodeAttachmentManager reportNodeAttachmentManager;
	protected DynamicJsonObjectResolver dynamicJsonObjectResolver;

	private TokenSelectionCriteriaMapBuilder tokenSelectionCriteriaMapBuilder;
	protected FunctionLocator functionLocator;

	protected boolean useLegacyOutput;
	
	@Override
	public void init(ExecutionContext context) {
		super.init(context);
		FunctionTypeRegistry functionTypeRegistry = context.require(FunctionTypeRegistry.class);
		functionAccessor = context.require(FunctionAccessor.class);
		functionExecutionService = context.require(FunctionExecutionService.class);
		reportNodeAttachmentManager = new ReportNodeAttachmentManager(context);
		dynamicJsonObjectResolver = new DynamicJsonObjectResolver(new DynamicJsonValueResolver(context.getExpressionHandler()));
		this.tokenSelectionCriteriaMapBuilder = new TokenSelectionCriteriaMapBuilder(functionTypeRegistry, dynamicJsonObjectResolver);
		this.functionLocator = new FunctionLocator(functionAccessor, new SelectorHelper(dynamicJsonObjectResolver));
		this.useLegacyOutput = context.getConfiguration().getPropertyAsBoolean(KEYWORD_OUTPUT_LEGACY_FORMAT, false);
	}

	@Override
	protected void createReportSkeleton_(CallFunctionReportNode parentNode, CallFunction testArtefact) {
		try {
			Function function = getFunction(testArtefact);
			if (function instanceof CompositeFunction) {
				Plan plan = ((CompositeFunction) function).getPlan();
				delegateCreateReportSkeleton(plan.getRoot(), parentNode);
			} else {
				FunctionGroupContext functionGroupContext = getFunctionGroupContext();
				boolean closeFunctionGroupSessionAfterExecution = (functionGroupContext == null);

				// Inject the mocked function execution service of the token forecasting context instead of the function execution service of the context
				FunctionExecutionService functionExecutionService = getTokenForecastingContext(context).getFunctionExecutionServiceForTokenForecasting();
				FunctionGroupSession functionGroupSession = getOrCreateFunctionGroupSession(functionExecutionService, functionGroupContext);
				try {
					// Do not force the local token selection in order to simulate a real token selection
					selectToken(parentNode, testArtefact, function, functionGroupContext, functionGroupSession, false);
				} finally {
					if (closeFunctionGroupSessionAfterExecution) {
						functionGroupSession.close();
					}
				}
			}
		} catch (Exception e) {
			if (logger.isDebugEnabled()) {
				logger.debug("Ignoring error during skeleton phase", e);
			}
		}
	}

	@Override
	public AbstractArtefact resolveArtefactCall(CallFunction artefact) {
		Function function = getFunction(artefact);
		if(function instanceof CompositeFunction) {
			return ((CompositeFunction) function).getPlan().getRoot();
		} else {
			return null;
		}
	}

	@Override
	protected void execute_(CallFunctionReportNode node, CallFunction testArtefact) throws Exception {
		// Append the artefactId of the current artefact to the path
		pushArtefactPath(node, testArtefact);

		String argumentStr = testArtefact.getArgument().get();
		node.setInput(argumentStr);
		
		Function function = getFunction(testArtefact);
		
		ExecutionCallbacks executionCallbacks = context.getExecutionCallbacks();
		executionCallbacks.beforeFunctionExecution(context, node, function);
		
		node.setFunctionId(function.getId().toString());
		node.setFunctionAttributes(function.getAttributes());

		String name = node.getName();
		// Name the report node after the keyword if it's not already the case
		String functionName = function.getAttribute(AbstractOrganizableObject.NAME);
		if(name.equals(CallFunction.ARTEFACT_NAME) && functionName != null) {
			node.setName(functionName);
		}
		
		FunctionInput input = buildInput(argumentStr);
		node.setInput(input.getPayload().toString());
		
		validateInput(input, function);

		Output output;
		if(!context.isSimulation()) {
			FunctionGroupContext functionGroupContext = getFunctionGroupContext();
			boolean closeFunctionGroupSessionAfterExecution = (functionGroupContext == null);
			FunctionGroupSession functionGroupSession = getOrCreateFunctionGroupSession(functionExecutionService, functionGroupContext);

			// Force local token selection for local plan executions
			boolean forceLocalToken =  context.getOperationMode() == OperationMode.LOCAL;
			TokenWrapper token = selectToken(node, testArtefact, function, functionGroupContext, functionGroupSession, forceLocalToken);

			try {
				Token gridToken = token.getToken();
				if(gridToken.isLocal()) {
					TokenReservationSession session = (TokenReservationSession) gridToken.getAttachedObject(TokenWrapper.TOKEN_RESERVATION_SESSION);
					session.put(AbstractFunctionHandler.EXECUTION_CONTEXT_KEY, new ExecutionContextWrapper(context));
					session.put(AbstractFunctionHandler.ARTEFACT_PATH, currentArtefactPath());
				}
				
				node.setAgentUrl(token.getAgent().getAgentUrl());
				node.setTokenId(token.getID());
				
				OperationManager.getInstance().enter(OPERATION_KEYWORD_CALL, new Object[]{function.getAttributes(), token.getToken(), token.getAgent()},
						node.getId().toString());

				try {
					output = functionExecutionService.callFunction(token.getID(), function, input, JsonObject.class, context);
				} finally {
					OperationManager.getInstance().exit();
				}
				executionCallbacks.afterFunctionExecution(context, node, function, output);
				
				Error error = output.getError();
				if(error!=null) {
					node.setError(error);
					node.setStatus(error.getType()==ErrorType.TECHNICAL?ReportNodeStatus.TECHNICAL_ERROR:ReportNodeStatus.FAILED);
				} else {
					node.setStatus(ReportNodeStatus.PASSED);
				}
	
				if(output.getPayload() != null) {
					Object outputPayload = (useLegacyOutput) ? output.getPayload() : new UserFriendlyJsonObject(output.getPayload());
					context.getVariablesManager().putVariable(node, "output", outputPayload);
					node.setOutput(output.getPayload().toString());
					node.setOutputObject(output.getPayload());
					ReportNode parentNode = context.getReportNodeCache().get(node.getParentID());
					if(parentNode!=null) {
						context.getVariablesManager().putVariable(parentNode, "previous", outputPayload);
					}
				}
				
				if(output.getAttachments()!=null) {
					for(Attachment a:output.getAttachments()) {
						AttachmentMeta attachmentMeta;
						try {
							attachmentMeta = reportNodeAttachmentManager.createAttachment(AttachmentHelper.hexStringToByteArray(a.getHexContent()), a.getName());
							node.addAttachment(attachmentMeta);					
						} catch (AttachmentQuotaException e) {
							// attachment has been skipped. Nothing else to do here
						}
					}
				}
				if(output.getMeasures()!=null) {
					node.setMeasures(output.getMeasures());
				}
				
				String drainOutputValue = testArtefact.getResultMap().get();
				drainOutput(drainOutputValue, output);
			} finally {
				if(closeFunctionGroupSessionAfterExecution) {
					functionGroupSession.releaseTokens(true);
				}
	
				callChildrenArtefacts(node, testArtefact);
			}
		} else {
			output = new Output<>();
			output.setPayload(JsonProviderCache.createObjectBuilder().build());
			node.setOutputObject(output.getPayload());
			node.setOutput(output.getPayload().toString());
			node.setStatus(ReportNodeStatus.PASSED);
		}
	}

	private FunctionGroupContext getFunctionGroupContext() {
		return (FunctionGroupContext) context.getVariablesManager().getVariable(FunctionGroupHandler.FUNCTION_GROUP_CONTEXT_KEY);
	}

	private FunctionGroupSession getOrCreateFunctionGroupSession(FunctionExecutionService functionExecutionService, FunctionGroupContext functionGroupContext) {
		FunctionGroupSession functionGroupSession;
		if (functionGroupContext == null) {
			functionGroupSession = new FunctionGroupSession(functionExecutionService);
		} else {
			functionGroupSession = functionGroupContext.getSession();
		}
		return functionGroupSession;
	}

	private TokenWrapper selectToken(CallFunctionReportNode node, CallFunction testArtefact, Function function, FunctionGroupContext functionGroupContext, FunctionGroupSession functionGroupSession, boolean localToken) throws FunctionExecutionServiceException {
		CallFunctionTokenWrapperOwner tokenWrapperOwner = new CallFunctionTokenWrapperOwner(node.getId().toString(), context.getExecutionId(), context.getExecutionParameters().getDescription());
		boolean localTokenRequired = localToken || tokenSelectionCriteriaMapBuilder.isLocalTokenRequired(testArtefact, function);
		TokenWrapper token;
		if(localTokenRequired) {
			token = functionGroupSession.getLocalToken();
		} else {
			Map tokenSelectionCriteria = tokenSelectionCriteriaMapBuilder.buildSelectionCriteriaMap(testArtefact, function, functionGroupContext, getBindings());
			token = functionGroupSession.getRemoteToken(getOwnAttributesForTokenSelection(), tokenSelectionCriteria, tokenWrapperOwner, (functionGroupContext != null));
		}
		return token;
	}

	/**
	 * @return the map of attributes that will be presented for the token selection to the agent token.
	 * These attributes will be used to match the right token if the agent token defines criteria for the selector (token pretender)
	 */
	private Map getOwnAttributesForTokenSelection() {
		String executionId = context.getExecutionId();
		return Map.of(TOKEN_ATTRIBUTE_PARTITION, executionId);
	}

	private void validateInput(FunctionInput input, Function function) {
		if(context.getConfiguration().getPropertyAsBoolean("enforceschemas", false)){
			JsonSchemaValidator.validate(function.getSchema().toString(), input.getPayload().toString());
		}
	}

	private Function getFunction(CallFunction testArtefact) {
		return functionLocator.getFunction(testArtefact, context.getObjectPredicate(),
				ExecutionContextBindings.get(context));
	}

	@SuppressWarnings("unchecked")
	private void drainOutput(String drainOutputValue, Output output) {
		if(drainOutputValue!=null&&drainOutputValue.trim().length()>0) {
			JsonObject resultJson = output.getPayload();
			if(resultJson!=null) {
				Object var = context.getVariablesManager().getVariable(drainOutputValue);
				if(var instanceof Map) {
					Map resultMap = jsonToMap(resultJson);
					((Map)var).putAll(resultMap);
				} else if(var instanceof DataSetHandle) {
					DataSetHandle dataSetHandle = (DataSetHandle) var;
					Map resultMap = jsonToMap(resultJson);
					for(String key:resultJson.keySet()) {
						JsonValue jsonValue = resultJson.get(key);
						if(jsonValue instanceof JsonArray) {
							JsonArray array = (JsonArray) jsonValue;
							array.forEach(value-> {
								if(value.getValueType().equals(ValueType.OBJECT)) {
									Map rowAsMap = jsonToMap((JsonObject) value);
									dataSetHandle.addRow(rowAsMap);
								}
							});
						}
					}
					if(!resultMap.isEmpty()) {
						dataSetHandle.addRow(resultMap);						
					}
				} else {
					throw new RuntimeException("The variable '"+drainOutputValue+"' is neither a Map nor a DataSet handle");
				}					
			}
		}
	}

	private Map jsonToMap(JsonObject jsonOutput) {
		Map resultMap = new LinkedHashMap<>();
		for(String key:jsonOutput.keySet()) {
			JsonValue value = jsonOutput.get(key);
			if(value.getValueType() == ValueType.STRING) {
				resultMap.put(key, jsonOutput.getString(key));
			} else if (!value.getValueType().equals(ValueType.OBJECT)&&!value.getValueType().equals(ValueType.ARRAY)) {
				resultMap.put(key, jsonOutput.getString(key).toString());
			}
		}
		return resultMap;
	}

	protected void callChildrenArtefacts(CallFunctionReportNode node, CallFunction testArtefact) {
		if(testArtefact.getChildren()!=null&&testArtefact.getChildren().size()>0) {
			VariablesManager variableManager = context.getVariablesManager();
			variableManager.putVariable(node, "callReport", node);
			
//			node.getOutputObject().forEach((k,v)->{
//				variableManager.putVariable(node, k, v.toString());
//			});
			
			SequentialArtefactScheduler scheduler = new SequentialArtefactScheduler(context);
			scheduler.execute_(node, testArtefact, true);				
		}
	}
	
	private FunctionInput buildInput(String argumentStr) {
		JsonObject argument = parseAndResolveJson(argumentStr);
		
		Map properties = new HashMap<>();
		context.getVariablesManager().getAllVariables().forEach((key,value)->properties.put(key, value!=null?value.toString():""));
		properties.put(AbstractFunctionHandler.PARENTREPORTID_KEY, context.getCurrentReportNode().getId().toString());
		
		FunctionInput input = new FunctionInput<>();
		input.setPayload(argument);
		input.setProperties(properties);
		return input;
	}

	private JsonObject parseAndResolveJson(String functionStr) {
		JsonObject query;
		try {
			if(functionStr!=null&&functionStr.trim().length()>0) {
				query = JsonProviderCache.createReader(new StringReader(functionStr)).readObject();
			} else {
				query = JsonProviderCache.createObjectBuilder().build();
			}
		} catch(JsonParsingException e) {
			throw new RuntimeException("Error while parsing argument (input): string was '"+functionStr+"'",e);
		}
		return dynamicJsonObjectResolver.evaluate(query, getBindings());
	}


	@Override
	public CallFunctionReportNode createReportNode_(ReportNode parentNode, CallFunction testArtefact) {
		return new CallFunctionReportNode();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy