/* * 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.prism; 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.prism.xjc.PrismForJAXBUtil; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.DebugDumpable; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.PrettyPrinter; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SystemException; import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import javax.xml.namespace.QName; import com.evolveum.prism.xml.ns._public.query_3.SearchFilterType; import com.evolveum.prism.xml.ns._public.types_3.EvaluationTimeType; import com.evolveum.prism.xml.ns._public.types_3.PolyStringType; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Radovan Semancik */ public class PrismReferenceValue extends PrismValue implements DebugDumpable, Serializable { private static final long serialVersionUID = 1L; private static final QName F_OID = new QName(PrismConstants.NS_TYPES, "oid"); private static final QName F_TYPE = new QName(PrismConstants.NS_TYPES, "type"); private static final QName F_RELATION = new QName(PrismConstants.NS_TYPES, "relation"); private String oid = null; private PrismObject<?> object = null; private QName targetType = null; private QName relation = null; private String description = null; private SearchFilterType filter = null; private EvaluationTimeType resolutionTime; private PolyString targetName = null; private Referencable referencable; public PrismReferenceValue() { this(null,null,null); } public PrismReferenceValue(String oid) { this(oid, null, null); } public PrismReferenceValue(String oid, QName targetType) { this(oid, null, null); this.targetType = targetType; } public PrismReferenceValue(String oid, OriginType type, Objectable source) { super(type,source); this.oid = oid; } /** * OID of the object that this reference refers to (reference target). * * May return null, but the reference is in that case incomplete and * unusable. * * @return the target oid */ public String getOid() { if (oid != null) { return oid; } if (object != null) { return object.getOid(); } return null; } public void setOid(String oid) { checkMutability(); this.oid = oid; } public PrismObject getObject() { return object; } public void setObject(PrismObject object) { checkMutability(); this.object = object; } /** * Returns XSD type of the object that this reference refers to. It may be * used in XPath expressions and similar filters. * * May return null if the type name is not set. * * @return the target type name */ public QName getTargetType() { if (targetType != null) { return targetType; } if (object != null && object.getDefinition() != null) { return object.getDefinition().getTypeName(); } return null; } public void setTargetType(QName targetType) { setTargetType(targetType, false); } /** * @param targetType * @param allowEmptyNamespace This is an ugly hack. See comment in DOMUtil.validateNonEmptyQName. */ public void setTargetType(QName targetType, boolean allowEmptyNamespace) { checkMutability(); // Null value is OK if (targetType != null) { // But non-empty is not .. Itemable item = getParent(); DOMUtil.validateNonEmptyQName(targetType, " in target type in reference "+ (item == null ? "(unknown)" : item.getElementName()), allowEmptyNamespace); } this.targetType = targetType; } /** * Returns cached name of the target object. * This is a ephemeral value. It is usually not stored. * It may be computed at object retrieval time or it may not be present at all. * This is NOT an authoritative information. Setting it or changing it will * not influence the reference meaning. OID is the only authoritative linking * mechanism. * @return cached name of the target object. */ public PolyString getTargetName() { if (targetName != null) { return targetName; } if (object != null) { return object.getName(); } return null; } public void setTargetName(PolyString name) { checkMutability(); this.targetName = name; } public void setTargetName(PolyStringType name) { checkMutability(); if (name == null) { this.targetName = null; } else { this.targetName = name.toPolyString(); } } // The PRV (this object) should have a parent with a prism context public Class<Objectable> getTargetTypeCompileTimeClass() { PrismContext prismContext = getPrismContext(); return prismContext != null ? getTargetTypeCompileTimeClass(prismContext) : null; } public Class<Objectable> getTargetTypeCompileTimeClass(PrismContext prismContext) { QName type = getTargetType(); if (type == null) { return null; } else { PrismObjectDefinition<Objectable> objDef = prismContext.getSchemaRegistry().findObjectDefinitionByType(type); return objDef != null ? objDef.getCompileTimeClass() : null; } } public QName getRelation() { return relation; } public void setRelation(QName relation) { checkMutability(); this.relation = relation; } public String getDescription() { return description; } public void setDescription(String description) { checkMutability(); this.description = description; } public SearchFilterType getFilter() { return filter; } public void setFilter(SearchFilterType filter) { checkMutability(); this.filter = filter; } public EvaluationTimeType getResolutionTime() { return resolutionTime; } public void setResolutionTime(EvaluationTimeType resolutionTime) { checkMutability(); this.resolutionTime = resolutionTime; } @Override public PrismReferenceDefinition getDefinition() { return (PrismReferenceDefinition) super.getDefinition(); } @Override public boolean isRaw() { // Reference value cannot be raw return false; } @Override public Object find(ItemPath path) { if (path == null || path.isEmpty()) { return this; } ItemPathSegment first = path.first(); if (!(first instanceof NameItemPathSegment)) { throw new IllegalArgumentException("Attempt to resolve inside the reference value using a non-name path "+path+" in "+this); } QName subName = ((NameItemPathSegment)first).getName(); if (compareLocalPart(F_OID,subName)) { return this.getOid(); } else if (compareLocalPart(F_TYPE,subName)) { return this.getTargetType(); } else if (compareLocalPart(F_RELATION,subName)) { return this.getRelation(); } else { throw new IllegalArgumentException("Attempt to resolve inside the reference value using a unrecognized path "+path+" in "+this); } } @Override public <IV extends PrismValue,ID extends ItemDefinition> PartiallyResolvedItem<IV,ID> findPartial(ItemPath path) { if (path == null || path.isEmpty()) { return new PartiallyResolvedItem<IV,ID>((Item<IV,ID>)getParent(), null); } return new PartiallyResolvedItem<IV,ID>((Item<IV,ID>)getParent(), path); } private boolean compareLocalPart(QName a, QName b) { if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } return a.getLocalPart().equals(b.getLocalPart()); } @Override public void applyDefinition(ItemDefinition definition, boolean force) throws SchemaException { if (!(definition instanceof PrismReferenceDefinition)) { throw new IllegalArgumentException("Cannot apply "+definition+" to a reference value"); } applyDefinition((PrismReferenceDefinition)definition, force); } public void applyDefinition(PrismReferenceDefinition definition, boolean force) throws SchemaException { super.applyDefinition(definition, force); if (object == null) { return; } if (object.getDefinition() != null && !force) { return; } PrismContext prismContext = definition.getPrismContext(); QName targetTypeName = definition.getTargetTypeName(); if (targetTypeName == null) { throw new SchemaException("Cannot apply definition to composite object in reference "+getParent() +": the target type name is not specified in the reference schema"); } PrismObjectDefinition<? extends Objectable> objectDefinition = prismContext.getSchemaRegistry().findObjectDefinitionByCompileTimeClass(object.getCompileTimeClass()); if (objectDefinition == null) { objectDefinition = prismContext.getSchemaRegistry().findObjectDefinitionByType(targetTypeName); } if (objectDefinition == null) { throw new SchemaException("Cannot apply definition to composite object in reference "+getParent() +": no definition for object type "+targetTypeName); } // this should do it object.applyDefinition((PrismObjectDefinition)objectDefinition, force); } @Override public void recompute(PrismContext prismContext) { // Nothing to do } @Override public void checkConsistenceInternal(Itemable rootItem, boolean requireDefinitions, boolean prohibitRaw, ConsistencyCheckScope scope) { if (!scope.isThorough()) { return; } ItemPath myPath = getPath(); if (StringUtils.isBlank(oid) && object == null && filter == null) { boolean mayBeEmpty = false; if (getParent() != null && getParent().getDefinition() != null) { ItemDefinition itemDefinition = getParent().getDefinition(); if (itemDefinition instanceof PrismReferenceDefinition) { PrismReferenceDefinition prismReferenceDefinition = (PrismReferenceDefinition) itemDefinition; mayBeEmpty = prismReferenceDefinition.isComposite(); } } if (!mayBeEmpty) { throw new IllegalStateException("Neither OID, object nor filter specified in reference value "+this+" ("+myPath+" in "+rootItem+")"); } } if (object != null) { try { object.checkConsistence(); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(e.getMessage()+" in reference "+myPath+" in "+rootItem, e); } catch (IllegalStateException e) { throw new IllegalStateException(e.getMessage()+" in reference "+myPath+" in "+rootItem, e); } } } @Override public boolean isEmpty() { return oid == null && object == null && filter == null; } /** * Returns a version of this value that is canonical, that means it has the minimal form. * E.g. it will have only OID and no object. */ public PrismReferenceValue toCanonical() { PrismReferenceValue can = new PrismReferenceValue(); can.setOid(getOid()); // do NOT copy object can.setTargetType(getTargetType()); can.setRelation(getRelation()); can.setFilter(getFilter()); can.setResolutionTime(getResolutionTime()); can.setDescription(getDescription()); return can; } @Override public boolean equalsComplex(PrismValue other, boolean ignoreMetadata, boolean isLiteral) { return other instanceof PrismReferenceValue && equalsComplex((PrismReferenceValue) other, ignoreMetadata, isLiteral); } public boolean equalsComplex(PrismReferenceValue other, boolean ignoreMetadata, boolean isLiteral) { if (!super.equalsComplex(other, ignoreMetadata, isLiteral)) { return false; } if (this.getOid() == null) { if (other.getOid() != null) return false; } else if (!this.getOid().equals(other.getOid())) return false; // Special handling: if both oids are null we need to compare embedded objects if (this.oid == null && other.oid == null) { if (this.object != null || other.object != null) { if (this.object == null || other.object == null) { // one is null the other is not return false; } if (!this.object.equals(other.object)) { return false; } } } if (!equalsTargetType(other)) { return false; } if (!relationsEquivalent(relation, other.relation)) { return false; } return true; } private boolean relationsEquivalent(QName r1, QName r2) { return QNameUtil.match(normalizedRelation(r1), normalizedRelation(r2)); } private QName normalizedRelation(QName r) { if (r != null) { return r; } PrismContext prismContext = getPrismContext(); return prismContext != null ? prismContext.getDefaultRelation() : null; } private boolean equalsTargetType(PrismReferenceValue other) { QName otherTargetType = other.getTargetType(); if (otherTargetType == null && other.getDefinition() != null) { otherTargetType = other.getDefinition().getTargetTypeName(); } QName thisTargetType = this.getTargetType(); if (thisTargetType == null && this.getDefinition() != null) { thisTargetType = this.getDefinition().getTargetTypeName(); } return QNameUtil.match(thisTargetType, otherTargetType); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; PrismReferenceValue other = (PrismReferenceValue) obj; return equalsComplex(other, false, false); } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((oid == null) ? 0 : oid.hashCode()); result = prime * result + ((targetType == null) ? 0 : targetType.hashCode()); result = prime * result + ((relation == null) ? 0 : relation.hashCode()); return result; } @Override public boolean representsSameValue(PrismValue other, boolean lax) { if (other instanceof PrismReferenceValue) { return representsSameValue((PrismReferenceValue)other); } else { return false; } } public boolean representsSameValue(PrismReferenceValue other) { if (this.getOid() != null && other.getOid() != null) { return this.getOid().equals(other.getOid()) && relationsEquivalent(this.getRelation(), other.getRelation()); } return false; } public static PrismReferenceValue createFromTarget(PrismObject<?> refTarget) { PrismReferenceValue refVal = new PrismReferenceValue(refTarget.getOid()); refVal.setObject(refTarget); if (refTarget.getDefinition() != null) { refVal.setTargetType(refTarget.getDefinition().getTypeName()); } return refVal; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("PRV("); if (object == null) { sb.append("oid=").append(oid); sb.append(", targetType=").append(PrettyPrinter.prettyPrint(targetType)); if (targetName != null) { sb.append(", targetName=").append(PrettyPrinter.prettyPrint(targetName.getOrig())); } } else { sb.append("object=").append(object); } if (getRelation() != null) { sb.append(", relation=").append(PrettyPrinter.prettyPrint(getRelation())); } if (getOriginType() != null) { sb.append(", type=").append(getOriginType()); } if (getOriginObject() != null) { sb.append(", source=").append(getOriginObject()); } if (filter != null) { sb.append(", filter"); } if (resolutionTime != null) { sb.append(", resolutionTime=").append(resolutionTime); } sb.append(")"); return sb.toString(); } public Referencable asReferencable() { if (referencable != null) { return referencable; } Itemable parent = getParent(); if (parent != null) { QName xsdType = parent.getDefinition().getTypeName(); Class clazz = getPrismContext().getSchemaRegistry().getCompileTimeClass(xsdType); if (clazz != null) { try { referencable = (Referencable) clazz.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new SystemException("Couldn't create jaxb object instance of '" + clazz + "': " + e.getMessage(), e); } } referencable.setupReferenceValue(this); } // A hack, just to avoid crashes. TODO think about this! return new Referencable() { PrismReferenceValue referenceValue = PrismReferenceValue.this; @Override public PrismReferenceValue asReferenceValue() { return referenceValue; } @Override public void setupReferenceValue(PrismReferenceValue value) { referenceValue = value; } @Override public String getOid() { return referenceValue.getOid(); } @Override public QName getType() { return referenceValue.getTargetType(); } @Override public PolyStringType getTargetName() { return PrismForJAXBUtil.getReferenceTargetName(referenceValue); } @Override public QName getRelation() { return referenceValue.getRelation(); } @Override public String getDescription() { return referenceValue.getDescription(); } @Override public EvaluationTimeType getResolutionTime() { return referenceValue.getResolutionTime(); } @Override public SearchFilterType getFilter() { SearchFilterType filter = new SearchFilterType(); filter.setFilterClauseXNode(PrismForJAXBUtil.getReferenceFilterClauseXNode(referenceValue)); return filter; } }; } @NotNull public static List<Referencable> asReferencables(@NotNull Collection<PrismReferenceValue> values) { return values.stream().map(prv -> prv.asReferencable()).collect(Collectors.toList()); } @NotNull public static List<PrismReferenceValue> asReferenceValues(@NotNull Collection<? extends Referencable> referencables) { return referencables.stream().map(ref -> ref.asReferenceValue()).collect(Collectors.toList()); } @Override public String debugDump() { return toString(); } @Override public String debugDump(int indent) { return debugDump(indent, false); } public String debugDump(int indent, boolean expandObject) { StringBuilder sb = new StringBuilder(); DebugUtil.indentDebugDump(sb, indent); sb.append(toString()); if (expandObject && object != null) { sb.append("\n"); sb.append(object.debugDump(indent + 1)); } return sb.toString(); } @Override public PrismReferenceValue clone() { return clone(true); } public PrismReferenceValue clone(boolean copyFullObject) { PrismReferenceValue clone = new PrismReferenceValue(getOid(), getOriginType(), getOriginObject()); copyValues(clone, copyFullObject); return clone; } protected void copyValues(PrismReferenceValue clone, boolean copyFullObject) { super.copyValues(clone); clone.targetType = this.targetType; if (this.object != null && copyFullObject) { clone.object = this.object.clone(); } clone.description = this.description; clone.filter = this.filter; clone.resolutionTime = this.resolutionTime; clone.relation = this.relation; clone.targetName = this.targetName; } @Override public boolean match(PrismValue otherValue) { return equalsRealValue(otherValue); } /* (non-Javadoc) * @see com.evolveum.midpoint.prism.PrismValue#getHumanReadableDump() */ @Override public String toHumanReadableString() { StringBuilder sb = new StringBuilder(); sb.append("oid=").append(oid); if (getTargetType() != null) { sb.append("("); sb.append(DebugUtil.formatElementName(getTargetType())); sb.append(")"); } if (targetName != null) { sb.append("('").append(targetName).append("')"); } if (getRelation() != null) { sb.append("["); sb.append(getRelation().getLocalPart()); sb.append("]"); } if (getObject() != null) { sb.append('*'); } return sb.toString(); } @Override public Class<?> getRealClass() { return Referencable.class; } @SuppressWarnings("unchecked") @Nullable @Override public Referencable getRealValue() { return asReferencable(); } public static boolean containsOid(Collection<PrismReferenceValue> values, @NotNull String oid) { return values.stream().anyMatch(v -> oid.equals(v.getOid())); } }