/*
* Copyright (c) 2010-2017 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.model.common.expression.evaluator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.xml.namespace.QName;
import org.apache.commons.lang.BooleanUtils;
import com.evolveum.midpoint.model.api.ModelService;
import com.evolveum.midpoint.model.common.expression.Expression;
import com.evolveum.midpoint.model.common.expression.ExpressionEvaluationContext;
import com.evolveum.midpoint.model.common.expression.ExpressionFactory;
import com.evolveum.midpoint.model.common.expression.ExpressionUtil;
import com.evolveum.midpoint.model.common.expression.ExpressionVariables;
import com.evolveum.midpoint.model.common.expression.evaluator.caching.AbstractSearchExpressionEvaluatorCache;
import com.evolveum.midpoint.prism.Containerable;
import com.evolveum.midpoint.prism.Definition;
import com.evolveum.midpoint.prism.ItemDefinition;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismContainerDefinition;
import com.evolveum.midpoint.prism.PrismContainerValue;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.PrismObjectDefinition;
import com.evolveum.midpoint.prism.PrismValue;
import com.evolveum.midpoint.prism.crypto.Protector;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.delta.PlusMinusZero;
import com.evolveum.midpoint.prism.delta.PrismValueDeltaSetTriple;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.path.NameItemPathSegment;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.QueryJaxbConvertor;
import com.evolveum.midpoint.prism.util.CloneUtil;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.ResultHandler;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.schema.constants.ObjectTypes;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.MiscSchemaUtil;
import com.evolveum.midpoint.schema.util.ObjectResolver;
import com.evolveum.midpoint.security.api.SecurityEnforcer;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.PolicyViolationException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SecurityViolationException;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.AssignmentType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.VariableBindingDefinitionType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectSearchStrategyType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PopulateItemType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PopulateType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.SearchObjectExpressionEvaluatorType;
import com.evolveum.prism.xml.ns._public.query_3.SearchFilterType;
import com.evolveum.prism.xml.ns._public.types_3.ItemPathType;
/**
* @author Radovan Semancik
*/
public abstract class AbstractSearchExpressionEvaluator<V extends PrismValue,D extends ItemDefinition>
extends AbstractValueTransformationExpressionEvaluator<V,D,SearchObjectExpressionEvaluatorType> {
private static final Trace LOGGER = TraceManager.getTrace(AbstractSearchExpressionEvaluator.class);
private PrismContext prismContext;
private D outputDefinition;
private Protector protector;
private ObjectResolver objectResolver;
private ModelService modelService;
protected AbstractSearchExpressionEvaluator(SearchObjectExpressionEvaluatorType expressionEvaluatorType,
D outputDefinition, Protector protector, ObjectResolver objectResolver,
ModelService modelService, PrismContext prismContext, SecurityEnforcer securityEnforcer) {
super(expressionEvaluatorType, securityEnforcer);
this.outputDefinition = outputDefinition;
this.prismContext = prismContext;
this.protector = protector;
this.objectResolver = objectResolver;
this.modelService = modelService;
}
public PrismContext getPrismContext() {
return prismContext;
}
public ItemDefinition getOutputDefinition() {
return outputDefinition;
}
public Protector getProtector() {
return protector;
}
public ObjectResolver getObjectResolver() {
return objectResolver;
}
public ModelService getModelService() {
return modelService;
}
@Override
protected List<V> transformSingleValue(ExpressionVariables variables, PlusMinusZero valueDestination, boolean useNew,
ExpressionEvaluationContext context, String contextDescription, Task task, OperationResult result)
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException {
// if (LOGGER.isTraceEnabled()) {
// LOGGER.trace("transformSingleValue in {}\nvariables:\n{}\nvalueDestination: {}\nuseNew: {}",
// new Object[]{contextDescription, variables.debugDump(1), valueDestination, useNew});
// }
QName targetTypeQName = getExpressionEvaluatorType().getTargetType();
if (targetTypeQName == null) {
targetTypeQName = getDefaultTargetType();
}
if (targetTypeQName != null && QNameUtil.isUnqualified(targetTypeQName)) {
targetTypeQName = getPrismContext().getSchemaRegistry().resolveUnqualifiedTypeName(targetTypeQName);
}
ObjectTypes targetType = ObjectTypes.getObjectTypeFromTypeQName(targetTypeQName);
if (targetType == null) {
throw new SchemaException("Unknown target type "+targetTypeQName+" in "+shortDebugDump());
}
Class<? extends ObjectType> targetTypeClass = targetType.getClassDefinition();
List<V> resultValues = null;
ObjectQuery query = null;
List<ItemDelta<V, D>> additionalAttributeDeltas = null;
PopulateType populateAssignmentType = getExpressionEvaluatorType().getPopulate();
if (populateAssignmentType != null) {
additionalAttributeDeltas = collectAdditionalAttributes(populateAssignmentType, outputDefinition, variables, context, contextDescription, task, result);
}
if (getExpressionEvaluatorType().getOid() != null) {
resultValues = new ArrayList<>(1);
resultValues.add(createPrismValue(getExpressionEvaluatorType().getOid(), targetTypeQName, additionalAttributeDeltas,
context));
} else {
SearchFilterType filterType = getExpressionEvaluatorType().getFilter();
if (filterType == null) {
throw new SchemaException("No filter in "+shortDebugDump());
}
query = QueryJaxbConvertor.createObjectQuery(targetTypeClass, filterType, prismContext);
if (LOGGER.isTraceEnabled()){
LOGGER.trace("XML query converted to: {}", query.debugDump());
}
query = ExpressionUtil.evaluateQueryExpressions(query, variables, context.getExpressionFactory(),
prismContext, context.getContextDescription(), task, result);
if (LOGGER.isTraceEnabled()){
LOGGER.trace("Expression in query evaluated to: {}", query.debugDump());
}
query = extendQuery(query, context);
if (LOGGER.isTraceEnabled()){
LOGGER.trace("Query after extension: {}", query.debugDump());
}
resultValues = executeSearchUsingCache(targetTypeClass, targetTypeQName, query, additionalAttributeDeltas, context, contextDescription, task, context
.getResult());
if (resultValues.isEmpty()) {
ObjectReferenceType defaultTargetRef = getExpressionEvaluatorType().getDefaultTargetRef();
if (defaultTargetRef != null) {
resultValues.add(createPrismValue(defaultTargetRef.getOid(), targetTypeQName, additionalAttributeDeltas,
context));
}
}
}
if (resultValues.isEmpty() && getExpressionEvaluatorType().isCreateOnDemand() == Boolean.TRUE &&
(valueDestination == PlusMinusZero.PLUS || valueDestination == PlusMinusZero.ZERO || useNew)) {
String createdObjectOid = createOnDemand(targetTypeClass, variables, context, context.getContextDescription(), task, context
.getResult());
resultValues.add(createPrismValue(createdObjectOid, targetTypeQName, additionalAttributeDeltas, context));
}
LOGGER.trace("Search expression got {} results for query {}", resultValues==null?"null":resultValues.size(), query);
return (List<V>) resultValues;
}
protected ObjectQuery extendQuery(ObjectQuery query, ExpressionEvaluationContext params) throws SchemaException, ExpressionEvaluationException {
return query;
}
protected QName getDefaultTargetType() {
return null;
}
// subclasses may provide caching
protected AbstractSearchExpressionEvaluatorCache getCache() {
return null;
}
private <O extends ObjectType> List<V> executeSearchUsingCache(Class<O> targetTypeClass, final QName targetTypeQName, ObjectQuery query, List<ItemDelta<V, D>> additionalAttributeDeltas,
final ExpressionEvaluationContext params, String contextDescription, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException {
ObjectSearchStrategyType searchStrategy = getSearchStrategy();
AbstractSearchExpressionEvaluatorCache cache = getCache();
if (cache == null) {
return executeSearch(null, targetTypeClass, targetTypeQName, query, searchStrategy, additionalAttributeDeltas, params, contextDescription, task, result);
}
List<V> list = cache.getQueryResult(targetTypeClass, query, searchStrategy, params, prismContext);
if (list != null) {
LOGGER.trace("Cache: HIT {} ({})", query, targetTypeClass.getSimpleName());
return CloneUtil.clone(list);
}
LOGGER.trace("Cache: MISS {} ({})", query, targetTypeClass.getSimpleName());
List<PrismObject> rawResult = new ArrayList<>();
list = executeSearch(rawResult, targetTypeClass, targetTypeQName, query, searchStrategy, additionalAttributeDeltas, params, contextDescription, task, result);
if (list != null && !list.isEmpty()) {
// we don't want to cache negative results (e.g. if used with focal objects it might mean that they would be attempted to create multiple times)
cache.putQueryResult(targetTypeClass, query, searchStrategy, params, list, rawResult, prismContext);
}
return list;
}
private ObjectSearchStrategyType getSearchStrategy() {
SearchObjectExpressionEvaluatorType evaluator = getExpressionEvaluatorType();
if (evaluator.getSearchStrategy() != null) {
return evaluator.getSearchStrategy();
}
if (BooleanUtils.isTrue(evaluator.isSearchOnResource())) {
return ObjectSearchStrategyType.ON_RESOURCE;
}
return ObjectSearchStrategyType.IN_REPOSITORY;
}
private <O extends ObjectType> List<V> executeSearch(List<PrismObject> rawResult,
Class<O> targetTypeClass, final QName targetTypeQName, ObjectQuery query,
ObjectSearchStrategyType searchStrategy,
List<ItemDelta<V, D>> additionalAttributeDeltas,
ExpressionEvaluationContext params, String contextDescription,
Task task, OperationResult result)
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException {
// TODO think about handling of CommunicationException | ConfigurationException | SecurityViolationException
// Currently if tryAlsoRepository=true (for ON_RESOURCE strategy), such errors result in searching pure repo. And if there's no such
// object in the repo, probably no exception is raised.
// But if ON_RESOURCE_IF_NEEDED, and the object does not exist in repo, an exception WILL be raised.
//
// Probably we could create specific types of fetch strategies to reflect various error handling requirements.
// (Or treat it via separate parameter.)
switch (searchStrategy) {
case IN_REPOSITORY:
return executeSearchAttempt(rawResult, targetTypeClass, targetTypeQName, query, false, false, additionalAttributeDeltas, params, contextDescription, task, result);
case ON_RESOURCE:
return executeSearchAttempt(rawResult, targetTypeClass, targetTypeQName, query, true, true, additionalAttributeDeltas, params, contextDescription, task, result);
case ON_RESOURCE_IF_NEEDED:
List<V> inRepo = executeSearchAttempt(rawResult, targetTypeClass, targetTypeQName, query, false, false, additionalAttributeDeltas, params, contextDescription, task, result);
if (!inRepo.isEmpty()) {
return inRepo;
}
if (rawResult != null) {
rawResult.clear();
}
return executeSearchAttempt(rawResult, targetTypeClass, targetTypeQName, query, true, false, additionalAttributeDeltas, params, contextDescription, task, result);
default:
throw new IllegalArgumentException("Unknown search strategy: " + searchStrategy);
}
}
private <O extends ObjectType> List<V> executeSearchAttempt(final List<PrismObject> rawResult,
Class<O> targetTypeClass, final QName targetTypeQName,
ObjectQuery query, boolean searchOnResource, boolean tryAlsoRepository,
final List<ItemDelta<V, D>> additionalAttributeDeltas,
final ExpressionEvaluationContext params, String contextDescription,
Task task, OperationResult result)
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException {
final List<V> list = new ArrayList<V>();
Collection<SelectorOptions<GetOperationOptions>> options = new ArrayList<>();
if (!searchOnResource) {
options.add(SelectorOptions.create(GetOperationOptions.createNoFetch()));
}
extendOptions(options, searchOnResource);
ResultHandler<O> handler = new ResultHandler<O>() {
@Override
public boolean handle(PrismObject<O> object, OperationResult parentResult) {
if (rawResult != null) {
rawResult.add(object);
}
list.add(createPrismValue(object.getOid(), targetTypeQName, additionalAttributeDeltas, params));
// TODO: we should count results and stop after some reasonably high number?
return true;
}
};
try {
objectResolver.searchIterative(targetTypeClass, query, options, handler, task, result);
} catch (IllegalStateException e) { // this comes from checkConsistence methods
throw new IllegalStateException(e.getMessage()+" in "+contextDescription, e);
} catch (SchemaException e) {
throw new SchemaException(e.getMessage()+" in "+contextDescription, e);
} catch (SystemException e) {
throw new SystemException(e.getMessage()+" in "+contextDescription, e);
} catch (CommunicationException | ConfigurationException
| SecurityViolationException e) {
if (searchOnResource && tryAlsoRepository) {
options = SelectorOptions.createCollection(GetOperationOptions.createNoFetch());
try {
objectResolver.searchIterative(targetTypeClass, query, options, handler, task, result);
} catch (SchemaException e1) {
throw new SchemaException(e1.getMessage()+" in "+contextDescription, e1);
} catch (CommunicationException | ConfigurationException
| SecurityViolationException e1) {
// TODO improve handling of exception.. we do not want to
// stop whole projection computation, but what to do if the
// shadow for group doesn't exist? (MID-2107)
throw new ExpressionEvaluationException("Unexpected expression exception "+e+": "+e.getMessage(), e);
}
} else {
throw new ExpressionEvaluationException("Unexpected expression exception "+e+": "+e.getMessage(), e);
}
} catch (ObjectNotFoundException e) {
throw e;
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Assignment expression resulted in {} objects, using query:\n{}",
list.size(), query.debugDump());
}
return list;
}
protected void extendOptions(Collection<SelectorOptions<GetOperationOptions>> options, boolean searchOnResource) {
// Nothing to do. To be overridden by subclasses
}
// e.g parameters, activation for assignment etc.
protected <C extends Containerable> List<ItemDelta<V,D>> collectAdditionalAttributes(PopulateType fromPopulate,
D outputDefinition, ExpressionVariables variables, ExpressionEvaluationContext params,
String contextDescription, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException {
if (!(outputDefinition instanceof PrismContainerDefinition)) {
return null;
}
List<ItemDelta<V,D>> deltas = new ArrayList<>();
for (PopulateItemType populateItem: fromPopulate.getPopulateItem()) {
ItemDelta<V,D> itemDelta = evaluatePopulateExpression(populateItem, variables, params,
(PrismContainerDefinition<C>) outputDefinition, contextDescription, false, task, result);
if (itemDelta != null) {
deltas.add(itemDelta);
}
}
return deltas;
}
protected abstract V createPrismValue(String oid, QName targetTypeQName, List<ItemDelta<V, D>> additionalAttributeDeltas, ExpressionEvaluationContext params);
private <O extends ObjectType> String createOnDemand(Class<O> targetTypeClass, ExpressionVariables variables,
ExpressionEvaluationContext params, String contextDescription, Task task, OperationResult result)
throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Going to create assignment targets on demand, variables:\n{}", variables.formatVariables());
}
PrismObjectDefinition<O> objectDefinition = prismContext.getSchemaRegistry().findObjectDefinitionByCompileTimeClass(targetTypeClass);
PrismObject<O> newObject = objectDefinition.instantiate();
PopulateType populateObject = getExpressionEvaluatorType().getPopulateObject();
if (populateObject == null) {
LOGGER.warn("No populateObject in assignment expression in {}, "
+ "object created on demand will be empty. Subsequent operations will most likely fail", contextDescription);
} else {
for (PopulateItemType populateItem: populateObject.getPopulateItem()) {
ItemDelta<?,?> itemDelta = evaluatePopulateExpression(populateItem, variables, params,
objectDefinition, contextDescription, true, task, result);
if (itemDelta != null) {
itemDelta.applyTo(newObject);
}
}
}
LOGGER.debug("Creating object on demand from {}: {}", contextDescription, newObject);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Creating object on demand:\n{}", newObject.debugDump());
}
ObjectDelta<O> addDelta = newObject.createAddDelta();
Collection<ObjectDelta<? extends ObjectType>> deltas = MiscSchemaUtil.createCollection(addDelta);
try {
modelService.executeChanges(deltas, null, task, result);
} catch (ObjectAlreadyExistsException | CommunicationException | ConfigurationException
| PolicyViolationException | SecurityViolationException e) {
throw new ExpressionEvaluationException(e.getMessage(), e);
}
return addDelta.getOid();
}
private <IV extends PrismValue, ID extends ItemDefinition, C extends Containerable> ItemDelta<IV,ID> evaluatePopulateExpression(PopulateItemType populateItem,
ExpressionVariables variables, ExpressionEvaluationContext params, PrismContainerDefinition<C> objectDefinition,
String contextDescription, boolean evaluateMinus, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException {
ExpressionType expressionType = populateItem.getExpression();
if (expressionType == null) {
LOGGER.warn("No expression in populateObject in assignment expression in {}, "
+ "skipping. Subsequent operations will most likely fail", contextDescription);
return null;
}
VariableBindingDefinitionType targetType = populateItem.getTarget();
if (targetType == null) {
LOGGER.warn("No target in populateObject in assignment expression in {}, "
+ "skipping. Subsequent operations will most likely fail", contextDescription);
return null;
}
ItemPathType itemPathType = targetType.getPath();
if (itemPathType == null) {
throw new SchemaException("No path in target definition in "+contextDescription);
}
ItemPath targetPath = itemPathType.getItemPath();
ID propOutputDefinition = ExpressionUtil.resolveDefinitionPath(targetPath, variables,
objectDefinition, "target definition in "+contextDescription);
if (propOutputDefinition == null) {
throw new SchemaException("No target item that would conform to the path "+targetPath+" in "+contextDescription);
}
String expressionDesc = "expression in assignment expression in "+contextDescription;
ExpressionFactory expressionFactory = params.getExpressionFactory();
Expression<IV,ID> expression = expressionFactory.makeExpression(expressionType, propOutputDefinition,
expressionDesc, task, result);
ExpressionEvaluationContext context = new ExpressionEvaluationContext(null, variables,
expressionDesc, task, result);
context.setExpressionFactory(expressionFactory);
context.setStringPolicyResolver(params.getStringPolicyResolver());
context.setDefaultTargetContext(params.getDefaultTargetContext());
context.setSkipEvaluationMinus(true);
context.setSkipEvaluationPlus(false);
PrismValueDeltaSetTriple<IV> outputTriple = expression.evaluate(context);
LOGGER.trace("output triple: {}", outputTriple.debugDump());
Collection<IV> pvalues = outputTriple.getNonNegativeValues();
// Maybe not really clean but it works. TODO: refactor later
NameItemPathSegment first = (NameItemPathSegment)targetPath.first();
if (first.isVariable()) {
targetPath = targetPath.rest();
}
ItemDelta<IV,ID> itemDelta = propOutputDefinition.createEmptyDelta(targetPath);
itemDelta.addValuesToAdd(PrismValue.cloneCollection(pvalues));
LOGGER.trace("Item delta:\n{}", itemDelta.debugDump());
return itemDelta;
}
// Override the default in this case. It makes more sense like this.
@Override
protected Boolean isIncludeNullInputs() {
Boolean superValue = super.isIncludeNullInputs();
if (superValue != null) {
return superValue;
}
return false;
}
/* (non-Javadoc)
* @see com.evolveum.midpoint.common.expression.ExpressionEvaluator#shortDebugDump()
*/
@Override
public String shortDebugDump() {
return "abstractSearchExpression";
}
}