package com.sap.runlet.interpreter.objects;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import com.sap.runlet.abstractinterpreter.Side;
import com.sap.runlet.abstractinterpreter.objects.AbstractValueObject;
import com.sap.runlet.abstractinterpreter.objects.ClassTypedObject;
import com.sap.runlet.abstractinterpreter.objects.Link;
import com.sap.runlet.abstractinterpreter.objects.RunletObject;
import com.sap.runlet.abstractinterpreter.repository.RepositoryObject;
import com.sap.runlet.abstractinterpreter.repository.SnapshotIdentifier;
import com.sap.runlet.interpreter.RunletInterpreter;
import com.sap.runlet.interpreter.RunletLinkContainer;
import data.classes.ActualObjectParameter;
import data.classes.Association;
import data.classes.AssociationEnd;
import data.classes.ClassTypeDefinition;
import data.classes.Parameter;
import data.classes.SapClass;
import data.classes.TypeDefinition;
/**
* Represents an object that is instance of a value class. Key functionality
* of a value object is that it can be cloned, e.g., to produce a copy which then
* can have composite parts replaced as compared to the original.<p>
*
* Equality/hash code is based on the class of this object (formally:
* <tt>getType().getClazz()</tt>) and all the values of those association ends whose
* other end attaches to this class and is marked as
* {@link AssociationEnd#isContributesToEquality() contributing to equality}. It would
* not be possible to determine those association links from the {@link RunletLinkContainer}
* because the link container requires the equality definition already to look up
* this object in its data structures. Therefore, the equality-relevant association
* end values need to be replicated into this object. This redundancy is managed by
* the {@link RunletLinkContainer} alone.<p>
*
* @author Axel Uhl (D043530)
*/
public class ValueObject
extends AbstractValueObject<Association, AssociationEnd, SapClass, TypeDefinition, ClassTypeDefinition>
implements Cloneable {
private static Logger log = Logger.getLogger(ValueObject.class.getName());
/**
* If the value class of which this is an instance uses object parameters
* (see {@link SapClass#getFormalObjectParameters}), this list contains the objects
* resulting from evaluating the actual object parameters when this value object
* was created. Always non-<tt>null</tt>.
*/
private List<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> actualObjectParameters;
/**
* Constructs an object representing a value. Values are characterized by the combination of the
* values of the opposite ends of those association ends attaching to this object's class and
* marked as contributing to equality {@see AssociationEnd#isContributesToEquality()}.
* @param interpreter
* needed to evaluate any actual object parameter expressions on the class type
* definition <tt>type</tt>
*/
protected ValueObject(ClassTypeDefinition type, SnapshotIdentifier snapshot, RunletInterpreter interpreter) {
super(type, snapshot, interpreter.getModelAdapter());
if (type != null) { // null case may occur in case an ExceptionObject is being constructed
fillActualObjectParameters(type, interpreter);
}
}
/**
* Constructs an object representing a value, initializing its equality-defining properties
* which from then on are immutable. Values are generally characterized by the combination of
* the values of the opposite ends of those association ends attaching to this object's class
* and marked as contributing to equality {@see AssociationEnd#isContributesToEquality()}.
* @param interpreter
* needed to evaluate any actual object parameter expressions on the class type
* definition <tt>type</tt>
*/
public ValueObject(ClassTypeDefinition type,
Map<AssociationEnd, Collection<ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>> propertyValues,
SnapshotIdentifier snapshot, RunletInterpreter interpreter) {
super(type, propertyValues, snapshot, interpreter.getModelAdapter());
fillActualObjectParameters(type, interpreter);
}
/**
* From the {@link ActualObjectParameter}s attached to <tt>type</tt>
* evaluates the {@link ActualObjectParameter#getValue() expressions} that
* define the actual parameter values, using the <tt>interpreter</tt>. If
* values are not specified for default parameters, these will be filled
* from the default values defined for the respective formal parameters. The
* results are added to {@link #actualObjectParameters} in the order in
* which the parameters occur in {@link ActualObjectParameter#getValue()}.
*/
private void fillActualObjectParameters(ClassTypeDefinition type, RunletInterpreter interpreter) {
List<Parameter> formalObjectParameters = type.getClazz().getFormalObjectParameters();
if (formalObjectParameters.size() > 0) {
actualObjectParameters = new ArrayList<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>(
type.getObjectParameters().size());
for (ActualObjectParameter aop : type.getObjectParameters()) {
try {
actualObjectParameters.add(interpreter.evaluate(aop.getValue()));
} catch (Exception e) {
log.throwing(ValueObject.class.getName(), "fillActualObjectParameters", e);
throw new RuntimeException(e);
}
}
// fill missing from default values:
for (int i=type.getObjectParameters().size(); i<formalObjectParameters.size(); i++) {
try {
/** TODO avoid endless recursion for default values of the same type as *type*
*
* Example: Number|Number precision=1|
*
* When only the type "Number" is used, the default value expression "1" is evaluated
* here which requires creating a native value object for "1" which has to have
* its actual object parameters set. Since the number literal "1" does not provide
* a value for the formal object parameter "precision," the default value will be
* computed again here, leading to an endless recursion.
*/
actualObjectParameters.add(interpreter.evaluate(formalObjectParameters.get(i).getDefaultValue()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} else {
actualObjectParameters = Collections.emptyList();
}
}
/**
* Creates a clone of this value object by copying the references to all attributes
* including the type definition, native objects for the {@link NativeObject} subclass
* as well as the map that contains the equality-relevant "property" values and the
* {@link ClassTypedObject#getOrigin() snapshot}. The clone returned may, e.g., get
* its snapshot altered without this object having its snapshot altered. However,
* it is not permissible to try to write into {@link #equalityRelevantAssociationEndValues}
* because it would change the equality/hashCode definitions for this value object.
*/
public ValueObject clone() {
ValueObject result = (ValueObject) super.clone();
return result;
}
public Map<AssociationEnd, Collection<ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>> getEqualityRelevantAssociationEndValues() {
return Collections.unmodifiableMap(super.getEqualityRelevantAssociationEndValues());
}
/**
* From the {@link #equalityRelevantAssociationEndValues} map creates a set of links that a
* {@link RunletLinkContainer} may load and that establish (part of) this value from the link
* container's perspective, limited to the association indicated by
* <tt>equalityRelevantFarEnd</tt>. The links returned share the same
* {@link SnapshotIdentifier} as this value returns from its
* {@link RepositoryObject#getOrigin()} operation. The objects at the far end are cloned if
* their {@link RunletObject#getOrigin snapshot} does not match this value's snapshot, and the
* clone is assigned the same snapshot identifier returned by this value's
* {@link RepositoryObject#getOrigin() snapshot}.<p>
*
* This method only returns links connected to this value object. It does not recurse
* into other value objects that are equality-relevant for this value object.<p>
*
* Example: If an <tt>Address</tt> class has two equality-relevant associations, one to
* <tt>String</tt> with far end <tt>street</tt>, one to <tt>City</tt> with far end <tt>city</tt>,
* then if the <tt>street</tt> end is passed to this method, it will create a link from this
* <tt>Address</tt> object to the <tt>String</tt> object that this value has as its <tt>street</tt>
* role. If the <tt>String</tt> value has a different snapshot identifier than this value,
* it will be cloned and the snapshot of the clone will be adjusted to match this object's
* snapshot identifier.
*/
public Collection<Link<Association, AssociationEnd, SapClass, TypeDefinition, ClassTypeDefinition>> getEqualityRelevantLinks(AssociationEnd equalityRelevantFarEnd) {
Collection<Link<Association, AssociationEnd, SapClass, TypeDefinition, ClassTypeDefinition>> links = createCollection(equalityRelevantFarEnd.getType().isOrdered(),
equalityRelevantFarEnd.getType().isUnique());
Side farSide = getModelAdapter().getSideOfEnd(equalityRelevantFarEnd);
boolean thisIsLeft = farSide.equals(Side.RIGHT);
Link<Association, AssociationEnd, SapClass, TypeDefinition, ClassTypeDefinition> link;
for (ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition> objectAtFarEnd : getEqualityRelevantAssociationEndValues()
.get(equalityRelevantFarEnd)) {
ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition> objectAtFarEndWithSnapshotAdjusted = objectAtFarEnd
.getCopyWithOrigin(getOrigin());
if (thisIsLeft) {
link = new Link<Association, AssociationEnd, SapClass, TypeDefinition, ClassTypeDefinition>(
this, objectAtFarEndWithSnapshotAdjusted, equalityRelevantFarEnd
.getAssociation(), getOrigin(), getModelAdapter());
} else {
link = new Link<Association, AssociationEnd, SapClass, TypeDefinition, ClassTypeDefinition>(
objectAtFarEndWithSnapshotAdjusted, this, equalityRelevantFarEnd
.getAssociation(), getOrigin(), getModelAdapter());
}
links.add(link);
}
return links;
}
/**
* Two values are considered logically equal if they are instance of the same class and all their
* equality-relevant properties have equal values.
*/
@Override
public boolean logicallyEquals(RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition> o) {
if (this == o) {
return true;
}
Iterator<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> oIter = o.iterator();
if (!oIter.hasNext()) {
return false;
}
RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition> oro = oIter.next();
if (oIter.hasNext() || !(oro instanceof ValueObject)) {
return false;
}
if (this == oro) {
return true;
}
ValueObject vo = (ValueObject) oro;
return getType().getClazz().equals(vo.getType().getClazz()) &&
logicallyEquals(getEqualityRelevantAssociationEndValues(), vo.getEqualityRelevantAssociationEndValues()) &&
logicallyEquals(actualObjectParameters, vo.actualObjectParameters);
}
@Override
public int logicalHashCode() {
return getType().getClass().hashCode() ^ getEqualityRelevantAssociationEndValues().hashCode() ^
(actualObjectParameters == null ? 0 : actualObjectParameters.hashCode());
}
/**
* Compares the two maps based on the {@link RunletObject#logicallyEquals(RunletObject) logical equality}
* of the {@link RunletObject}s contained in the value collections.
*/
@SuppressWarnings("unchecked") // need to cast from Collection<ClassTypedObject> to Collection<RiverObject>
protected boolean logicallyEquals(
Map<AssociationEnd, Collection<ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>> a,
Map<AssociationEnd, Collection<ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>> b) {
if (a == b) {
return true;
}
if (a.keySet().equals(b.keySet())) {
for (AssociationEnd key:a.keySet()) {
Collection<?> c_a = a.get(key);
Collection<?> c_b = b.get(key);
if (!logicallyEquals((Collection<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>) c_a,
(Collection<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>) c_b)) {
return false;
}
}
}
return true;
}
protected boolean logicallyEquals(Collection<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> c_a,
Collection<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> c_b) {
boolean result = true;
if (c_a.size() != c_b.size()) {
result = false;
} else {
Iterator<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> i_a = c_a.iterator();
Iterator<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> i_b = c_b.iterator();
while (result && i_a.hasNext()) {
if (!i_a.next().logicallyEquals(i_b.next())) {
result = false;
}
}
}
return result;
}
@Override
public boolean isUnique() {
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getType().getClazz().getName());
if (getType().getObjectParameters().size() > 0) {
sb.append('|');
boolean commaBefore = false;
for (RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition> aop : getActualObjectParameters()) {
if (commaBefore) {
sb.append(", ");
} else {
commaBefore = true;
}
sb.append(aop);
}
sb.append('|');
}
sb.append("(");
boolean first = true;
Map<AssociationEnd, Collection<ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>>> values = getEqualityRelevantAssociationEndValues();
for (AssociationEnd ae:values.keySet()) {
if (!first) {
sb.append(", ");
} else {
first = false;
}
sb.append(ae.getName());
sb.append(": ");
if (values.get(ae).size() > 1) {
if (ae.getType().isUnique()) {
sb.append('{');
}
if (ae.getType().isOrdered()) {
sb.append('[');
}
if (!ae.getType().isOrdered() && !ae.getType().isUnique()) {
sb.append('(');
}
}
boolean ffirst = true;
for (ClassTypedObject<AssociationEnd, TypeDefinition, ClassTypeDefinition> cto : values.get(ae)) {
if (!ffirst) {
sb.append(", ");
} else {
ffirst = false;
}
sb.append(cto);
}
if (values.get(ae).size() > 1) {
if (ae.getType().isOrdered()) {
sb.append(']');
}
if (ae.getType().isUnique()) {
sb.append('}');
}
if (!ae.getType().isOrdered() && !ae.getType().isUnique()) {
sb.append(')');
}
}
}
sb.append(")");
return sb.toString();
}
/**
* Returns an immutable collection of the actual object parameters for this value object.
* These correspond to the formal object parameters defined for this object's value class.
*
* @see ActualObjectParameter
* @see SapClass#getFormalObjectParameters()
*/
public List<RunletObject<AssociationEnd, TypeDefinition, ClassTypeDefinition>> getActualObjectParameters() {
return Collections.unmodifiableList(actualObjectParameters);
}
}