org.openmdx.state2.cci.ObjectAndValidStateComparator Maven / Gradle / Ivy
/*
* ====================================================================
* Description: A state-aware XML comparator
* Owner: OMEX AG, Switzerland, http://www.omex.ch
* ====================================================================
*
* This software is published under the BSD license as listed below.
*
* Copyright (c) 2011, OMEX AG, Switzerland
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of the openMDX team nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* ------------------
*
* This product includes software developed by other organizations as
* listed in the NOTICE file.
*/
package org.openmdx.state2.cci;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.resource.cci.MappedRecord;
import javax.xml.datatype.XMLGregorianCalendar;
import org.openmdx.application.xml.Importer;
import org.openmdx.base.accessor.cci.SystemAttributes;
import org.openmdx.base.collection.Sets;
import org.openmdx.base.exception.ServiceException;
import org.openmdx.base.mof.cci.ModelElement_1_0;
import org.openmdx.base.mof.cci.ModelHelper;
import org.openmdx.base.mof.cci.Model_1_0;
import org.openmdx.base.mof.cci.Multiplicity;
import org.openmdx.base.mof.spi.Model_1Factory;
import org.openmdx.base.naming.Path;
import org.openmdx.base.resource.Records;
import org.openmdx.base.rest.cci.ObjectRecord;
import org.openmdx.kernel.exception.BasicException;
import org.openmdx.state2.spi.Order;
import org.openmdx.state2.spi.TechnicalAttributes;
import org.xml.sax.InputSource;
/**
* A state-aware XML comparator
*
* Note:
* This class is not thread safe.
*
*/
public class ObjectAndValidStateComparator {
private Model_1_0 model = Model_1Factory.getModel();
List differences = new ArrayList();
/**
* Aspects are collected under their core reference and processed in a subsequent step
*/
Map aspects = new HashMap();
/**
* The features to be ignored by default
*/
protected final static Collection FEATURES_IGNORED_BY_DEFAULT = Arrays.asList(
"org:openmdx:base:Removable:removedAt",
"org:openmdx:base:Removable:removedBy",
"org:openmdx:base:Creatable:createdAt",
"org:openmdx:base:Creatable:createdBy",
"org:openmdx:base:Modifiable:modifiedAt",
"org:openmdx:base:Modifiable:modifiedBy",
"org:openmdx:state2:StateCapable:stateVersion"
);
/**
* State validity
*/
protected final static Collection RANGE_FEATURES = Arrays.asList(
"org:openmdx:state2:DateState:stateValidFrom",
"org:openmdx:state2:DateState:stateValidTo",
"org:openmdx:state2:DateTimeState:stateValidFrom",
"org:openmdx:state2:DateTimeState:stateInvalidFrom"
);
/**
* Accepts equal values
*/
public static final FeatureComparator EQUALITY = new FeatureComparator(){
public MappedRecord compare(
Path xri,
String feature,
Multiplicity multiplicity,
Object expected,
Object actual
) {
if(expected == actual) {
return null;
}
MappedRecord difference = CARDINALITY.compare(xri, feature, multiplicity, expected, actual);
if(difference != null) {
return difference;
}
final boolean equal;
switch(multiplicity) {
case SET:
equal = asSet(actual).equals(asSet(expected));
break;
case LIST:
equal = asList(actual).equals(asList(expected));
break;
default:
equal = actual != null && expected != null && actual.equals(expected);
break;
}
return equal ? null : ObjectAndValidStateComparator.newDifference(
xri,
feature,
multiplicity,
"The actual value of feature '" + feature + "' does not match the expected one",
expected,
actual
);
}
private Set> asSet(
Object value
){
if(value == null) {
return Collections.emptySet();
} else if (value instanceof Collection>) {
return Sets.asSet((Collection>)value);
} else {
return Collections.singleton(value);
}
}
private List> asList(
Object value
){
if(value == null) {
return Collections.emptyList();
} else if (value instanceof List>) {
return (List>)value;
} else {
return Collections.singletonList(value);
}
}
};
/**
* Accepts values with the same cardinality.
*/
public static final FeatureComparator CARDINALITY = new FeatureComparator(){
public MappedRecord compare(
Path xri,
String feature,
Multiplicity multiplicity,
Object expected,
Object actual
) {
int expectedCardinality = getCardinality(expected);
int actualCardinality = getCardinality(actual);
return expectedCardinality == actualCardinality ? null : ObjectAndValidStateComparator.newDifference(
xri,
feature,
multiplicity,
"The actual cardinality " + actualCardinality + " does not match the expected cardinality " + expectedCardinality,
expected,
actual
);
}
private int getCardinality(
Object value
){
return
value == null ? 0 :
value instanceof Collection> ? ((Collection>)value).size() :
value instanceof Map,?> ? ((Map,?>)value).size() :
1;
}
};
/**
* Accepts any values
*/
public static final FeatureComparator IGNORE = new FeatureComparator() {
public MappedRecord compare(
Path xri,
String feature,
Multiplicity multiplicity,
Object expected,
Object actual
) {
return null;
}
};
/**
* Compare the objects and return the differences
*
* @param expectedObjects
* @param actualObjects
*
* @return the differences
* @throws ServiceException
*/
public List compare(
SortedMap expectedObjects,
SortedMap actualObjects
) throws ServiceException {
clear();
compareObjects(
expectedObjects.values().iterator(),
actualObjects.values().iterator()
);
for(Map.Entry entry : this.aspects.entrySet()) {
this.compareAspects(entry.getKey(), entry.getValue());
}
return this.differences;
}
protected void clear(){
this.differences.clear();
this.aspects.clear();
}
/**
* Compare the objects and return the differences
*
* @param expected
* @param actual
*
* @return the differences
* @throws ServiceException
*/
public List compare(
Iterable expected,
Iterable actual
) throws ServiceException {
SortedMap expectedObjects = new TreeMap();
Importer.importObjects(Importer.asTarget(expectedObjects), expected);
SortedMap actualObjects = new TreeMap();
Importer.importObjects(Importer.asTarget(actualObjects), actual);
return this.compare(expectedObjects, actualObjects);
}
/**
* Retrieve a feature comparator
*
* @param qualifiedName the qualified feature name
* @return the comparator to be used for this feature
*/
protected FeatureComparator getFeatureComparator(
String qualifiedName
){
return FEATURES_IGNORED_BY_DEFAULT.contains(qualifiedName) ? IGNORE : EQUALITY;
}
private boolean isInstanceOf(
MappedRecord object,
String type
) throws ServiceException{
return this.model.isSubtypeOf(object.getRecordName(), type);
}
private Aspects getAspects(
Path core
){
Aspects aspects = this.aspects.get(core);
if(aspects == null) {
this.aspects.put(
core,
aspects = new Aspects()
);
}
return aspects;
}
/**
* Tests whether an object is modelled as stated but not used as such
*
* @param object
*
* @return true
if the object is modelled as stated but not used as such
* @throws ServiceException
*/
protected boolean isValidTimeUnique(
MappedRecord object
) throws ServiceException{
return
isInstanceOf(object, "org:openmdx:state2:BasicState") &&
Boolean.TRUE.equals(object.get(TechnicalAttributes.VALID_TIME_UNIQUE));
}
private ObjectRecord getNext(
Iterator i,
boolean expected
) throws ServiceException{
while(i.hasNext()) {
final ObjectRecord holder = i.next();
MappedRecord object = holder.getValue();
if(isInstanceOf(object, "org:openmdx:base:Aspect")) {
if(isValidTimeUnique(object)){
return holder;
}
Object core = object.get(SystemAttributes.CORE);
if(core instanceof Path) {
Aspects aspects = getAspects((Path)core);
(expected ? aspects.expected : aspects.actual).add(object);
} else {
this.differences.add(
newDifference(holder.getResourceIdentifier(), "An aspect has no core reference", (MappedRecord)null, object)
);
}
} else {
return holder;
}
}
return null;
}
private void compareObjects(
Iterator ei,
Iterator ai
) throws ServiceException {
ObjectRecord e = getNext(ei, true);
ObjectRecord a = getNext(ai, false);
while(e != null || a != null) {
while(a != null && (e == null || a.getResourceIdentifier().compareTo(e.getResourceIdentifier()) < 0 )) {
this.differences.add(
newDifference(a.getResourceIdentifier(), "Unexpected actual object", (MappedRecord)null, a.getValue())
);
a = getNext(ai, true);
}
while(e != null && (a == null || a.getResourceIdentifier().compareTo(e.getResourceIdentifier()) > 0 )) {
this.differences.add(
newDifference(e.getResourceIdentifier(), "Missing actual object", e.getValue(), null)
);
e = getNext(ei, false);
}
if(a != null && e != null && a.getResourceIdentifier().equals(e.getResourceIdentifier())) {
compareObject(e.getResourceIdentifier(), e.getValue(), a.getValue(), false);
e = getNext(ei, true);
a = getNext(ai, false);
}
}
}
private MappedRecord getState(
List source,
Interval interval
) throws ServiceException{
for(MappedRecord object : source) {
if(this.isInstanceOf(object, "org:openmdx:state2:DateState")) {
XMLGregorianCalendar validFrom = (XMLGregorianCalendar) object.get(TechnicalAttributes.STATE_VALID_FROM);
XMLGregorianCalendar validTo = (XMLGregorianCalendar) object.get(TechnicalAttributes.STATE_VALID_TO);
if(object.get(SystemAttributes.REMOVED_AT) == null) {
if(
Order.compareValidFrom(interval.validFrom, validFrom) >= 0 &&
Order.compareValidTo(interval.validTo, validTo) <= 0
){
return object;
}
}
} else {
throw new ServiceException(
BasicException.Code.DEFAULT_DOMAIN,
BasicException.Code.NOT_IMPLEMENTED,
"Unsupported Aspect",
new BasicException.Parameter("class",object.getRecordName())
);
}
}
return null;
}
private void compareAspects(
Path xri,
Aspects aspects
) throws ServiceException {
List intervals = new ArrayList();
intervals.add(new Interval(null,null));
getIntervals(intervals, aspects.expected);
getIntervals(intervals, aspects.actual);
for(Interval interval : intervals) {
MappedRecord expected = getState(aspects.expected, interval);
MappedRecord actual = getState(aspects.actual, interval);
if(expected != actual) {
if(actual == null) {
this.differences.add(
newDifference(xri, interval, ("No actual state for time range " + interval), expected, null)
);
} else if(expected == null) {
this.differences.add(
newDifference(xri, interval, ("Unexpected state for time range " + interval), null, actual)
);
} else {
compareObject(xri, expected, actual, true);
}
}
}
}
private void getIntervals(
List intervals,
Collection source
){
for(MappedRecord object : source) {
XMLGregorianCalendar validFrom = (XMLGregorianCalendar) object.get(TechnicalAttributes.STATE_VALID_FROM);
XMLGregorianCalendar validTo = (XMLGregorianCalendar) object.get(TechnicalAttributes.STATE_VALID_TO);
From: for(Interval interval : intervals){
int i = Order.compareValidFrom(validFrom, interval.validFrom);
if(i == 0) {
break From;
}
if(i > 0 && Order.compareValidFromToValidTo(validFrom, interval.validTo) <= 0) {
intervals.add(new Interval(validFrom, interval.validTo));
interval.validTo = Order.predecessor(validFrom);
break From;
}
}
To: for(Interval interval : intervals){
int i = Order.compareValidTo(interval.validTo, validTo);
if(i == 0) {
break To;
}
if(i > 0 && Order.compareValidFromToValidTo(interval.validFrom, validTo) <= 0) {
intervals.add(new Interval(interval.validFrom, validTo));
interval.validFrom = Order.successor(validTo);
break To;
}
}
}
}
/**
* State aware feature comparator determination
*
* @param qualifiedName the qualified feature name
* @param aspect true
in case of aspects
*
* @return the feature comparator to be used
*
* @throws ServiceException
*/
private FeatureComparator getFeatureComparator(
String qualifiedName,
boolean aspect
) throws ServiceException{
return aspect && RANGE_FEATURES.contains(qualifiedName) ? IGNORE : getFeatureComparator(qualifiedName);
}
@SuppressWarnings("unchecked")
private void compareObject(
Path xri,
MappedRecord expected,
MappedRecord actual,
boolean aspect
) throws ServiceException{
String type = expected.getRecordName();
ModelElement_1_0 classifierDef = this.model.getElement(type);
if(type.equals(actual.getRecordName())) {
Set features = new HashSet(expected.keySet());
features.addAll(actual.keySet());
for(String feature : features) {
ModelElement_1_0 featureDef = this.model.getFeatureDef(classifierDef, feature, false);
Object actualValue = actual.get(feature);
MappedRecord difference;
if(featureDef == null) {
difference = ObjectAndValidStateComparator.newDifference(
xri,
feature,
null,
"The attribute '" + feature + "' is not a modelled feature",
null,
actualValue
);
} else {
Object expectedValue = expected.get(feature);
String qualifiedName = featureDef.getQualifiedName();
difference = getFeatureComparator(
qualifiedName,
aspect
).compare(
xri,
feature,
ModelHelper.getMultiplicity(featureDef),
expectedValue,
actualValue
);
}
if(difference != null){
this.differences.add(difference);
}
}
} else {
this.differences.add(
newDifference(xri, "Object class mismatch", expected, actual)
);
}
}
/**
* Convert a value to a string.
*
* Its main function is to convert object ids to XRIs
*
* @param value
* @param multiplicity
*
* @return a string representation of the value
* @deprecated Use {@link #toString(Object)} instead
*/
public static Object toString(
Object value,
Multiplicity multiplicity
){
return toString(value);
}
/**
* Convert a value to a string.
*
* Its main function is to convert object ids to XRIs
*
* @param value
* @return a string representation of the value
*/
public static Object toString(
Object value
){
if(value instanceof Path) {
return ((Path)value).toXRI();
} else if(value instanceof Collection) {
Collection