/*
* 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.impl.dataModel;
import com.evolveum.midpoint.common.refinery.*;
import com.evolveum.midpoint.model.api.DataModelVisualizer;
import com.evolveum.midpoint.model.api.ModelService;
import com.evolveum.midpoint.model.impl.dataModel.dot.DotModel;
import com.evolveum.midpoint.model.impl.dataModel.model.AdHocDataItem;
import com.evolveum.midpoint.model.impl.dataModel.model.DataItem;
import com.evolveum.midpoint.model.impl.dataModel.model.ResourceDataItem;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
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.builder.QueryBuilder;
import com.evolveum.midpoint.schema.constants.ExpressionConstants;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.processor.ResourceSchema;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author pmederly
*/
@Component
public class DataModelVisualizerImpl implements DataModelVisualizer {
private static final Trace LOGGER = TraceManager.getTrace(DataModelVisualizerImpl.class);
public static final QName ACTIVATION_EXISTENCE = new QName(SchemaConstants.NS_C, "existence");
@Autowired
private ModelService modelService;
@Autowired
private PrismContext prismContext;
@Override
public String visualize(Collection<String> resourceOids, Target target, Task task, OperationResult result)
throws SchemaException, SecurityViolationException, ObjectNotFoundException, CommunicationException, ConfigurationException {
LOGGER.debug("Starting data model visualization");
DataModel model = new DataModel(prismContext);
ObjectQuery resourceQuery;
if (resourceOids != null) {
resourceQuery = QueryBuilder.queryFor(ResourceType.class, prismContext)
.id(resourceOids.toArray(new String[0]))
.build();
} else {
resourceQuery = null;
}
List<PrismObject<ResourceType>> resources = modelService.searchObjects(ResourceType.class, resourceQuery, null, task, result);
createDataItems(model, resources);
processResourceMappings(model, resources);
return export(model, target);
}
private String export(DataModel model, Target target) {
if (target == null || target == Target.DOT) {
return new DotModel(model).exportDot();
} else {
throw new UnsupportedOperationException("Not implemented yet.");
}
}
@Override
@SuppressWarnings("unchecked")
public String visualize(ResourceType resource, Target target, Task task, OperationResult result)
throws SchemaException, SecurityViolationException, ObjectNotFoundException, CommunicationException, ConfigurationException {
LOGGER.debug("Starting data model visualization for {}", ObjectTypeUtil.toShortString(resource));
DataModel model = new DataModel(prismContext);
List<PrismObject<ResourceType>> resources = new ArrayList<>();
resources.add(resource.clone().asPrismObject());
createDataItems(model, resources);
processResourceMappings(model, resources);
return export(model, target);
}
private void processResourceMappings(DataModel model, List<PrismObject<ResourceType>> resources) throws SchemaException {
for (PrismObject<ResourceType> resource : resources) {
LOGGER.debug("Processing {}", ObjectTypeUtil.toShortString(resource));
RefinedResourceSchema refinedResourceSchema = RefinedResourceSchemaImpl.getRefinedSchema(resource);
if (refinedResourceSchema == null) {
LOGGER.debug("Refined resource schema is null, skipping the resource.");
continue;
}
List<? extends RefinedObjectClassDefinition> refinedDefinitions = refinedResourceSchema.getRefinedDefinitions();
for (RefinedObjectClassDefinition refinedDefinition : refinedDefinitions) {
LOGGER.debug("Processing refined definition {}", refinedDefinition);
Collection<? extends RefinedAttributeDefinition<?>> attributeDefinitions = refinedDefinition.getAttributeDefinitions();
final ShadowKindType kind = def(refinedDefinition.getKind());
final String intent = def(refinedDefinition.getIntent());
for (RefinedAttributeDefinition<?> attributeDefinition : attributeDefinitions) {
if (attributeDefinition.isIgnored()) {
continue;
}
LOGGER.debug("Processing refined attribute definition for {}", attributeDefinition.getName());
ResourceDataItem attrItem = model.findResourceItem(resource.getOid(), kind, intent, getObjectClassName(refinedDefinition),
new ItemPath(attributeDefinition.getName()));
if (attributeDefinition.getOutboundMappingType() != null) {
processOutboundMapping(model, attrItem, attributeDefinition.getOutboundMappingType(), null);
}
processInboundMappings(model, attrItem, attributeDefinition.getInboundMappingTypes());
}
Collection<RefinedAssociationDefinition> associationDefinitions = refinedDefinition.getAssociationDefinitions();
for (RefinedAssociationDefinition associationDefinition : associationDefinitions) {
if (associationDefinition.isIgnored()) {
continue;
}
LOGGER.debug("Processing refined association definition for {}", associationDefinition.getName());
ResourceDataItem assocItem = model.findResourceItem(resource.getOid(), kind, intent, getObjectClassName(refinedDefinition),
new ItemPath(associationDefinition.getName()));
if (associationDefinition.getOutboundMappingType() != null) {
processOutboundMapping(model, assocItem, associationDefinition.getOutboundMappingType(), null);
}
// if (associationDefinition.getAssociationTarget() != null) {
// RefinedObjectClassDefinition target = associationDefinition.getAssociationTarget();
// boolean objectToSubject = associationDefinition.getResourceObjectAssociationType().getDirection() == ResourceObjectAssociationDirectionType.OBJECT_TO_SUBJECT;
// associationDefinition.getResourceObjectAssociationType().getAssociationAttribute()
// }
}
ResourceActivationDefinitionType actMapping = refinedDefinition.getActivationSchemaHandling();
if (actMapping != null) {
QName objectClassName = getObjectClassName(refinedDefinition);
processBidirectionalMapping(model, resource.getOid(), kind, intent, objectClassName, new ItemPath(FocusType.F_ACTIVATION, ActivationType.F_ADMINISTRATIVE_STATUS), actMapping.getAdministrativeStatus());
processBidirectionalMapping(model, resource.getOid(), kind, intent, objectClassName, new ItemPath(FocusType.F_ACTIVATION, ActivationType.F_VALID_FROM), actMapping.getValidFrom());
processBidirectionalMapping(model, resource.getOid(), kind, intent, objectClassName, new ItemPath(FocusType.F_ACTIVATION, ActivationType.F_VALID_TO), actMapping.getValidTo());
processBidirectionalMapping(model, resource.getOid(), kind, intent, objectClassName, new ItemPath(FocusType.F_ACTIVATION, ActivationType.F_LOCKOUT_STATUS), actMapping.getLockoutStatus());
processBidirectionalMapping(model, resource.getOid(), kind, intent, objectClassName, new ItemPath(FocusType.F_ACTIVATION, ACTIVATION_EXISTENCE), actMapping.getExistence());
}
ResourcePasswordDefinitionType pwdDef = refinedDefinition.getPasswordDefinition();
if (pwdDef != null) {
final ItemPath pwdPath = new ItemPath(UserType.F_CREDENTIALS, CredentialsType.F_PASSWORD);
ResourceDataItem resourceDataItem = model.findResourceItem(resource.getOid(), kind, intent,
getObjectClassName(refinedDefinition), pwdPath);
if (resourceDataItem == null) {
throw new IllegalStateException("No resource item for " + resource.getOid() + ":" + kind + ":" + intent + ":" + pwdPath);
}
if (pwdDef.getOutbound() != null) {
for (MappingType outbound : pwdDef.getOutbound()) {
processOutboundMapping(model, resourceDataItem, outbound, pwdPath);
}
}
for (MappingType inbound : pwdDef.getInbound()) {
processInboundMapping(model, resourceDataItem, inbound, pwdPath);
}
}
}
}
}
private void processBidirectionalMapping(DataModel model, String oid, ShadowKindType kind, String intent, QName objectClassName, ItemPath itemPath,
ResourceBidirectionalMappingType mapping) {
if (mapping == null) {
return;
}
ResourceDataItem resourceDataItem = model.findResourceItem(oid, kind, intent, objectClassName, itemPath);
if (resourceDataItem == null) {
throw new IllegalStateException("No resource item for " + oid + ":" + kind + ":" + intent + ":" + objectClassName + ":" + itemPath);
}
for (MappingType outbound : mapping.getOutbound()) {
processOutboundMapping(model, resourceDataItem, outbound, itemPath);
}
for (MappingType inbound : mapping.getInbound()) {
processInboundMapping(model, resourceDataItem, inbound, itemPath);
}
}
private void createDataItems(DataModel model, List<PrismObject<ResourceType>> resources) throws SchemaException {
LOGGER.debug("createDataItems starting");
for (PrismObject<ResourceType> resource : resources) {
final ResourceSchema resourceSchema = RefinedResourceSchemaImpl.getResourceSchema(resource, prismContext);
if (resourceSchema == null) {
LOGGER.debug("Resource schema is null, skipping the resource.");
continue;
}
RefinedResourceSchema refinedResourceSchema = RefinedResourceSchemaImpl.getRefinedSchema(resource);
if (refinedResourceSchema == null) {
LOGGER.debug("Refined resource schema is null, skipping the resource."); // actually shouldn't be null if resource schema exists
continue;
}
model.registerResource(resource);
List<? extends RefinedObjectClassDefinition> refinedDefinitions = refinedResourceSchema.getRefinedDefinitions();
for (RefinedObjectClassDefinition refinedDefinition : refinedDefinitions) {
LOGGER.debug("Processing refined definition {} in {}", refinedDefinition, resource);
Collection<? extends RefinedAttributeDefinition<?>> attributeDefinitions = refinedDefinition.getAttributeDefinitions();
//Collection<? extends ResourceAttributeDefinition> rawAttributeDefinitions = refinedDefinition.getObjectClassDefinition().getAttributeDefinitions();
final ShadowKindType kind = def(refinedDefinition.getKind());
final String intent = def(refinedDefinition.getIntent());
for (RefinedAttributeDefinition<?> attributeDefinition : attributeDefinitions) {
if (attributeDefinition.isIgnored()) {
continue;
}
LOGGER.debug("Registering refined attribute definition for {}", attributeDefinition.getName());
ResourceDataItem attrItem = new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, attributeDefinition.getName());
attrItem.setRefinedAttributeDefinition(attributeDefinition);
// TODO check the name
model.registerDataItem(attrItem);
}
// TODO check attributes not mentioned in schema handling
Collection<RefinedAssociationDefinition> associationDefinitions = refinedDefinition.getAssociationDefinitions();
for (RefinedAssociationDefinition associationDefinition : associationDefinitions) {
if (associationDefinition.isIgnored()) {
continue;
}
LOGGER.debug("Registering refined association definition for {}", associationDefinition.getName());
ResourceDataItem assocItem = new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, associationDefinition.getName());
model.registerDataItem(assocItem);
}
model.registerDataItem(new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, new ItemPath(ShadowType.F_ACTIVATION, ActivationType.F_ADMINISTRATIVE_STATUS)));
model.registerDataItem(new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, new ItemPath(ShadowType.F_ACTIVATION, ActivationType.F_LOCKOUT_STATUS)));
model.registerDataItem(new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, new ItemPath(ShadowType.F_ACTIVATION, ActivationType.F_VALID_FROM)));
model.registerDataItem(new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, new ItemPath(ShadowType.F_ACTIVATION, ActivationType.F_VALID_TO)));
model.registerDataItem(new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, new ItemPath(ShadowType.F_ACTIVATION, ACTIVATION_EXISTENCE)));
model.registerDataItem(new ResourceDataItem(model, resource.getOid(), kind, intent, refinedResourceSchema, refinedDefinition, new ItemPath(ShadowType.F_CREDENTIALS, CredentialsType.F_PASSWORD)));
}
}
// createRepoDataItems(UserType.class);
// createRepoDataItems(RoleType.class);
// createRepoDataItems(OrgType.class);
// createRepoDataItems(ServiceType.class);
LOGGER.debug("createDataItems finished");
}
static ShadowKindType def(ShadowKindType kind) {
return kind != null ? kind : ShadowKindType.ACCOUNT;
}
static String def(String intent) {
return intent != null ? intent : "default";
}
private void processInboundMappings(DataModel model, ResourceDataItem item, List<MappingType> mappings) {
if (mappings == null) {
return;
}
for (MappingType mapping : mappings) {
processInboundMapping(model, item, mapping, null);
}
}
private void processInboundMapping(@NotNull DataModel model, @NotNull ResourceDataItem sourceItem, @NotNull MappingType mapping,
@Nullable ItemPath defaultTargetItemPath) {
LOGGER.debug("Processing inbound mapping: {} for {}", mapping, sourceItem);
List<DataItem> sources = new ArrayList<>();
for (VariableBindingDefinitionType sourceDecl : mapping.getSource()) {
LOGGER.debug(" - src: {}", sourceDecl.getPath());
DataItem explicitSourceItem = resolveSourceItem(model, sourceItem, mapping, sourceDecl, null);
sources.add(explicitSourceItem);
}
if (!sources.contains(sourceItem)) {
sources.add(sourceItem);
}
DataItem targetItem = null;
VariableBindingDefinitionType targetDecl = mapping.getTarget();
if (mapping.getTarget() != null) {
LOGGER.debug(" - target: {}", targetDecl.getPath());
targetItem = resolveTargetItem(model, sourceItem, mapping, targetDecl, ExpressionConstants.VAR_FOCUS);
} else if (defaultTargetItemPath != null) {
targetItem = resolveTargetItem(model, sourceItem, mapping, defaultTargetItemPath, ExpressionConstants.VAR_FOCUS);
}
model.registerMappingRelation(sources, targetItem, mapping);
}
private DataItem resolveSourceItem(@NotNull DataModel model, @NotNull ResourceDataItem currentItem,
@NotNull MappingType mapping, @NotNull VariableBindingDefinitionType sourceDecl, @Nullable QName defaultVariable) {
// todo from the description
return resolveSourceItem(model, currentItem, mapping, sourceDecl.getPath().getItemPath(), defaultVariable);
}
// for outbound (but sometimes also inbound) mappings
@NotNull
private DataItem resolveSourceItem(@NotNull DataModel model, @NotNull ResourceDataItem currentItem,
@NotNull MappingType mapping, @NotNull ItemPath path, @Nullable QName defaultVariable) {
if (!(path.first() instanceof NameItemPathSegment)) {
LOGGER.warn("Probably incorrect path ({}) - does not start with a name - skipping", path);
return createAdHocDataItem(model, path);
}
QName varName;
ItemPath itemPath;
NameItemPathSegment firstNameSegment = (NameItemPathSegment) path.first();
if (firstNameSegment.isVariable()) {
varName = firstNameSegment.getName();
itemPath = path.tail();
} else {
if (defaultVariable == null) {
LOGGER.warn("No default variable for mapping source");
return createAdHocDataItem(model, path);
}
varName = defaultVariable;
itemPath = path;
}
if (QNameUtil.match(ExpressionConstants.VAR_ACCOUNT, varName)) {
return resolveResourceItem(model, currentItem, itemPath);
} else if (QNameUtil.match(ExpressionConstants.VAR_USER, varName)) {
return model.resolveRepositoryItem(UserType.class, itemPath);
} else if (QNameUtil.match(ExpressionConstants.VAR_ACTOR, varName)) {
return model.resolveRepositoryItem(UserType.class, itemPath); // TODO
} else if (QNameUtil.match(ExpressionConstants.VAR_FOCUS, varName)) {
Class<? extends ObjectType> guessedClass = guessFocusClass(currentItem.getResourceOid(), currentItem.getKind(), currentItem.getIntent());
DataItem item = model.resolveRepositoryItem(guessedClass, itemPath);
if (item != null) {
return item;
}
// TODO guess e.g. by item existence in schema
LOGGER.warn("Couldn't resolve {} in $focus", path);
} else if (QNameUtil.match(ExpressionConstants.VAR_INPUT, varName)) {
return currentItem;
} else {
LOGGER.warn("Unsupported variable {} in {}", varName, path);
}
return createAdHocDataItem(model, path);
}
private DataItem createAdHocDataItem(DataModel model, ItemPath path) {
return new AdHocDataItem(path);
}
// currently for inbounds only
@NotNull
private DataItem resolveTargetItem(@NotNull DataModel model, @NotNull ResourceDataItem currentItem,
@NotNull MappingType mapping, @NotNull VariableBindingDefinitionType targetDecl, @Nullable QName defaultVariable) {
// todo from the description
return resolveTargetItem(model, currentItem, mapping, targetDecl.getPath().getItemPath(), defaultVariable);
}
// currently for inbounds only
@NotNull
private DataItem resolveTargetItem(@NotNull DataModel model, @NotNull ResourceDataItem currentItem,
@NotNull MappingType mapping, @NotNull ItemPath path, @Nullable QName defaultVariable) {
if (!(path.first() instanceof NameItemPathSegment)) {
LOGGER.warn("Probably incorrect path ({}) - does not start with a name - skipping", path);
return createAdHocDataItem(model, path);
}
QName varName;
ItemPath itemPath;
NameItemPathSegment firstNameSegment = (NameItemPathSegment) path.first();
if (firstNameSegment.isVariable()) {
varName = firstNameSegment.getName();
itemPath = path.tail();
} else {
if (defaultVariable == null) {
LOGGER.warn("No default variable for mapping target");
return createAdHocDataItem(model, path);
}
varName = defaultVariable;
itemPath = path;
}
if (QNameUtil.match(ExpressionConstants.VAR_ACCOUNT, varName)) {
return resolveResourceItem(model, currentItem, itemPath); // does make sense?
} else if (QNameUtil.match(ExpressionConstants.VAR_USER, varName)) {
return model.resolveRepositoryItem(UserType.class, itemPath);
} else if (QNameUtil.match(ExpressionConstants.VAR_ACTOR, varName)) {
return model.resolveRepositoryItem(UserType.class, itemPath); // TODO
} else if (QNameUtil.match(ExpressionConstants.VAR_FOCUS, varName)) {
Class<? extends ObjectType> guessedClass = guessFocusClass(currentItem.getResourceOid(), currentItem.getKind(), currentItem.getIntent());
DataItem item = model.resolveRepositoryItem(guessedClass, itemPath);
if (item != null) {
return item;
}
// TODO guess e.g. by item existence in schema
LOGGER.warn("Couldn't resolve {} in $focus", path);
} else if (QNameUtil.match(ExpressionConstants.VAR_INPUT, varName)) {
return currentItem; // does make sense?
} else {
LOGGER.warn("Unsupported variable {} in {}", varName, path);
}
return createAdHocDataItem(model, path);
}
private Class<? extends ObjectType> guessFocusClass(@NotNull String resourceOid, @NotNull ShadowKindType kind, @NotNull String intent) {
// TODO use synchronization as well
switch (kind) {
case ACCOUNT: return UserType.class;
case ENTITLEMENT: return RoleType.class;
case GENERIC: return OrgType.class;
}
throw new IllegalStateException();
}
private ResourceDataItem resolveResourceItem(DataModel model, ResourceDataItem currentItem, ItemPath path) {
return model.findResourceItem(currentItem.getResourceOid(), currentItem.getKind(), currentItem.getIntent(),
currentItem.getObjectClassName(), path);
}
private void processOutboundMapping(@NotNull DataModel model, @NotNull ResourceDataItem targetItem, @NotNull MappingType mapping,
@Nullable ItemPath defaultSourceItemPath) {
LOGGER.debug("Processing outbound mapping: {} for {}", mapping, targetItem);
List<DataItem> sources = new ArrayList<>();
for (VariableBindingDefinitionType sourceDecl : mapping.getSource()) {
LOGGER.debug(" - src: {}", sourceDecl.getPath());
DataItem sourceItem = resolveSourceItem(model, targetItem, mapping, sourceDecl, ExpressionConstants.VAR_FOCUS);
sources.add(sourceItem);
}
if (defaultSourceItemPath != null) {
DataItem defaultSource = resolveSourceItem(model, targetItem, mapping, defaultSourceItemPath, ExpressionConstants.VAR_FOCUS);
if (!sources.contains(defaultSource)) {
sources.add(defaultSource);
}
}
VariableBindingDefinitionType targetDecl = mapping.getTarget();
if (targetDecl != null) {
LOGGER.warn(" - ignoring target (mapping is outbound): {}; using {} instead", targetDecl.getPath(), targetItem);
}
model.registerMappingRelation(sources, targetItem, mapping);
}
// TODO move to appropriate place
private QName getObjectClassName(RefinedObjectClassDefinition def) {
return def != null ? def.getTypeName() : null;
}
}