/* * Copyright (c) 2010-2013 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.notifications.impl.formatters; import com.evolveum.midpoint.notifications.impl.NotificationFunctionsImpl; import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.path.IdItemPathSegment; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.path.ItemPathSegment; import com.evolveum.midpoint.prism.path.NameItemPathSegment; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.schema.GetOperationOptions; import com.evolveum.midpoint.schema.SelectorOptions; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.ValueDisplayUtil; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.PrettyPrinter; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.logging.LoggingUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowAssociationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; import org.apache.commons.lang.Validate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import javax.xml.namespace.QName; import java.util.*; /** * @author mederly */ @Component public class TextFormatter { @Autowired(required = true) @Qualifier("cacheRepositoryService") private transient RepositoryService cacheRepositoryService; private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle( SchemaConstants.SCHEMA_LOCALIZATION_PROPERTIES_RESOURCE_BASE_PATH); private static final Trace LOGGER = TraceManager.getTrace(TextFormatter.class); public String formatObjectModificationDelta(ObjectDelta<? extends Objectable> objectDelta, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { return formatObjectModificationDelta(objectDelta, hiddenPaths, showOperationalAttributes, null, null); } // objectOld and objectNew are used for explaining changed container values, e.g. assignment[1]/tenantRef (see MID-2047) // if null, they are ignored public String formatObjectModificationDelta(ObjectDelta<? extends Objectable> objectDelta, List<ItemPath> hiddenPaths, boolean showOperationalAttributes, PrismObject objectOld, PrismObject objectNew) { Validate.notNull(objectDelta, "objectDelta is null"); Validate.isTrue(objectDelta.isModify(), "objectDelta is not a modification delta"); PrismObjectDefinition objectDefinition; if (objectNew != null && objectNew.getDefinition() != null) { objectDefinition = objectNew.getDefinition(); } else if (objectOld != null && objectOld.getDefinition() != null) { objectDefinition = objectOld.getDefinition(); } else { objectDefinition = null; } if (LOGGER.isTraceEnabled()) { LOGGER.trace("formatObjectModificationDelta: objectDelta = " + objectDelta.debugDump() + ", hiddenPaths = " + PrettyPrinter.prettyPrint(hiddenPaths)); } StringBuilder retval = new StringBuilder(); List<ItemDelta> toBeDisplayed = filterAndOrderItemDeltas(objectDelta, hiddenPaths, showOperationalAttributes); for (ItemDelta itemDelta : toBeDisplayed) { retval.append(" - "); retval.append(getItemDeltaLabel(itemDelta, objectDefinition)); retval.append(":\n"); formatItemDeltaContent(retval, itemDelta, hiddenPaths, showOperationalAttributes); } explainPaths(retval, toBeDisplayed, objectDefinition, objectOld, objectNew, hiddenPaths, showOperationalAttributes); return retval.toString(); } private void explainPaths(StringBuilder sb, List<ItemDelta> deltas, PrismObjectDefinition objectDefinition, PrismObject objectOld, PrismObject objectNew, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { if (objectOld == null && objectNew == null) { return; // no data - no point in trying } boolean first = true; List<ItemPath> alreadyExplained = new ArrayList<>(); for (ItemDelta itemDelta : deltas) { ItemPath pathToExplain = getPathToExplain(itemDelta); if (pathToExplain == null || ItemPath.containsSubpathOrEquivalent(alreadyExplained, pathToExplain)) { continue; // null or already processed } PrismObject source = null; Object item = null; if (objectNew != null) { item = objectNew.find(pathToExplain); source = objectNew; } if (item == null && objectOld != null) { item = objectOld.find(pathToExplain); source = objectOld; } if (item == null) { LOGGER.warn("Couldn't find {} in {} nor {}, no explanation could be created.", new Object[] {pathToExplain, objectNew, objectOld}); continue; } if (first) { sb.append("\nNotes:\n"); first = false; } String label = getItemPathLabel(pathToExplain, itemDelta.getDefinition(), objectDefinition); // the item should be a PrismContainerValue if (item instanceof PrismContainerValue) { sb.append(" - ").append(label).append(":\n"); formatContainerValue(sb, " ", (PrismContainerValue) item, false, hiddenPaths, showOperationalAttributes); } else { LOGGER.warn("{} in {} was expected to be a PrismContainerValue; it is {} instead", new Object[]{pathToExplain, source, item.getClass()}); if (item instanceof PrismContainer) { formatPrismContainer(sb, " ", (PrismContainer) item, false, hiddenPaths, showOperationalAttributes); } else if (item instanceof PrismReference) { formatPrismReference(sb, " ", (PrismReference) item, false); } else if (item instanceof PrismProperty) { formatPrismProperty(sb, " ", (PrismProperty) item); } else { sb.append("Unexpected item: ").append(item).append("\n"); } } alreadyExplained.add(pathToExplain); } } private void formatItemDeltaContent(StringBuilder sb, ItemDelta itemDelta, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { formatItemDeltaValues(sb, "ADD", itemDelta.getValuesToAdd(), false, hiddenPaths, showOperationalAttributes); formatItemDeltaValues(sb, "DELETE", itemDelta.getValuesToDelete(), true, hiddenPaths, showOperationalAttributes); formatItemDeltaValues(sb, "REPLACE", itemDelta.getValuesToReplace(), false, hiddenPaths, showOperationalAttributes); } private void formatItemDeltaValues(StringBuilder sb, String type, Collection<? extends PrismValue> values, boolean mightBeRemoved, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { if (values != null) { for (PrismValue prismValue : values) { sb.append(" - " + type + ": "); String prefix = " "; formatPrismValue(sb, prefix, prismValue, mightBeRemoved, hiddenPaths, showOperationalAttributes); if (!(prismValue instanceof PrismContainerValue)) { // container values already end with newline sb.append("\n"); } } } } // todo - should each hiddenAttribute be prefixed with something like F_ATTRIBUTE? Currently it should not be. public String formatAccountAttributes(ShadowType shadowType, List<ItemPath> hiddenAttributes, boolean showOperationalAttributes) { Validate.notNull(shadowType, "shadowType is null"); StringBuilder retval = new StringBuilder(); if (shadowType.getAttributes() != null) { formatContainerValue(retval, "", shadowType.getAttributes().asPrismContainerValue(), false, hiddenAttributes, showOperationalAttributes); } if (shadowType.getCredentials() != null) { formatContainerValue(retval, "", shadowType.getCredentials().asPrismContainerValue(), false, hiddenAttributes, showOperationalAttributes); } if (shadowType.getActivation() != null) { formatContainerValue(retval, "", shadowType.getActivation().asPrismContainerValue(), false, hiddenAttributes, showOperationalAttributes); } if (shadowType.getAssociation() != null) { boolean first = true; for (ShadowAssociationType shadowAssociationType : shadowType.getAssociation()) { if (first) { first = false; retval.append("\n"); } retval.append("Association:\n"); formatContainerValue(retval, " ", shadowAssociationType.asPrismContainerValue(), false, hiddenAttributes, showOperationalAttributes); retval.append("\n"); } } return retval.toString(); } public String formatObject(PrismObject object, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { Validate.notNull(object, "object is null"); StringBuilder retval = new StringBuilder(); formatContainerValue(retval, "", object.getValue(), false, hiddenPaths, showOperationalAttributes); return retval.toString(); } private void formatPrismValue(StringBuilder sb, String prefix, PrismValue prismValue, boolean mightBeRemoved, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { if (prismValue instanceof PrismPropertyValue) { sb.append(ValueDisplayUtil.toStringValue((PrismPropertyValue) prismValue)); } else if (prismValue instanceof PrismReferenceValue) { sb.append(formatReferenceValue((PrismReferenceValue) prismValue, mightBeRemoved)); } else if (prismValue instanceof PrismContainerValue) { sb.append("\n"); formatContainerValue(sb, prefix, (PrismContainerValue) prismValue, mightBeRemoved, hiddenPaths, showOperationalAttributes); } else { sb.append("Unexpected PrismValue type: "); sb.append(prismValue); LOGGER.error("Unexpected PrismValue type: " + prismValue.getClass() + ": " + prismValue); } } private void formatContainerValue(StringBuilder sb, String prefix, PrismContainerValue containerValue, boolean mightBeRemoved, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { // sb.append("Container of type " + containerValue.getParent().getDefinition().getTypeName()); // sb.append("\n"); List<Item> toBeDisplayed = filterAndOrderItems(containerValue.getItems(), hiddenPaths, showOperationalAttributes); for (Item item : toBeDisplayed) { if (item instanceof PrismProperty) { formatPrismProperty(sb, prefix, item); } else if (item instanceof PrismReference) { formatPrismReference(sb, prefix, item, mightBeRemoved); } else if (item instanceof PrismContainer) { formatPrismContainer(sb, prefix, item, mightBeRemoved, hiddenPaths, showOperationalAttributes); } else { sb.append("Unexpected Item type: "); sb.append(item); sb.append("\n"); LOGGER.error("Unexpected Item type: " + item.getClass() + ": " + item); } } } private void formatPrismContainer(StringBuilder sb, String prefix, Item item, boolean mightBeRemoved, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { for (PrismContainerValue subContainerValue : ((PrismContainer<? extends Containerable>) item).getValues()) { sb.append(prefix); sb.append(" - "); sb.append(getItemLabel(item)); if (subContainerValue.getId() != null) { sb.append(" #").append(subContainerValue.getId()); } sb.append(":\n"); String prefixSubContainer = prefix + " "; formatContainerValue(sb, prefixSubContainer, subContainerValue, mightBeRemoved, hiddenPaths, showOperationalAttributes); } } private void formatPrismReference(StringBuilder sb, String prefix, Item item, boolean mightBeRemoved) { sb.append(prefix); sb.append(" - "); sb.append(getItemLabel(item)); sb.append(": "); if (item.size() > 1) { for (PrismReferenceValue referenceValue : ((PrismReference) item).getValues()) { sb.append("\n"); sb.append(prefix + " - "); sb.append(formatReferenceValue(referenceValue, mightBeRemoved)); } } else if (item.size() == 1) { sb.append(formatReferenceValue(((PrismReference) item).getValue(0), mightBeRemoved)); } sb.append("\n"); } private void formatPrismProperty(StringBuilder sb, String prefix, Item item) { sb.append(prefix); sb.append(" - "); sb.append(getItemLabel(item)); sb.append(": "); if (item.size() > 1) { for (PrismPropertyValue propertyValue : ((PrismProperty<? extends Object>) item).getValues()) { sb.append("\n"); sb.append(prefix + " - "); sb.append(ValueDisplayUtil.toStringValue(propertyValue)); } } else if (item.size() == 1) { sb.append(ValueDisplayUtil.toStringValue(((PrismProperty<? extends Object>) item).getValue(0))); } sb.append("\n"); } private String formatReferenceValue(PrismReferenceValue value, boolean mightBeRemoved) { OperationResult result = new OperationResult("dummy"); PrismObject<? extends ObjectType> object = value.getObject(); if (object == null) { object = getPrismObject(value.getOid(), mightBeRemoved, result); } String qualifier = ""; if (object != null && object.asObjectable() instanceof ShadowType) { ShadowType shadowType = (ShadowType) object.asObjectable(); ResourceType resourceType = shadowType.getResource(); if (resourceType == null) { PrismObject<? extends ObjectType> resource = getPrismObject(shadowType.getResourceRef().getOid(), false, result); if (resource != null) { resourceType = (ResourceType) resource.asObjectable(); } } if (resourceType != null) { qualifier = " on " + resourceType.getName(); } else { qualifier = " on resource " + shadowType.getResourceRef().getOid(); } } String referredObjectIdentification; if (object != null) { referredObjectIdentification = PolyString.getOrig(object.asObjectable().getName()) + " (" + object.toDebugType() + ")" + qualifier; } else { String nameOrOid = value.getTargetName() != null ? value.getTargetName().getOrig() : value.getOid(); if (mightBeRemoved) { referredObjectIdentification = "(cannot display the actual name of " + localPart(value.getTargetType()) + ":" + nameOrOid + ", as it might be already removed)"; } else { referredObjectIdentification = localPart(value.getTargetType()) + ":" + nameOrOid; } } return value.getRelation() != null ? referredObjectIdentification + " [" + value.getRelation().getLocalPart() + "]" : referredObjectIdentification; } private PrismObject<? extends ObjectType> getPrismObject(String oid, boolean mightBeRemoved, OperationResult result) { try { Collection<SelectorOptions<GetOperationOptions>> options = SelectorOptions.createCollection(GetOperationOptions.createReadOnly()); return cacheRepositoryService.getObject(ObjectType.class, oid, options, result); } catch (ObjectNotFoundException e) { if (!mightBeRemoved) { LoggingUtils.logException(LOGGER, "Couldn't resolve reference when displaying object name within a notification (it might be already removed)", e); } else { } } catch (SchemaException e) { LoggingUtils.logException(LOGGER, "Couldn't resolve reference when displaying object name within a notification", e); } return null; } private String localPartOfType(Item item) { if (item.getDefinition() != null) { return localPart(item.getDefinition().getTypeName()); } else { return null; } } private String localPart(QName qname) { return qname == null ? null : qname.getLocalPart(); } // we call this on filtered list of item deltas - all of they have definition set private String getItemDeltaLabel(ItemDelta itemDelta, PrismObjectDefinition objectDefinition) { return getItemPathLabel(itemDelta.getPath(), itemDelta.getDefinition(), objectDefinition); } private String getItemPathLabel(ItemPath path, Definition deltaDefinition, PrismObjectDefinition objectDefinition) { NameItemPathSegment lastNamedSegment = path.lastNamed(); StringBuilder sb = new StringBuilder(); for (ItemPathSegment segment : path.getSegments()) { if (segment instanceof NameItemPathSegment) { if (sb.length() > 0) { sb.append("/"); } Definition itemDefinition; if (objectDefinition == null) { if (segment == lastNamedSegment) { // definition for last segment is the definition taken from delta itemDefinition = deltaDefinition; // this may be null but we don't care } else { itemDefinition = null; // definitions for previous segments are unknown } } else { // todo we could make this iterative (resolving definitions while walking down the path); but this is definitely simpler to implement and debug :) itemDefinition = objectDefinition.findItemDefinition(path.allUpToIncluding(segment)); } if (itemDefinition != null && itemDefinition.getDisplayName() != null) { sb.append(resolve(itemDefinition.getDisplayName())); } else { sb.append(((NameItemPathSegment) segment).getName().getLocalPart()); } } else if (segment instanceof IdItemPathSegment) { sb.append("[").append(((IdItemPathSegment) segment).getId()).append("]"); } } return sb.toString(); } private String resolve(String key) { if (key != null && RESOURCE_BUNDLE.containsKey(key)) { return RESOURCE_BUNDLE.getString(key); } else { return key; } } // we call this on filtered list of item deltas - all of they have definition set private ItemPath getPathToExplain(ItemDelta itemDelta) { ItemPath path = itemDelta.getPath(); for (int i = 0; i < path.size(); i++) { ItemPathSegment segment = path.getSegments().get(i); if (segment instanceof IdItemPathSegment) { if (i < path.size()-1 || itemDelta.isDelete()) { return path.allUpToIncluding(i); } else { // this means that the path ends with [id] segment *and* the value(s) are // only added and deleted, i.e. they are shown in the delta anyway // (actually it is questionable whether path in delta can end with [id] segment, // but we test for this case just to be sure) return null; } } } return null; } private List<ItemDelta> filterAndOrderItemDeltas(ObjectDelta<? extends Objectable> objectDelta, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { List<ItemDelta> toBeDisplayed = new ArrayList<ItemDelta>(objectDelta.getModifications().size()); List<QName> noDefinition = new ArrayList<>(); for (ItemDelta itemDelta: objectDelta.getModifications()) { if (itemDelta.getDefinition() != null) { if ((showOperationalAttributes || !itemDelta.getDefinition().isOperational()) && !NotificationFunctionsImpl .isAmongHiddenPaths(itemDelta.getPath(), hiddenPaths)) { toBeDisplayed.add(itemDelta); } } else { noDefinition.add(itemDelta.getElementName()); } } if (!noDefinition.isEmpty()) { LOGGER.error("ItemDeltas for {} without definition - WILL NOT BE INCLUDED IN NOTIFICATION. Containing object delta:\n{}", noDefinition, objectDelta.debugDump()); } Collections.sort(toBeDisplayed, new Comparator<ItemDelta>() { @Override public int compare(ItemDelta delta1, ItemDelta delta2) { Integer order1 = delta1.getDefinition().getDisplayOrder(); Integer order2 = delta2.getDefinition().getDisplayOrder(); if (order1 != null && order2 != null) { return order1 - order2; } else if (order1 == null && order2 == null) { return 0; } else if (order1 == null) { return 1; } else { return -1; } } }); return toBeDisplayed; } // we call this on filtered list of items - all of them have definition set private String getItemLabel(Item item) { return item.getDefinition().getDisplayName() != null ? resolve(item.getDefinition().getDisplayName()) : item.getElementName().getLocalPart(); } private List<Item> filterAndOrderItems(List<Item> items, List<ItemPath> hiddenPaths, boolean showOperationalAttributes) { if (items == null) { return new ArrayList<>(); } List<Item> toBeDisplayed = new ArrayList<Item>(items.size()); List<QName> noDefinition = new ArrayList<>(); for (Item item : items) { if (item.getDefinition() != null) { boolean isHidden = NotificationFunctionsImpl.isAmongHiddenPaths(item.getPath(), hiddenPaths); if (!isHidden && (showOperationalAttributes || !item.getDefinition().isOperational())) { toBeDisplayed.add(item); } } else { noDefinition.add(item.getElementName()); } } if (!noDefinition.isEmpty()) { LOGGER.error("Items {} without definition - THEY WILL NOT BE INCLUDED IN NOTIFICATION.\nAll items:\n{}", noDefinition, DebugUtil.debugDump(items)); } Collections.sort(toBeDisplayed, new Comparator<Item>() { @Override public int compare(Item item1, Item item2) { Integer order1 = item1.getDefinition().getDisplayOrder(); Integer order2 = item2.getDefinition().getDisplayOrder(); if (order1 != null && order2 != null) { return order1 - order2; } else if (order1 == null && order2 == null) { return 0; } else if (order1 == null) { return 1; } else { return -1; } } }); return toBeDisplayed; } }