blob: d822fa25008d61858b7b750bdd3146155715fd8a [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.sdo.helper;
import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.helper.EqualityHelper;
import commonj.sdo.helper.HelperContext;
import commonj.sdo.impl.HelperProvider;
import java.util.Iterator;
import java.util.List;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.SDOProperty;
import org.eclipse.persistence.sdo.SDOSequence;
import org.eclipse.persistence.sdo.SDOType;
import org.eclipse.persistence.oxm.XMLRoot;
import org.eclipse.persistence.oxm.sequenced.Setting;
/**
* <p><b>Purpose</b>: A helper class for checking deep or shallow equality of DataObjects.</p>
* <p>ChangeSummary is not in scope for equality checking.
*
* @see org.eclipse.persistence.sdo.SDODataObject
* @since Oracle TopLink 11.1.1.0.0
*
*/
public class SDOEqualityHelper implements EqualityHelper {
/*
References:
SDO59-DeepCopy.doc
SDO_Ref_BiDir_Relationships_DesignSpec.doc
http://files.oraclecorp.com/content/MySharedFolders/ST%20Functional%20Specs/AS11gR1/TopLink/SDO/SDO_Ref_BiDir_Relationships_DesignSpec.doc
09/12/06 - Add bidirectional property support in compareProperty()
09/18/06 - remove shallow opposite property check - just return true if both objects are set and are DO's
01/29/07 - #5852525 handle null properties with isSet=true
04/11/07 - Implement Sequence functionality
*/
/** hold the context containing all helpers so that we can preserve inter-helper relationships */
private HelperContext aHelperContext;
/**
* INTERNAL:
* This default constructor must be used in conjunction with the setHelperContext() function.
* The custom constructor that takes a HelperContext parameter is recommended over this default constructor.
*/
public SDOEqualityHelper() {
}
/**
* Constructor that takes in a HelperContext instance that contains this equalityHelper.<br>
* This is the recommended constructor.
* @param aContext
*/
public SDOEqualityHelper(HelperContext aContext) {
aHelperContext = aContext;
}
/**
* <p>Two DataObjects are equalShallow if
* they have the same {@link DataObject#getType Type}
* and all their compared Properties are equal.
* The set of Properties compared are the
* {@link DataObject#getInstanceProperties() instance properties}
* where property.getType().isDataType() is true
* and property.getType() is not ChangeSummaryType.
* <br>Two of these Property values are equal if they are both not
* {@link DataObject#isSet(Property) set}, or set to an equal value
* dataObject1.get(property).equals(dataObject2.get(property))
* <br>If the type is a sequenced type, the sequence entries must be the same.
* For each entry x in the sequence where the property is used in the comparison,
* dataObject1.getSequence().getValue(x).equals(
* dataObject2.getSequence().getValue(x)) and
* dataObject1.getSequence().getProperty(x) ==
* dataObject2.getSequence().getProperty(x)
* must be true.
* </p>
* Returns true the objects have the same Type and all values of all compared Properties are equal.
* @param dataObject1 DataObject to be compared
* @param dataObject2 DataObject to be compared
* @return true the objects have the same Type and all values of all compared Properties are equal.
*/
@Override
public boolean equalShallow(DataObject dataObject1, DataObject dataObject2) {
return compareDataObjects(dataObject1, dataObject2, false);
}
/**
* <p>Two DataObjects are equal(Deep) if they are equalShallow,
* all their compared Properties are equal, and all reachable DataObjects in their
* graphs excluding containers are equal.
* The set of Properties compared are the
* {@link DataObject#getInstanceProperties() instance properties}
* where property.getType().isDataType() is false,
* and is not a container property, ie !property.getOpposite().isContainment()
* <br>Two of these Property values are equal if they are both not
* {@link DataObject#isSet(Property) set}, or all the DataObjects
* they refer to are {@link #equal(DataObject, DataObject) equal} in the
* context of dataObject1 and dataObject2.
* <br>Note that properties to a containing DataObject are not compared
* which means two DataObject trees can be equal even if their containers are not equal.
* <br>If the type is a sequenced type, the sequence entries must be the same.
* For each entry x in the sequence where the property is used in the comparison,
* equal(dataObject1.getSequence().getValue(x),
* dataObject2.getSequence().getValue(x)) and
* dataObject1.getSequence().getProperty(x) == dataObject2.getSequence().getProperty(x)
* must be true.
* </p><p>
* A DataObject directly or indirectly referenced by dataObject1 or dataObject2
* can only be equal to exactly one DataObject directly or indirectly referenced
* by dataObject1 or dataObject2, respectively.
* This ensures that dataObject1 and dataObject2 are equal if the graph formed by
* all their referenced DataObjects have the same shape.
* </p>
* Returns true if the trees of DataObjects are equal(Deep).
* @param dataObject1 DataObject to be compared
* @param dataObject2 DataObject to be compared
* @return true if the trees of DataObjects are equal(Deep).
*/
@Override
public boolean equal(DataObject dataObject1, DataObject dataObject2) {
return compareDataObjects(dataObject1, dataObject2, true);
}
/**
* INTERNAL:
* Separately, checks the declared properties and open content properties.
* @param dataObject1 the DataObject to be compared
* @param dataObject2 the DataObject to be compared
* @param isDeep if comparison is deep
* @return true if two DataObjects meet requirements of shallow equal or deep equal
*/
private boolean compareDataObjects(DataObject dataObject1, DataObject dataObject2, boolean isDeep) {
// Note that properties to a containing DataObject are not compared which
// means two DataObject trees can be equal even if their containers are not equal.
if (null == dataObject1) {
return dataObject2 == null;
}
if (null == dataObject2) {// dataObject1 is not null while dataObject2 is null
return false;
}
// they don't have the same type !! assumption is the same for now, but may be changed to use equals !!
if (dataObject1.getType() != dataObject2.getType()) {
return false;
} else {// type is the same for them
/**
* We have 2 strategies here - sequences or dataObjects first?
* Analysis: A sequence and its dataObject share a subset of element properties.
* The disjoint set for sequences includes the set of unstructured text settings.
* The disjoint set for dataObject includes all attribute properties.
*
* The choice of which to do first comes down to performance
*/
/**
* Compare sequences - only out of order settings and unstructured
* text will not be picked up by a dataObject comparison.
* There is no need to check sequenced state on both objects because they share the same SDOType instance.
*/
if (dataObject1.getType().isSequenced()) {
if (!compareSequences((SDOSequence) dataObject1.getSequence(), (SDOSequence) dataObject2.getSequence(), isDeep)) {
return false;
}
}
// First, compare properties that are not open content.
// Attribute property differences will not be picked up in the sequence comparison
if (!compare(dataObject1, dataObject2, isDeep, dataObject1.getType().getProperties())) {
return false;
}
// Second, compare open content properties
List properties_1 = ((SDODataObject)dataObject1)._getOpenContentProperties();
List properties_2 = ((SDODataObject)dataObject2)._getOpenContentProperties();
// different size of open content properties
if ((properties_1.size() != properties_2.size()) || !properties_1.containsAll(properties_2)) {
return false;
}
if (!compare(dataObject1, dataObject2, isDeep, properties_1)) {
return false;
}
List attrProperties_1 = ((SDODataObject)dataObject1)._getOpenContentPropertiesAttributes();
List attrProperties_2 = ((SDODataObject)dataObject2)._getOpenContentPropertiesAttributes();
// different size of open content properties
if ((attrProperties_1.size() != attrProperties_2.size()) || !attrProperties_1.containsAll(attrProperties_2)) {
return false;
}
if (!compare(dataObject1, dataObject2, isDeep, attrProperties_1)) {
return false;
}
return true;
}
}
/**
* INTERNAL: Return whether the 2 sequences are equal.
* Element properties and unstructured text will be compared - attributes are out of scope.
* <p>
* For shallow equal - only dataType=true objects are compared, DataObject values are ignored but should be defaults.
* Note: A setting object should handle its own isEqual() behavior
*
* @param aSequence
* @param aSequenceCopy
* @param isDeep
* @return
*/
private boolean compareSequences(SDOSequence aSequence, SDOSequence aSequenceCopy, boolean isDeep) {
// corner case: isSequenced set to true after type definition had not sequence
if ((null == aSequence) && (null == aSequenceCopy)) {
return true;
}
// both sequences must be null
if ((null == aSequence) || (null == aSequenceCopy)) {
return false;
}
// for shallow equal - match whether we skipped creating settings or set value=null for shallow copies
if (isDeep && aSequence.size() != aSequenceCopy.size()) {
return false;
}
// the settings inside the sequence must be new objects
List originalSettingsList = aSequence.getSettings();
List copySettingsList = aSequenceCopy.getSettings();
if ((null == originalSettingsList) || (null == copySettingsList)) {
return false;
}
SDOProperty originalProperty = null;
SDOProperty copyProperty = null;
/**
* For shallow equal when dataType is false we do not check this setting,
* the value will be unset (default value) in the shallow copy.
* orig v1=String v2=DataObject v3=String
* shallowcopy v1=String v2=null(default) v3=String
* deepcopy v1=String v2=DataObject v3=String
*/
if (isDeep) {
for (int index = 0, size = aSequence.size(); index < size; index++) {
originalProperty = aSequence.getProperty((Setting) originalSettingsList.get(index));
copyProperty = aSequenceCopy.getProperty((Setting) copySettingsList.get(index));
// we must handle null properties that represent unstructured text
// both null = unstructured
// one null = invalid state (return not equal)
// both !null = valid state (check equality)
if (((null == originalProperty) && (null != copyProperty)) || ((null != originalProperty) && (null == copyProperty))) {
return false;
}
// the property field on the setting must point to the same property instance as the original
if (!arePropertiesEqual(originalProperty, copyProperty)) {// handle both properties == null
return false;
}
Object originalValue = aSequence.getValue(index);
Object copyValue = aSequenceCopy.getValue(index);
// for unstructuredText (null property) and simple dataTypes we check equality directly
if ((null == originalProperty) || originalProperty.getType().isDataType()) {
// if one of the values is null return false
if (((null == originalValue) && (null != copyValue)) || //
((null != originalValue) && (null == copyValue))) {
return false;
}
// if both values are null - they are equal
if ((null != originalValue) && !originalValue.equals(copyValue)) {// we can also use !.equals()
return false;
}
} else {
// For complex types
// we do not need to check deep equality on dataObjects twice here, just check instances
// because the dataObject compare will iterate all the properties of each dataObject
// only compare DataObjects when in a deep equal
if (isDeep) {
if ((null != originalValue) && (null != copyValue)) {
// setting.isSet is ignored for sequences
// perform a deep equal on the single item
// the values may not match their types - return false instead of a CCE
if (originalValue instanceof DataObject && copyValue instanceof DataObject) {
if (!equal((DataObject) originalValue, (DataObject) copyValue)) {
return false;
}
} else if (originalValue instanceof XMLRoot && copyValue instanceof XMLRoot) {
XMLRoot originalXMLRoot = (XMLRoot) originalValue;
XMLRoot copyXMLRoot = (XMLRoot) copyValue;
// compare local names of XMLRoot objects
if (!originalXMLRoot.getLocalName().equals(copyXMLRoot.getLocalName())) {
return false;
}
// compare uris of XMLRoot objects
if (!originalXMLRoot.getNamespaceURI().equals(copyXMLRoot.getNamespaceURI())) {
return false;
}
Object originalUnwrappedValue = (originalXMLRoot).getObject();
Object copyUnwrappedValue = (copyXMLRoot).getObject();
if (originalUnwrappedValue instanceof DataObject && copyUnwrappedValue instanceof DataObject) {
if (!equal((DataObject) originalUnwrappedValue, (DataObject) copyUnwrappedValue)) {
return false;
}
}
} else {
return false;
}
} else {
// both values must be null to be equal
if (((null == originalValue) && (null != copyValue)) || ((null == copyValue) && (null != originalValue))) {
return false;
}
}
} else {
// For DataObjects in general anything that is deep equal is also shallow equal - but not the reverse.
// In the case of shallow equal on sequences. We can ignore the state of the 2 complex objects.
// UC1: if aSequenceCopy setting was from a shallowCopy then it will be unset.
// UC2: if aSequenceCopy setting was from a deepCopy or a reversed argument shallowCopy then it may be unset or set.
// We will not check for a default value on either sequence setting.
}
}
}
} else {
int cpyIdx = 0;
boolean locatedSetting;
// compare settings
for (int idx = 0; idx < aSequence.getSettings().size(); idx++) {
SDOProperty nextProperty = aSequence.getProperty(idx);
if (nextProperty == null || nextProperty.getType().isDataType()) {
// compare to the next copy datatype setting
Object nextValue = aSequence.getValue(idx);
locatedSetting = false;
for (; cpyIdx < aSequenceCopy.getSettings().size(); cpyIdx++) {
SDOProperty nextCopyProperty = aSequenceCopy.getProperty(cpyIdx);
if (nextCopyProperty == null || nextCopyProperty.getType().isDataType()) {
// at this point we need to compare the two Settings and their properties
Object nextCopyValue = aSequenceCopy.getValue(cpyIdx);
if (nextValue == nextCopyValue && arePropertiesEqual(nextProperty, nextCopyProperty)) {
locatedSetting = true;
}
cpyIdx++;
break;
}
}
if (!locatedSetting) {
return false;
}
}
}
// at this point there should not be any more copy settings
if (getIndexOfNextDataTypeSetting(aSequenceCopy, cpyIdx) != -1) {
return false;
}
}
return true;
}
/**
* INTERNAL:
* Convenience method for returning the index of the next DataType
* Setting in a given sequence.
*
* @param aSequence
* @param index
* @return the next Setting after index in the sequence, or -1 if none
*/
private int getIndexOfNextDataTypeSetting(SDOSequence aSequence, int index) {
List<Setting> settings = aSequence.getSettings();
for (int i = index; i < settings.size(); i++) {
SDOProperty nextProperty = aSequence.getProperty(i);
if (nextProperty == null || nextProperty.getType().isDataType()) {
return i;
}
}
return -1;
}
/**
* INTERNAL:
* Convenience method that compares two Property objects for equality
* @param prop1
* @param prop2
* @return
*/
private boolean arePropertiesEqual(Property prop1, Property prop2) {
if (((null == prop1) && (null != prop2)) || ((null != prop1) && (null == prop2))) {
return false;
}
// the property field on the setting must point to the same property instance as the original
if (!prop1.equals(prop2)) {
return false;
}
return true;
}
/**
* INTERNAL:
* iterativly, compare the values of shared properties in two target DataObjects
* @param dataObject1 the DataObject to be compared
* @param dataObject2 the DataObject to be compared
* @param isDeep if comparison is deep
* @param properties list of properties shared by two DataObjects
* @return true if two DataObjects meet requirements of shallow equal or deep equal
*/
private boolean compare(DataObject dataObject1, DataObject dataObject2, boolean isDeep, List properties) {
Iterator iterProperties = properties.iterator();
while (iterProperties.hasNext()) {
// !! assumption is two dataobjects share the same property !!
SDOProperty p = (SDOProperty)iterProperties.next();
if (!compareProperty(dataObject1, dataObject2, isDeep, p)) {
return false;
}
}
return true;
}
/**
* INTERNAL:
* Recursively [PreOrder sequence] compare corresponding properties of 2 DataObjects
* check value of property p when p is not a many type property.
* @param dataObject1 the DataObject to be compared
* @param dataObject2 the DataObject to be compared
* @param isDeep if comparison is deep
* @param p the property shared by two DataObjects
* @return true if two DataObjects meet requirements of shallow equal or deep equal
*/
private boolean compareProperty(DataObject dataObject1, DataObject dataObject2, boolean isDeep, SDOProperty p) {
if (p.isMany()) {
return compareManyProperty(dataObject1, dataObject2, isDeep, p);
}
// base step
if (p.getType().isDataType() && !p.getType().isChangeSummaryType()) {
boolean isSet1 = dataObject1.isSet(p);
boolean isSet2 = dataObject2.isSet(p);
if (!isSet1 && !isSet2) {
return true;
}
if (isSet1 && isSet2) {
Object aProperty1 = dataObject1.get(p);
Object aProperty2 = dataObject2.get(p);
// if both properties are null then return equality
if (null == aProperty1) {
return aProperty2 == null;
}
if (null == aProperty2) {// aProperty1 is not null while aPropertyt2 is null
return false;
}
return aProperty1.equals(aProperty2);
}
return false;
}
// recursive step
if (isDeep && !p.getType().isChangeSummaryType()) {
if (!dataObject1.isSet(p) && !dataObject2.isSet(p)) {
return true;
}
// #5852525 handle null properties with isSet=true
if (!p.getType().isDataType()) {
if (null == p.getOpposite()) {
// compare unidirectional or containment=true properties - recursively
return compareDataObjects(dataObject1.getDataObject(p), dataObject2.getDataObject(p), isDeep);
} else {
// 20060906: handle bidirectional properties
// the check across to another branch in the tree will only go 1 shallow level deep
// avoiding an infinite recursive loop and deferring the check for that branch when it
// is encountered in its PreOrder sequence.
return compareDataObjects(dataObject1.getDataObject(p), dataObject2.getDataObject(p), false);
// Spec 3.10 "All reachable DOs in their graphs are equal"
// return true since both do1.get(p) and do2.get(p) are set and are instances of DO
//return true;
}
}
return false;
}
return true;
}
/**
* INTERNAL:
* iteratively check value of property p when p is many type property.
* @param dataObject1 the DataObject to be compared
* @param dataObject2 the DataObject to be compared
* @param isDeep if comparison is deep
* @param p the property shared by two DataObjects
* @return true if two DataObjects meet requirements of shallow equal or deep equal
*/
private boolean compareManyProperty(DataObject dataObject1, DataObject dataObject2, boolean isDeep, Property p) {
List l1 = dataObject1.getList(p);
List l2 = dataObject2.getList(p);
if (p.getType().isDataType()) {
if (dataObject1.isSet(p) != dataObject2.isSet(p)) {
return false;
}
if (l1.size() != l2.size()) {
return false;
}
for (int i = 0; i < l1.size(); i++) {
Object o1 = l1.get(i);
Object o2 = l2.get(i);
if (!o1.equals(o2)) {
return false;
}
}
return true;
}
if (isDeep) {
if (dataObject1.isSet(p) != dataObject2.isSet(p)) {
return false;
}
if (l1.size() != l2.size()) {
return false;
}
// !! can a list contains duplicated objects: {A, A'} and A equals A' or {A, A}
for (int i = 0, size = l1.size(); i < size; i++) {
DataObject o1_l1 = (DataObject)l1.get(i);
DataObject o2_l2 = (DataObject)l2.get(i);
if (!isADataObjectInList(o1_l1, l2)) {
return false;
}
if (!isADataObjectInList(o2_l2, l1)) {
return false;
}
}
}
return true;
}
/**
* INTERNAL:
* @param dataObject1
* @param objects
* @return
*/
private boolean isADataObjectInList(DataObject dataObject1, List objects) {
Iterator iterObjects = objects.iterator();
while (iterObjects.hasNext()) {
DataObject dataObject2 = (DataObject)iterObjects.next();
if (compareDataObjects(dataObject1, dataObject2, true)) {
return true;
}
}
return false;
}
/**
* INTERNAL:
* Return the helperContext containing this equalityHelper.
* @return
*/
public HelperContext getHelperContext() {
if(null == aHelperContext) {
aHelperContext = HelperProvider.getDefaultContext();
}
return aHelperContext;
}
/**
* INTERNAL:
* Set the helperContext if this equalityHelper was created using the default constructor.
* @param helperContext
*/
public void setHelperContext(HelperContext helperContext) {
aHelperContext = helperContext;
}
}