/*
* Copyright (c) 2010-2016 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.provisioning.impl;
import com.evolveum.midpoint.common.refinery.RefinedObjectClassDefinition;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.PrismProperty;
import com.evolveum.midpoint.prism.PrismPropertyValue;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.query.AndFilter;
import com.evolveum.midpoint.prism.query.EqualFilter;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.OrFilter;
import com.evolveum.midpoint.prism.query.RefFilter;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.provisioning.api.ConstraintViolationConfirmer;
import com.evolveum.midpoint.provisioning.api.ConstraintsCheckingResult;
import com.evolveum.midpoint.provisioning.api.ProvisioningService;
import com.evolveum.midpoint.repo.api.RepositoryService;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.ResourceShadowDiscriminator;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.schema.processor.ResourceAttributeDefinition;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.MiscUtil;
import com.evolveum.midpoint.util.caching.AbstractCache;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SecurityViolationException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType;
import com.evolveum.prism.xml.ns._public.types_3.PolyStringType;
import javax.xml.namespace.QName;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author semancik
* @author mederly
*/
public class ConstraintsChecker {
private static final Trace LOGGER = TraceManager.getTrace(ConstraintsChecker.class);
private static final Trace PERFORMANCE_ADVISOR = TraceManager.getPerformanceAdvisorTrace();
private static ThreadLocal<Cache> cacheThreadLocal = new ThreadLocal<>();
private PrismContext prismContext;
private RepositoryService cacheRepositoryService;
private ProvisioningService provisioningService;
private StringBuilder messageBuilder = new StringBuilder();
public PrismContext getPrismContext() {
return prismContext;
}
public void setPrismContext(PrismContext prismContext) {
this.prismContext = prismContext;
}
public void setCacheRepositoryService(RepositoryService cacheRepositoryService) {
this.cacheRepositoryService = cacheRepositoryService;
}
public void setProvisioningService(ProvisioningService provisioningService) {
this.provisioningService = provisioningService;
}
private RefinedObjectClassDefinition shadowDefinition;
private PrismObject<ShadowType> shadowObject;
private ResourceType resourceType;
private String shadowOid;
private ResourceShadowDiscriminator resourceShadowDiscriminator;
private ConstraintViolationConfirmer constraintViolationConfirmer;
public void setShadowDefinition(RefinedObjectClassDefinition shadowDefinition) {
this.shadowDefinition = shadowDefinition;
}
public void setShadowObject(PrismObject<ShadowType> shadowObject) {
this.shadowObject = shadowObject;
}
public void setResourceType(ResourceType resourceType) {
this.resourceType = resourceType;
}
public void setShadowOid(String shadowOid) {
this.shadowOid = shadowOid;
}
public void setResourceShadowDiscriminator(ResourceShadowDiscriminator resourceShadowDiscriminator) {
this.resourceShadowDiscriminator = resourceShadowDiscriminator;
}
public void setConstraintViolationConfirmer(ConstraintViolationConfirmer constraintViolationConfirmer) {
this.constraintViolationConfirmer = constraintViolationConfirmer;
}
private ConstraintsCheckingResult constraintsCheckingResult;
public ConstraintsCheckingResult check(Task task, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException, CommunicationException, ConfigurationException, SecurityViolationException {
constraintsCheckingResult = new ConstraintsCheckingResult();
constraintsCheckingResult.setSatisfiesConstraints(true);
PrismContainer<?> attributesContainer = shadowObject.findContainer(ShadowType.F_ATTRIBUTES);
if (attributesContainer == null) {
// No attributes no constraint violations
LOGGER.trace("Current shadow does not contain attributes, skipping checking uniqueness.");
return constraintsCheckingResult;
}
Collection<? extends ResourceAttributeDefinition> uniqueAttributeDefs = MiscUtil.unionExtends(shadowDefinition.getPrimaryIdentifiers(),
shadowDefinition.getSecondaryIdentifiers());
LOGGER.trace("Secondary IDs {}", shadowDefinition.getSecondaryIdentifiers());
for (ResourceAttributeDefinition attrDef: uniqueAttributeDefs) {
PrismProperty<?> attr = attributesContainer.findProperty(attrDef.getName());
LOGGER.trace("Attempt to check uniqueness of {} (def {})", attr, attrDef);
if (attr == null) {
continue;
}
constraintsCheckingResult.getCheckedAttributes().add(attr.getElementName());
boolean unique = checkAttributeUniqueness(attr, shadowDefinition, resourceType, shadowOid, task, result);
if (!unique) {
LOGGER.debug("Attribute {} conflicts with existing object (in {})", attr, resourceShadowDiscriminator);
constraintsCheckingResult.getConflictingAttributes().add(attr.getElementName());
constraintsCheckingResult.setSatisfiesConstraints(false);
}
}
constraintsCheckingResult.setMessages(messageBuilder.toString());
return constraintsCheckingResult;
}
private boolean checkAttributeUniqueness(PrismProperty identifier, RefinedObjectClassDefinition accountDefinition,
ResourceType resourceType, String oid, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException, SecurityViolationException {
List<PrismPropertyValue<?>> identifierValues = identifier.getValues();
if (identifierValues.isEmpty()) {
throw new SchemaException("Empty identifier "+identifier+" while checking uniqueness of "+oid+" ("+resourceType+")");
}
//TODO: set matching rule instead of null
ObjectQuery query = QueryBuilder.queryFor(ShadowType.class, prismContext)
.itemWithDef(identifier.getDefinition(), ShadowType.F_ATTRIBUTES, identifier.getDefinition().getName())
.eq(PrismPropertyValue.cloneCollection(identifierValues))
.and().item(ShadowType.F_OBJECT_CLASS).eq(accountDefinition.getObjectClassDefinition().getTypeName())
.and().item(ShadowType.F_RESOURCE_REF).ref(resourceType.getOid())
.and().block()
.item(ShadowType.F_DEAD).eq(false)
.or().item(ShadowType.F_DEAD).isNull()
.endBlock()
.build();
boolean unique = checkUniqueness(oid, identifier, query, task, result);
return unique;
}
private boolean checkUniqueness(String oid, PrismProperty identifier, ObjectQuery query, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, SecurityViolationException, CommunicationException, ConfigurationException {
if (Cache.isOk(resourceType.getOid(), oid, shadowDefinition.getTypeName(), identifier.getDefinition().getName(), identifier.getValues())) {
return true;
}
// Here was an attempt to call cacheRepositoryService.searchObjects directly (because we use noFetch, so the net result is searching in repo anyway).
// The idea was that it is faster and cacheable. However, it is not correct. We have to apply definition to query before execution, e.g.
// because there could be a matching rule; see ShadowManager.processQueryMatchingRuleFilter.
// Besides that, now the constraint checking is cached at a higher level, so this is not a big issue any more.
Collection<SelectorOptions<GetOperationOptions>> options = SelectorOptions.createCollection(GetOperationOptions.createNoFetch());
List<PrismObject<ShadowType>> foundObjects = provisioningService.searchObjects(ShadowType.class, query, options, task, result);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Uniqueness check of {} resulted in {} results, using query:\n{}",
identifier, foundObjects.size(), query.debugDump());
}
if (foundObjects.isEmpty()) {
Cache.setOk(resourceType.getOid(), oid, shadowDefinition.getTypeName(), identifier.getDefinition().getName(), identifier.getValues());
return true;
}
if (foundObjects.size() > 1) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Found {} objects with attribute {}", foundObjects.size() ,identifier.toHumanReadableString());
for (PrismObject<ShadowType> foundObject: foundObjects) {
LOGGER.debug("Conflicting object:\n{}", foundObject.debugDump());
}
}
message("Found more than one object with attribute "+identifier.toHumanReadableString());
return false;
}
LOGGER.trace("Comparing {} and {}", foundObjects.get(0).getOid(), oid);
boolean match = foundObjects.get(0).getOid().equals(oid);
if (!match) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Found conflicting existing object with attribute " + identifier.toHumanReadableString() + ":\n"
+ foundObjects.get(0).debugDump());
}
message("Found conflicting existing object with attribute " + identifier.toHumanReadableString() + ": " + foundObjects.get(0));
match = !constraintViolationConfirmer.confirmViolation(foundObjects.get(0).getOid());
constraintsCheckingResult.setConflictingShadow(foundObjects.get(0));
// we do not cache "OK" here because the violation confirmer could depend on attributes/items that are not under our observations
} else {
Cache.setOk(resourceType.getOid(), oid, shadowDefinition.getTypeName(), identifier.getDefinition().getName(), identifier.getValues());
return true;
}
return match;
}
private void message(String message) {
if (messageBuilder.length() != 0) {
messageBuilder.append(", ");
}
messageBuilder.append(message);
}
public static void enterCache() {
Cache.enter(cacheThreadLocal, Cache.class, LOGGER);
}
public static void exitCache() {
Cache.exit(cacheThreadLocal, LOGGER);
}
public static <T extends ShadowType> void onShadowAddOperation(T shadow) {
Cache cache = Cache.getCache();
if (cache != null) {
log("Clearing cache on shadow add operation");
cache.conflictFreeSituations.clear(); // TODO fix this brute-force approach
}
}
public static void onShadowModifyOperation(Collection<? extends ItemDelta> deltas) {
// here we must be very cautious; we do not know which attributes are naming ones!
// so in case of any attribute change, let's clear the cache
// (actually, currently only naming attributes are stored in repo)
Cache cache = Cache.getCache();
if (cache == null) {
return;
}
ItemPath attributesPath = new ItemPath(ShadowType.F_ATTRIBUTES);
for (ItemDelta itemDelta : deltas) {
if (attributesPath.isSubPathOrEquivalent(itemDelta.getParentPath())) {
log("Clearing cache on shadow attribute modify operation");
cache.conflictFreeSituations.clear();
return;
}
}
}
static private class Situation {
String resourceOid;
String knownShadowOid;
QName objectClassName;
QName attributeName;
Set attributeValues;
public Situation(String resourceOid, String knownShadowOid, QName objectClassName, QName attributeName, Set attributeValues) {
this.resourceOid = resourceOid;
this.knownShadowOid = knownShadowOid;
this.objectClassName = objectClassName;
this.attributeName = attributeName;
this.attributeValues = attributeValues;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Situation situation = (Situation) o;
if (!attributeName.equals(situation.attributeName)) return false;
if (attributeValues != null ? !attributeValues.equals(situation.attributeValues) : situation.attributeValues != null)
return false;
if (knownShadowOid != null ? !knownShadowOid.equals(situation.knownShadowOid) : situation.knownShadowOid != null)
return false;
if (objectClassName != null ? !objectClassName.equals(situation.objectClassName) : situation.objectClassName != null)
return false;
if (!resourceOid.equals(situation.resourceOid)) return false;
return true;
}
@Override
public int hashCode() {
int result = resourceOid.hashCode();
result = 31 * result + (knownShadowOid != null ? knownShadowOid.hashCode() : 0);
result = 31 * result + (objectClassName != null ? objectClassName.hashCode() : 0);
result = 31 * result + attributeName.hashCode();
result = 31 * result + (attributeValues != null ? attributeValues.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Situation{" +
"resourceOid='" + resourceOid + '\'' +
", knownShadowOid='" + knownShadowOid + '\'' +
", objectClassName=" + objectClassName +
", attributeName=" + attributeName +
", attributeValues=" + attributeValues +
'}';
}
}
public static class Cache extends AbstractCache {
private Set<Situation> conflictFreeSituations = new HashSet<>();
private static boolean isOk(String resourceOid, String knownShadowOid, QName objectClassName, QName attributeName, List attributeValues) {
return isOk(new Situation(resourceOid, knownShadowOid, objectClassName, attributeName, getRealValuesSet(attributeValues)));
}
public static boolean isOk(Situation situation) {
if (situation.attributeValues == null) { // special case - problem - TODO implement better
return false;
}
Cache cache = getCache();
if (cache == null) {
log("Cache NULL for {}", situation);
return false;
}
if (cache.conflictFreeSituations.contains(situation)) {
log("Cache HIT for {}", situation);
return true;
} else {
log("Cache MISS for {}", situation);
return false;
}
}
private static void setOk(String resourceOid, String knownShadowOid, QName objectClassName, QName attributeName, List attributeValues) {
setOk(new Situation(resourceOid, knownShadowOid, objectClassName, attributeName, getRealValuesSet(attributeValues)));
}
private static Set getRealValuesSet(List attributeValues) {
Set retval = new HashSet();
for (Object attributeValue : attributeValues) {
if (attributeValue == null) {
// can be skipped
} else if (attributeValue instanceof PrismPropertyValue) {
retval.add(((PrismPropertyValue) attributeValue).getValue());
} else {
LOGGER.warn("Unsupported attribute value: {}", attributeValue);
return null; // a problem!
}
}
return retval;
}
public static void setOk(Situation situation) {
Cache cache = getCache();
if (cache != null) {
cache.conflictFreeSituations.add(situation);
}
}
private static Cache getCache() {
return cacheThreadLocal.get();
}
public static void remove(PolyStringType name) {
Cache cache = getCache();
if (name != null && cache != null) {
log("Cache REMOVE for {}", name);
cache.conflictFreeSituations.remove(name.getOrig());
}
}
@Override
public String description() {
return "conflict-free situations: " + conflictFreeSituations;
}
}
private static void log(String message, Object... params) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(message, params);
}
if (PERFORMANCE_ADVISOR.isTraceEnabled()) {
PERFORMANCE_ADVISOR.trace(message, params);
}
}
}