blob: 9b1a30af3de3610de75f16b43f67c166335264b7 [file] [log] [blame]
/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* https://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 org.apache.cayenne;
import org.apache.cayenne.exp.path.CayennePath;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.EmbeddedAttribute;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.PropertyDescriptor;
import org.apache.cayenne.reflect.PropertyUtils;
import org.apache.cayenne.reflect.ToManyMapProperty;
import org.apache.cayenne.validation.BeanValidationFailure;
import org.apache.cayenne.validation.ValidationFailure;
import org.apache.cayenne.validation.ValidationResult;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Base implementation of {@link Persistent}, have no assumption about how data is actually stored.
* Provides implementation of properties declared in Persistent interface.
* <p>
* Three variants are currently supported:
* <ul>
* <li> field based storage, e.g. each entity class will directly define fields to store data
* <li> {@link Map} based storage, e.g. values will be stored in general Map ({@link GenericPersistentObject})
* <li> mixed fields and generic Map to store runtime attributes ({@link HybridPersistentObject})
* </ul>
* <p>
* This class can be used directly as superclass for field-based data objects.
* <p>
* To create own implementation of {@link Persistent} with custom field storage logic it is enough
* to implement {@link #readPropertyDirectly(String)} and {@link #writePropertyDirectly(String, Object)} methods
* and serialization support if needed (helper methods {@link #writeState(ObjectOutputStream)}
* and {@link #readState(ObjectInputStream)} are provided).
* <h1>POJO Note</h1>
* <p>
* If having PersistentObject as a superclass presents a problem in an application, source
* code of this class can be copied verbatim to a custom class generation template.
* Desired superclass can be set in CayenneModeler.
* </p>
*
* @since 1.2
*/
public abstract class PersistentObject implements Persistent, Validating {
protected ObjectId objectId;
protected int persistenceState;
protected transient ObjectContext objectContext;
protected long snapshotVersion = DEFAULT_VERSION;
/**
* Creates a new transient object.
*/
public PersistentObject() {
this.persistenceState = PersistenceState.TRANSIENT;
}
public int getPersistenceState() {
return persistenceState;
}
public ObjectContext getObjectContext() {
return objectContext;
}
/**
* @since 1.2
*/
@Override
public void setObjectContext(ObjectContext objectContext) {
this.objectContext = objectContext;
if (objectContext == null) {
this.persistenceState = PersistenceState.TRANSIENT;
}
}
public ObjectId getObjectId() {
return objectId;
}
public void setObjectId(ObjectId objectId) {
this.objectId = objectId;
}
/**
* Returns a map key for a given to-many map relationship and a target object.
*
* @since 3.0
*/
protected Object getMapKey(String relationshipName, Object value) {
EntityResolver resolver = objectContext.getEntityResolver();
ClassDescriptor descriptor = resolver
.getClassDescriptor(objectId.getEntityName());
if (descriptor == null) {
throw new IllegalStateException("Persistent's entity is unmapped, objectId: "
+ objectId);
}
PropertyDescriptor property = descriptor.getProperty(relationshipName);
if (property instanceof ToManyMapProperty) {
return ((ToManyMapProperty) property).getMapKey(value);
}
throw new IllegalArgumentException("Relationship '"
+ relationshipName
+ "' is not a to-many Map");
}
@Override
public Object readPropertyDirectly(String propName) {
// return null by default to keep compatibility with pre 4.1 versions
return null;
}
@Override
public void writePropertyDirectly(String propName, Object val) {
throw new IllegalArgumentException("Unknown property: " + propName);
}
protected void beforePropertyRead(String propName) {
if (objectContext != null) {
// will resolve faults ourselves below as checking class descriptors
// for the "lazyFaulting" flag is inefficient. Passing "false" here to
// suppress fault processing
objectContext.prepareForAccess(this, propName, false);
}
}
protected void beforePropertyWrite(String propName, Object oldValue, Object newValue) {
if (objectContext != null) {
objectContext.prepareForAccess(this, propName, false);
objectContext.propertyChanged(this, propName, oldValue, newValue);
}
}
@Override
public Object readProperty(String propertyName) {
beforePropertyRead(propertyName);
Object object = readPropertyDirectly(propertyName);
if (object instanceof Fault) {
object = ((Fault) object).resolveFault(this, propertyName);
writePropertyDirectly(propertyName, object);
}
return object;
}
/**
* Returns a value of the property identified by a property path. Supports
* reading both mapped and unmapped properties. Unmapped properties are
* accessed in a manner consistent with JavaBeans specification.
* <p>
* Property path (or nested property) is a dot-separated path used to
* traverse object relationships until the final object is found. If a null
* object found while traversing path, null is returned. If a list is
* encountered in the middle of the path, CayenneRuntimeException is thrown.
* Unlike {@link #readPropertyDirectly(String)}, this method will resolve an
* object if it is HOLLOW.
* <p>
* Examples:
* </p>
* <ul>
* <li>Read this object property:<br>
* <code>String name = (String)artist.readNestedProperty("name");</code><br>
* <br>
* </li>
* <li>Read an object related to this object:<br>
* <code>Gallery g = (Gallery)paintingInfo.readNestedProperty("toPainting.toGallery");</code>
* <br>
* <br>
* </li>
* <li>Read a property of an object related to this object: <br>
* <code>String name = (String)painting.readNestedProperty("toArtist.artistName");</code>
* <br>
* <br>
* </li>
* <li>Read to-many relationship list:<br>
* <code>List exhibits = (List)painting.readNestedProperty("toGallery.exhibitArray");</code>
* <br>
* <br>
* </li>
* <li>Read to-many relationship in the middle of the path:<br>
* <code>List&lt;String&gt; names = (List&lt;String&gt;)artist.readNestedProperty("paintingArray.paintingName");</code>
* <br>
* <br>
* </li>
* </ul>
*
* @since 1.0.5
*/
@Override
public Object readNestedProperty(String path) {
return readNestedProperty(CayennePath.of(path));
}
/**
* @inheritDoc
*/
@Override
public Object readNestedProperty(CayennePath path) {
if ((null == path) || path.isEmpty()) {
throw new IllegalArgumentException("the path must be supplied in order to lookup a nested property");
}
String firstSegment = path.first().value();
Object property = readSimpleProperty(firstSegment);
if (property == null) {
return null;
}
if (path.length() == 1) {
return property;
}
CayennePath pathRemainder = path.tail(1);
if (property instanceof Persistent) {
return ((Persistent) property).readNestedProperty(pathRemainder);
} else {
return Cayenne.readNestedProperty(property, pathRemainder);
}
}
Object readSimpleProperty(String property) {
// side effect - resolves HOLLOW object
Object object = readProperty(property);
// if a null value is returned, there is still a chance to
// find a non-persistent property via reflection
if (object == null) {
object = PropertyUtils.getProperty(this, property);
}
return object;
}
@Override
public void writeProperty(String propName, Object val) {
Object oldValue = readPropertyDirectly(propName);
beforePropertyWrite(propName, oldValue, val);
writePropertyDirectly(propName, val);
}
@SuppressWarnings("unchecked")
@Override
public void removeToManyTarget(String relName, Persistent value, boolean setReverse) {
// Now do the rest of the normal handling (regardless of whether it was
// flattened or not)
Object holder = readProperty(relName);
// call 'propertyChanged' AFTER readProperty as readProperty ensures
// that this object fault is resolved
objectContext.propertyChanged(this, relName, value, null);
// TODO: andrus 8/20/2007 - can we optimize this somehow, avoiding type checking??
if (holder instanceof Collection) {
((Collection<Object>) holder).remove(value);
} else if (holder instanceof Map) {
((Map<Object, Object>) holder).remove(getMapKey(relName, value));
}
if (value != null && setReverse) {
unsetReverseRelationship(relName, value);
}
}
@SuppressWarnings("unchecked")
@Override
public void addToManyTarget(String relName, Persistent value, boolean setReverse) {
if (value == null) {
throw new NullPointerException("Attempt to add null target Persistent.");
}
willConnect(relName, value);
// Now do the rest of the normal handling (regardless of whether it was
// flattened or not)
Object holder = readProperty(relName);
// call 'propertyChanged' AFTER readProperty as readProperty ensures
// that this object fault is resolved
objectContext.propertyChanged(this, relName, null, value);
// TODO: andrus 8/20/2007 - can we optimize this somehow, avoiding type checking??
if (holder instanceof Collection) {
((Collection<Object>) holder).add(value);
} else if (holder instanceof Map) {
((Map<Object, Object>) holder).put(getMapKey(relName, value), value);
}
if (setReverse) {
setReverseRelationship(relName, value);
}
}
/**
* Sets the relationships to the specified <code>Persistent</code> objects.
*
* <p>
* New relationships will be created with
* {@link #addToManyTarget(String, org.apache.cayenne.Persistent, boolean)},
* already established relationships stay untouched. Missing relationships
* will be removed with
* {@link #removeToManyTarget(String, org.apache.cayenne.Persistent, boolean)}
* and returned as List. You may delete them manually.
* </p>
*
* <p>
* Notice: Moving an object relationship to another object, is still needing
* an manually "unregister" from the first object by
* {@link #removeToManyTarget(String, org.apache.cayenne.Persistent, boolean)}
* </p>
*
* @param relName name of the relation
* @param values <code>Persistent</code> objects of this
* <code>Collection</code> are set to the object. No changes will
* be made to the the <code>Collection</code>, a copy is used. It
* is safe to pass a persisted <code>Collection</code> of another
* object.
* @param setReverse update reverse relationships
* @return <code>List&lt;? extends Persistent&gt;</code> of unrelated Persistent objects.
* If no relationship was removed an empty List is returned.
* @throws IllegalArgumentException if no relationship could be read by relName, or if the passed
* <code>Collection</code> is null. To clear all relationships
* use an empty <code>Collection</code>
* @throws UnsupportedOperationException if the relation Collection Type is neither
* <code>java.util.Collection</code> nor
* <code>java.util.Map</code>
* @since 4.0
*/
@SuppressWarnings("unchecked")
public List<? extends Persistent> setToManyTarget(String relName, Collection<? extends Persistent> values,
boolean setReverse) {
if (values == null) {
throw new IllegalArgumentException("values Collection is null. To clear all relationships use an empty Collection");
}
Object property = readProperty(relName);
if (property == null) {
throw new IllegalArgumentException("unknown relName " + relName);
}
Collection<Persistent> old;
if (property instanceof Map) {
old = ((Map<?, Persistent>) property).values();
} else if (property instanceof Collection) {
old = (Collection<Persistent>) property;
} else {
throw new UnsupportedOperationException("setToManyTarget operates only with Map or Collection types");
}
// operate on a copy of passed collection
values = new ArrayList<>(values);
List<Persistent> removedObjects = new ArrayList<>();
// remove all relationships, which are missing in passed collection
Persistent[] oldValues = old.toArray(new Persistent[0]);
for (Persistent obj : oldValues) {
if (!values.contains(obj)) {
removeToManyTarget(relName, obj, setReverse);
// collect objects whose relationship was removed
removedObjects.add(obj);
}
}
// don't add elements which are already present
for (Persistent obj : old) {
values.remove(obj);
}
// add new elements
for (Persistent obj : values) {
addToManyTarget(relName, obj, setReverse);
}
return removedObjects;
}
@Override
public void setToOneTarget(String relationshipName, Persistent value, boolean setReverse) {
willConnect(relationshipName, value);
Object oldTarget = readProperty(relationshipName);
if (oldTarget == value) {
return;
}
getObjectContext().propertyChanged(this, relationshipName, oldTarget, value);
if (setReverse) {
// unset old reverse relationship
if (oldTarget instanceof Persistent) {
unsetReverseRelationship(relationshipName, (Persistent) oldTarget);
}
// set new reverse relationship
if (value != null) {
setReverseRelationship(relationshipName, value);
}
}
// readProperty will call this
//objectContext.prepareForAccess(this, relationshipName, false);
writePropertyDirectly(relationshipName, value);
}
/**
* Called before establishing a relationship with another object. Applies
* "persistence by reachability" logic, pulling one of the two objects to a
* DataConext of another object in case one of the objects is transient. If
* both objects are persistent, and they don't have the same DataContext,
* CayenneRuntimeException is thrown.
*
* @since 1.2
*/
protected void willConnect(String relationshipName, Persistent object) {
// first handle most common case - both objects are in the same
// ObjectContext or target is null
if (object == null || this.getObjectContext() == object.getObjectContext()) {
return;
} else if (this.getObjectContext() == null && object.getObjectContext() != null) {
object.getObjectContext().registerNewObject(this);
} else if (this.getObjectContext() != null && object.getObjectContext() == null) {
this.getObjectContext().registerNewObject(object);
} else {
throw new CayenneRuntimeException("Cannot set object as destination of relationship %s"
+ " because it is in a different ObjectContext", relationshipName);
}
}
/**
* Initializes reverse relationship from object <code>val</code> to this
* object.
*
* @param relName name of relationship from this object to <code>val</code>.
*/
protected void setReverseRelationship(String relName, Persistent val) {
ObjRelationship rel = objectContext.getEntityResolver().getObjEntity(objectId.getEntityName())
.getRelationship(relName);
ObjRelationship revRel = rel.getReverseRelationship();
if (revRel != null) {
Object oldTarget = val.readProperty(revRel.getName());
if (oldTarget != val && oldTarget instanceof Persistent && val instanceof PersistentObject) {
((PersistentObject)val).unsetReverseRelationship(revRel.getName(), (Persistent) oldTarget);
}
if (revRel.isToMany()) {
val.addToManyTarget(revRel.getName(), this, false);
} else {
val.setToOneTarget(revRel.getName(), this, false);
}
}
}
/**
* Removes current object from reverse relationship of object
* <code>val</code> to this object.
*/
protected void unsetReverseRelationship(String relName, Persistent val) {
EntityResolver resolver = objectContext.getEntityResolver();
ObjEntity entity = resolver.getObjEntity(objectId.getEntityName());
if (entity == null) {
throw new IllegalStateException("Persistent's entity is unmapped, objectId: " + objectId);
}
ObjRelationship rel = entity.getRelationship(relName);
ObjRelationship revRel = rel.getReverseRelationship();
if (revRel != null) {
if (revRel.isToMany()) {
val.removeToManyTarget(revRel.getName(), this, false);
} else {
val.setToOneTarget(revRel.getName(), null, false);
}
}
}
@Override
public void setPersistenceState(int persistenceState) {
this.persistenceState = persistenceState;
}
/**
* @since 1.1
*/
@Override
public long getSnapshotVersion() {
return snapshotVersion;
}
/**
* @since 1.1
*/
@Override
public void setSnapshotVersion(long snapshotVersion) {
this.snapshotVersion = snapshotVersion;
}
/**
* Performs property validation of the object, appending any validation
* failures to the provided validationResult object. This method is invoked
* from "validateFor.." before committing a NEW or MODIFIED object to the
* database. Validation includes checking for null values and value sizes.
* PersistentObject subclasses may override this method, calling super.
*
* @since 1.1
*/
protected void validateForSave(ValidationResult validationResult) {
ObjEntity objEntity = getObjectContext().getEntityResolver().getObjEntity(this);
if (objEntity == null) {
throw new CayenneRuntimeException("No ObjEntity mapping found for Persistent %s", getClass().getName());
}
// validate mandatory attributes
Map<String, ValidationFailure> failedDbAttributes = null;
for (ObjAttribute next : objEntity.getAttributes()) {
// TODO: andrus, 2/20/2007 - handle embedded attribute
if (next instanceof EmbeddedAttribute) {
continue;
}
DbAttribute dbAttribute = next.getDbAttribute();
if (dbAttribute == null) {
throw new CayenneRuntimeException("ObjAttribute '%s"
+ "' does not have a corresponding DbAttribute", next.getName());
}
// pk may still be generated
if (dbAttribute.isPrimaryKey()) {
continue;
}
Object value = this.readPropertyDirectly(next.getName());
if (dbAttribute.isMandatory()) {
ValidationFailure failure = BeanValidationFailure.validateNotNull(this, next.getName(), value);
if (failure != null) {
if (failedDbAttributes == null) {
failedDbAttributes = new HashMap<>();
}
failedDbAttributes.put(dbAttribute.getName(), failure);
continue;
}
}
// validate length
if (value != null && dbAttribute.getMaxLength() > 0) {
if (value.getClass().isArray()) {
int len = Array.getLength(value);
if (len > dbAttribute.getMaxLength()) {
String message = "\"" + next.getName() + "\" exceeds maximum allowed length ("
+ dbAttribute.getMaxLength() + " bytes): " + len;
validationResult.addFailure(new BeanValidationFailure(this, next.getName(), message));
}
} else if (value instanceof CharSequence) {
int len = ((CharSequence) value).length();
if (len > dbAttribute.getMaxLength()) {
String message = "\"" + next.getName() + "\" exceeds maximum allowed length ("
+ dbAttribute.getMaxLength() + " chars): " + len;
validationResult.addFailure(new BeanValidationFailure(this, next.getName(), message));
}
}
}
}
// validate mandatory relationships
for (final ObjRelationship relationship : objEntity.getRelationships()) {
List<DbRelationship> dbRels = relationship.getDbRelationships();
if (dbRels.isEmpty()) {
continue;
}
// skip db relationships that we can't validate or that can't be invalid here
// can't handle paths longer than two db relationships
// see ObjRelationship.recalculateReadOnlyValue() for more info
if (relationship.isSourceIndependentFromTargetChange()) {
continue;
}
// if db relationship is not based on a PK and is based on mandatory
// attributes, see if we have a target object set
// relationship will be validated only if all db path has mandatory
// db relationships
boolean validate = true;
for (DbRelationship dbRelationship : dbRels) {
for (DbJoin join : dbRelationship.getJoins()) {
DbAttribute source = join.getSource();
if (source.isMandatory()) {
// clear attribute failures...
if (failedDbAttributes != null && !failedDbAttributes.isEmpty()) {
failedDbAttributes.remove(source.getName());
}
} else {
// do not validate if the relation is based on
// multiple keys with some that can be nullable.
validate = false;
}
}
}
if (validate) {
Object value = this.readPropertyDirectly(relationship.getName());
ValidationFailure failure = BeanValidationFailure.validateNotNull(this, relationship.getName(), value);
if (failure != null) {
validationResult.addFailure(failure);
}
}
}
// deal with previously found attribute failures...
if (failedDbAttributes != null && !failedDbAttributes.isEmpty()) {
for (ValidationFailure failure : failedDbAttributes.values()) {
validationResult.addFailure(failure);
}
}
}
/**
* Calls {@link #validateForSave(ValidationResult)}. PersistentObject
* subclasses may override it providing validation logic that should be
* executed for the newly created objects before saving them.
*
* @since 1.1
*/
@Override
public void validateForInsert(ValidationResult validationResult) {
validateForSave(validationResult);
}
/**
* Calls {@link #validateForSave(ValidationResult)}. PersistentObject
* subclasses may override it providing validation logic that should be
* executed for the modified objects before saving them.
*
* @since 1.1
*/
@Override
public void validateForUpdate(ValidationResult validationResult) {
validateForSave(validationResult);
}
/**
* This implementation does nothing. PersistentObject subclasses may
* override it providing validation logic that should be executed for the
* deleted objects before committing them.
*
* @since 1.1
*/
@Override
public void validateForDelete(ValidationResult validationResult) {
// does nothing
}
/**
* Serialization support.
* Will write down persistenceState and objectId, delegating data serialization down to sub-classes.
*/
private void writeObject(ObjectOutputStream out) throws IOException {
writeSerialized(out);
}
/**
* Serialization support.
* Will read persistenceState and objectId, delegating data serialization down to sub-classes.
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
readSerialized(in);
}
protected void writeSerialized(ObjectOutputStream out) throws IOException {
out.writeInt(persistenceState);
out.writeObject(objectId);
if (persistenceState == PersistenceState.COMMITTED
|| persistenceState == PersistenceState.HOLLOW) {
return;
}
writeState(out);
}
protected void readSerialized(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.persistenceState = in.readInt();
this.objectId = (ObjectId) in.readObject();
if (persistenceState == PersistenceState.COMMITTED
|| persistenceState == PersistenceState.HOLLOW) {
persistenceState = PersistenceState.HOLLOW;
return;
}
readState(in);
}
protected void writeState(ObjectOutputStream out) throws IOException {
// no additional info for base class
}
protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
// no additional info for base class
}
/**
* A variation of "toString" method, that may be more efficient in some
* cases. For example when printing a list of objects into the same String.
*/
public StringBuffer toStringBuffer(StringBuffer buffer, boolean fullDesc) {
String id = (objectId != null) ? objectId.toString() : "<no id>";
String state = PersistenceState.persistenceStateName(persistenceState);
buffer.append('{').append(id).append("; ").append(state).append("; ");
if (fullDesc) {
appendProperties(buffer);
}
buffer.append("}");
return buffer;
}
protected void appendProperties(StringBuffer buffer) {
}
@Override
public String toString() {
return toStringBuffer(new StringBuffer(), true).toString();
}
}