-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add "evaluateExpression" bulk action
Now we can evaluate arbitrary expressions from within bulk actions. Work in progress: 1. Only single return values are supported. 2. Expression profile determination is not part of this commit. Root authorization is therefore required for now. 3. Tests are quite rudimentary yet.
- Loading branch information
Showing
20 changed files
with
815 additions
and
276 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
...main/java/com/evolveum/midpoint/model/impl/scripting/actions/AbstractExecuteExecutor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
/* | ||
* Copyright (C) 2010-2023 Evolveum and contributors | ||
* | ||
* This work is dual-licensed under the Apache License 2.0 | ||
* and European Union Public License. See LICENSE file for details. | ||
*/ | ||
|
||
package com.evolveum.midpoint.model.impl.scripting.actions; | ||
|
||
import java.util.Collection; | ||
import java.util.function.Function; | ||
import javax.xml.namespace.QName; | ||
|
||
import org.apache.commons.lang3.StringUtils; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
import com.evolveum.midpoint.model.api.PipelineItem; | ||
import com.evolveum.midpoint.model.impl.lens.LensContext; | ||
import com.evolveum.midpoint.model.impl.scripting.ExecutionContext; | ||
import com.evolveum.midpoint.model.impl.scripting.PipelineData; | ||
import com.evolveum.midpoint.prism.*; | ||
import com.evolveum.midpoint.prism.xml.XmlTypeConverter; | ||
import com.evolveum.midpoint.schema.SchemaConstantsGenerated; | ||
import com.evolveum.midpoint.schema.constants.ExpressionConstants; | ||
import com.evolveum.midpoint.schema.expression.TypedValue; | ||
import com.evolveum.midpoint.schema.expression.VariablesMap; | ||
import com.evolveum.midpoint.schema.result.OperationResult; | ||
import com.evolveum.midpoint.util.QNameUtil; | ||
import com.evolveum.midpoint.util.exception.*; | ||
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; | ||
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.AbstractExecuteActionExpressionType; | ||
import com.evolveum.midpoint.xml.ns._public.model.scripting_3.ActionExpressionType; | ||
|
||
/** | ||
* Executes either `execute-script` or `evaluate-expression` actions. | ||
*/ | ||
abstract class AbstractExecuteExecutor<P extends AbstractExecuteExecutor.Parameters> | ||
extends BaseActionExecutor { | ||
|
||
private static final String PARAM_QUIET = "quiet"; // could be useful for other actions as well | ||
private static final String PARAM_OUTPUT_ITEM = "outputItem"; // item name or type (as URI!) -- EXPERIMENTAL | ||
private static final String PARAM_FOR_WHOLE_INPUT = "forWholeInput"; | ||
|
||
P getParameters( | ||
ActionExpressionType action, PipelineData input, ExecutionContext context, | ||
OperationResult globalResult, Function<Parameters, P> function) | ||
throws CommunicationException, ObjectNotFoundException, SchemaException, | ||
ScriptExecutionException, SecurityViolationException, ConfigurationException, ExpressionEvaluationException { | ||
|
||
ItemDefinition<?> outputDefinition; | ||
String outputItemUri = expressionHelper.getSingleArgumentValue( | ||
action.getParameter(), PARAM_OUTPUT_ITEM, false, false, | ||
getName(), input, context, String.class, globalResult); | ||
if (StringUtils.isNotBlank(outputItemUri)) { | ||
outputDefinition = getItemDefinition(outputItemUri); | ||
} else if (action instanceof AbstractExecuteActionExpressionType execute) { | ||
if (execute.getOutputItemName() != null) { | ||
outputDefinition = getItemDefinitionFromItemName(execute.getOutputItemName()); | ||
} else if (execute.getOutputTypeName() != null) { | ||
outputDefinition = getItemDefinitionFromTypeName(execute.getOutputTypeName()); | ||
} else { | ||
outputDefinition = null; | ||
} | ||
} else { | ||
outputDefinition = null; | ||
} | ||
boolean forWholeInput = expressionHelper.getActionArgument( | ||
Boolean.class, action, | ||
AbstractExecuteActionExpressionType.F_FOR_WHOLE_INPUT, PARAM_FOR_WHOLE_INPUT, | ||
input, context, false, PARAM_FOR_WHOLE_INPUT, globalResult); | ||
boolean quiet = expressionHelper.getActionArgument( | ||
Boolean.class, action, | ||
AbstractExecuteActionExpressionType.F_QUIET, PARAM_QUIET, | ||
input, context, false, PARAM_QUIET, globalResult); | ||
|
||
return function.apply(new Parameters(outputDefinition, forWholeInput, quiet)); | ||
} | ||
|
||
@NotNull abstract String getName(); | ||
|
||
private @NotNull ItemDefinition<?> getItemDefinition(String uri) throws ScriptExecutionException { | ||
QName name = QNameUtil.uriToQName(uri, true); | ||
ItemDefinition<?> byName = prismContext.getSchemaRegistry().findItemDefinitionByElementName(name); | ||
if (byName != null) { | ||
return byName; | ||
} | ||
|
||
ItemDefinition<?> byType = prismContext.getSchemaRegistry().findItemDefinitionByType(name); | ||
if (byType != null) { | ||
return byType; | ||
} | ||
|
||
throw new ScriptExecutionException( | ||
"Supplied item identification '" + uri + "' corresponds neither to item name nor type name"); | ||
} | ||
|
||
private @NotNull ItemDefinition<?> getItemDefinitionFromItemName(QName itemName) throws ScriptExecutionException { | ||
ItemDefinition<?> def = prismContext.getSchemaRegistry().findItemDefinitionByElementName(itemName); | ||
if (def != null) { | ||
return def; | ||
} | ||
throw new ScriptExecutionException("Item with name '" + itemName + "' couldn't be found."); | ||
} | ||
|
||
private @NotNull ItemDefinition<?> getItemDefinitionFromTypeName(QName typeName) throws ScriptExecutionException { | ||
ItemDefinition<?> byType = prismContext.getSchemaRegistry().findItemDefinitionByType(typeName); | ||
if (byType != null) { | ||
return byType; | ||
} | ||
|
||
if (XmlTypeConverter.canConvert(typeName)) { | ||
return prismContext.definitionFactory().createPropertyDefinition(SchemaConstantsGenerated.C_VALUE, typeName); | ||
} | ||
|
||
TypeDefinition typeDef = prismContext.getSchemaRegistry().findTypeDefinitionByType(typeName); | ||
if (typeDef instanceof SimpleTypeDefinition) { | ||
return prismContext.definitionFactory().createPropertyDefinition(SchemaConstantsGenerated.C_VALUE, typeName); | ||
} else if (typeDef instanceof ComplexTypeDefinition ctd) { | ||
if (ctd.isContainerMarker() || ctd.isObjectMarker()) { | ||
return prismContext.definitionFactory().createContainerDefinition(SchemaConstantsGenerated.C_VALUE, ctd); | ||
} else { | ||
return prismContext.definitionFactory().createPropertyDefinition(SchemaConstantsGenerated.C_VALUE, typeName); | ||
} | ||
} else if (typeDef != null) { | ||
throw new ScriptExecutionException("Type with name '" + typeName + "' couldn't be used as output type: " + typeDef); | ||
} else { | ||
throw new ScriptExecutionException("Type with name '" + typeName + "' couldn't be found."); | ||
} | ||
} | ||
|
||
|
||
@NotNull PipelineData executeInternal( | ||
PipelineData input, P parameters, ExecutionContext context, OperationResult globalResult) | ||
throws ScriptExecutionException { | ||
PipelineData output = PipelineData.createEmpty(); | ||
if (parameters.forWholeInput) { | ||
executeForWholeInput(input, output, parameters, context, globalResult); | ||
} else { | ||
iterateOverItems(input, context, globalResult, | ||
(value, item, result) -> | ||
processItem(context, parameters, output, item, value, result), | ||
(value, exception) -> | ||
context.println("Failed to execute script/expression on " | ||
+ getDescription(value) + exceptionSuffix(exception))); | ||
} | ||
return output; | ||
} | ||
|
||
private void executeForWholeInput( | ||
PipelineData input, PipelineData output, P parameters, ExecutionContext context, | ||
OperationResult globalResult) throws ScriptExecutionException { | ||
OperationResult result = operationsHelper.createActionResult(null, this, globalResult); | ||
context.checkTaskStop(); | ||
try { | ||
TypedValue<PipelineData> inputTypedValue = new TypedValue<>(input, PipelineData.class); | ||
Object outObject = doSingleExecution(parameters, inputTypedValue, context.getInitialVariables(), context, result); | ||
if (outObject != null) { | ||
addToData(outObject, PipelineData.newOperationResult(), output); | ||
} else { | ||
// no definition means we don't plan to provide any output - so let's just copy the input item to the output | ||
// (actually, null definition with non-null outObject should not occur) | ||
if (parameters.outputDefinition == null) { | ||
output.addAllFrom(input); | ||
} | ||
} | ||
if (!parameters.quiet) { | ||
context.println("Executed script/expression on the pipeline"); | ||
} | ||
|
||
} catch (Throwable ex) { | ||
Throwable exception = processActionException(ex, getName(), null, context); // TODO value for error reporting (3rd parameter) | ||
context.println("Failed to execute script/expression on the pipeline" + exceptionSuffix(exception)); | ||
} | ||
operationsHelper.trimAndCloneResult(result, null); | ||
} | ||
|
||
private void processItem( | ||
ExecutionContext context, P parameters, | ||
PipelineData output, PipelineItem item, PrismValue value, OperationResult result) | ||
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, CommunicationException, | ||
ConfigurationException, SecurityViolationException { | ||
|
||
// Hack. TODO: we need to add definitions to Pipeline items. | ||
@SuppressWarnings({ "unchecked", "rawtypes" }) | ||
TypedValue<?> typedValue = new TypedValue(value, value == null ? Object.class : value.getClass()); | ||
Object outObject = doSingleExecution(parameters, typedValue, item.getVariables(), context, result); | ||
if (outObject != null) { | ||
addToData(outObject, item.getResult(), output); | ||
} else { | ||
// no definition means we don't plan to provide any output - so let's just copy the input item to the output | ||
// (actually, null definition with non-null outObject should not occur) | ||
if (parameters.outputDefinition == null) { | ||
output.add(item); | ||
} | ||
} | ||
if (!parameters.quiet) { | ||
context.println("Executed script/expression on " + getDescription(value)); | ||
} | ||
} | ||
|
||
abstract <I> Object doSingleExecution(P parameters, TypedValue<I> inputTypedValue, | ||
VariablesMap externalVariables, ExecutionContext context, OperationResult result) | ||
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, CommunicationException, | ||
ConfigurationException, SecurityViolationException; | ||
|
||
private void addToData(@NotNull Object outObject, @NotNull OperationResult result, PipelineData output) { | ||
if (outObject instanceof Collection) { | ||
for (Object o : (Collection<?>) outObject) { | ||
addToData(o, result, output); | ||
} | ||
} else { | ||
PrismValue value; | ||
if (outObject instanceof PrismValue) { | ||
value = (PrismValue) outObject; | ||
} else if (outObject instanceof Objectable) { | ||
value = prismContext.itemFactory().createObjectValue((Objectable) outObject); | ||
} else if (outObject instanceof Containerable) { | ||
value = prismContext.itemFactory().createContainerValue((Containerable) outObject); | ||
} else { | ||
value = prismContext.itemFactory().createPropertyValue(outObject); | ||
} | ||
output.add(new PipelineItem(value, result)); | ||
} | ||
} | ||
|
||
// TODO implement seriously! This implementation requires custom modelContext variable that might or might not be present | ||
// (it is set e.g. for policy rule script execution) | ||
<F extends ObjectType> LensContext<F> getLensContext(VariablesMap externalVariables) { | ||
TypedValue<?> modelContextTypedValue = externalVariables.get(ExpressionConstants.VAR_MODEL_CONTEXT); | ||
//noinspection unchecked | ||
return modelContextTypedValue != null ? (LensContext<F>) modelContextTypedValue.getValue() : null; | ||
} | ||
|
||
/** | ||
* Parameters for `execute-script` and `evaluate-expression` actions. | ||
*/ | ||
static class Parameters { | ||
|
||
final ItemDefinition<?> outputDefinition; | ||
final boolean forWholeInput; | ||
final boolean quiet; | ||
|
||
Parameters( | ||
ItemDefinition<?> outputDefinition, | ||
boolean forWholeInput, | ||
boolean quiet) { | ||
this.outputDefinition = outputDefinition; | ||
this.forWholeInput = forWholeInput; | ||
this.quiet = quiet; | ||
} | ||
} | ||
} |
Oops, something went wrong.