blob: 0d5a21ac0e728bf2f936406da5861e58cb2e2c98 [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
*
* 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 org.apache.cayenne;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.query.ObjectIdQuery;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.Property;
import org.apache.cayenne.reflect.PropertyUtils;
/**
* Various utils for processing persistent objects and their properties
* <p>
* <i>DataObjects and Primary Keys: All methods that allow to extract primary key values
* or use primary keys to find objects are provided for convenience. Still the author's
* belief is that integer sequential primary keys are meaningless in the object model and
* are pure database artifacts. Therefore relying heavily on direct access to PK provided
* via this class (or other such Cayenne API) is not a clean design practice in many
* cases, and sometimes may actually lead to security issues. </i>
* </p>
*
* @since 3.1 its predecessor was called DataObjectUtils
*/
public class Cayenne {
/**
* A special property denoting a size of the to-many collection, when encountered at
* the end of the path</p>
*/
final static String PROPERTY_COLLECTION_SIZE = "@size";
/**
* Returns mapped ObjEntity for object. If an object is transient or is not mapped
* returns null.
*/
public static ObjEntity getObjEntity(Persistent p) {
return (p.getObjectContext() != null) ? p
.getObjectContext()
.getEntityResolver()
.lookupObjEntity(p) : null;
}
/**
* Returns class descriptor for the object or null if the object is not registered
* with an ObjectContext or descriptor was not found.
*/
public static ClassDescriptor getClassDescriptor(Persistent object) {
ObjectContext context = object.getObjectContext();
if (context == null) {
return null;
}
return context.getEntityResolver().getClassDescriptor(
object.getObjectId().getEntityName());
}
/**
* Returns property descriptor for specified property.
*
* @param properyName path to the property
* @return property descriptor, <code>null</code> if not found
*/
public static Property getProperty(Persistent object, String properyName) {
ClassDescriptor descriptor = getClassDescriptor(object);
if (descriptor == null) {
return null;
}
return descriptor.getProperty(properyName);
}
/**
* 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)Cayenne.readNestedProperty(artist, "name");</code><br>
* <br>
* </li>
* <li>Read an object related to this object:<br>
* <code>Gallery g = (Gallery)Cayenne.readNestedProperty(paintingInfo, "toPainting.toGallery");</code>
* <br>
* <br>
* </li>
* <li>Read a property of an object related to this object: <br>
* <code>String name = (String)Cayenne.readNestedProperty(painting, "toArtist.artistName");</code>
* <br>
* <br>
* </li>
* <li>Read to-many relationship list:<br>
* <code>List exhibits = (List)Cayenne.readNestedProperty(painting, "toGallery.exhibitArray");</code>
* <br>
* <br>
* </li>
* <li>Read to-many relationship in the middle of the path:<br>
* <code>List<String> names = (List<String>)Cayenne.readNestedProperty(artist, "paintingArray.paintingName");</code>
* <br>
* <br>
* </li>
* </ul>
*/
public static Object readNestedProperty(Object o, String path) {
if (o == null) {
return null;
}
else if (o instanceof DataObject) {
return ((DataObject) o).readNestedProperty(path);
}
else if (o instanceof Collection<?>) {
// This allows people to put @size at the end of a property
// path and be able to find out the size of a relationship.
Collection<?> collection = (Collection<?>) o;
if (path.equals(PROPERTY_COLLECTION_SIZE)) {
return collection.size();
}
// Support for collection property in the middle of the path
Collection<Object> result = o instanceof List<?>
? new ArrayList<Object>()
: new HashSet<Object>();
for (Object item : collection) {
if (item instanceof DataObject) {
DataObject cdo = (DataObject) item;
Object rest = cdo.readNestedProperty(path);
if (rest instanceof Collection<?>) {
// We don't want nested collections. E.g.
// readNestedProperty("paintingArray.paintingTitle") should return
// List<String>
result.addAll((Collection<?>) rest);
}
else {
result.add(rest);
}
}
}
return result;
}
if ((null == path) || (0 == path.length())) {
throw new IllegalArgumentException(
"the path must be supplied in order to lookup a nested property");
}
int dotIndex = path.indexOf('.');
if (0 == dotIndex) {
throw new IllegalArgumentException(
"the path is invalid because it starts with a period character");
}
if (dotIndex == path.length() - 1) {
throw new IllegalArgumentException(
"the path is invalid because it ends with a period character");
}
if (-1 == dotIndex) {
return readSimpleProperty(o, path);
}
String path0 = path.substring(0, dotIndex);
String pathRemainder = path.substring(dotIndex + 1);
// this is copied from the old code where the placement of a plus
// character at the end of a segment of a property path would
// simply strip out the plus. I am not entirely sure why this is
// done. See unit test 'testReadNestedPropertyToManyInMiddle1'.
if ('+' == path0.charAt(path0.length() - 1)) {
path0 = path0.substring(0, path0.length() - 1);
}
Object property = readSimpleProperty(o, path0);
return readNestedProperty(property, pathRemainder);
}
private static final Object readSimpleProperty(Object o, String propertyName) {
if (o instanceof Persistent) {
Property property = getProperty((Persistent) o, propertyName);
if (property != null) {
return property.readProperty(o);
}
}
// handling non-persistent property
return PropertyUtils.getProperty(o, propertyName);
}
/**
* Constructs a dotted path from a list of strings. Useful for creating
* more complex paths while preserving compilation safety. For example,
* instead of saying:
* <p>
* <pre>orderings.add(new Ordering("department.name", SortOrder.ASCENDING));</pre>
* <p>
* You can use makePath() with the constants generated by Cayenne Modeler:
* <p>
* <pre>orderings.add(new Ordering(Cayenne.makePath(USER.DEPARTMENT_PROPERTY, Department.NAME_PROPERTY), SortOrder.ASCENDING));</pre>
* <p>
* @param pathParts The varargs list of paths to join.
* @return A string of all the paths joined by a "." (used by Cayenne in queries and orderings).
* <p>
* @since 3.1
*/
public static String makePath(String...pathParts) {
StringBuilder builder = new StringBuilder();
String separator = "";
for (String path : pathParts) {
builder.append(separator).append(path);
separator = ".";
}
return builder.toString();
}
/**
* Returns an int primary key value for a persistent object. Only works for single
* column numeric primary keys. If an object is transient or has an ObjectId that can
* not be converted to an int PK, an exception is thrown.
*/
public static long longPKForObject(Persistent dataObject) {
Object value = pkForObject(dataObject);
if (!(value instanceof Number)) {
throw new CayenneRuntimeException("PK is not a number: "
+ dataObject.getObjectId());
}
return ((Number) value).longValue();
}
/**
* Returns an int primary key value for a persistent object. Only works for single
* column numeric primary keys. If an object is transient or has an ObjectId that can
* not be converted to an int PK, an exception is thrown.
*/
public static int intPKForObject(Persistent dataObject) {
Object value = pkForObject(dataObject);
if (!(value instanceof Number)) {
throw new CayenneRuntimeException("PK is not a number: "
+ dataObject.getObjectId());
}
return ((Number) value).intValue();
}
/**
* Returns a primary key value for a persistent object. Only works for single column
* primary keys. If an object is transient or has a compound ObjectId, an exception is
* thrown.
*/
public static Object pkForObject(Persistent dataObject) {
Map<String, Object> pk = extractObjectId(dataObject);
if (pk.size() != 1) {
throw new CayenneRuntimeException("Expected single column PK, got "
+ pk.size()
+ " columns, ID: "
+ pk);
}
return pk.entrySet().iterator().next().getValue();
}
/**
* Returns a primary key map for a persistent object. This method is the most generic
* out of all methods for primary key retrieval. It will work for all possible types
* of primary keys. If an object is transient, an exception is thrown.
*/
public static Map<String, Object> compoundPKForObject(Persistent dataObject) {
return Collections.unmodifiableMap(extractObjectId(dataObject));
}
static Map<String, Object> extractObjectId(Persistent dataObject) {
if (dataObject == null) {
throw new IllegalArgumentException("Null DataObject");
}
ObjectId id = dataObject.getObjectId();
if (!id.isTemporary()) {
return id.getIdSnapshot();
}
// replacement ID is more tricky... do some sanity check...
if (id.isReplacementIdAttached()) {
ObjEntity objEntity = dataObject
.getObjectContext()
.getEntityResolver()
.lookupObjEntity(dataObject);
if (objEntity != null) {
DbEntity entity = objEntity.getDbEntity();
if (entity != null && entity.isFullReplacementIdAttached(id)) {
return id.getReplacementIdMap();
}
}
}
throw new CayenneRuntimeException("Can't get primary key from temporary id.");
}
/**
* Returns an object matching an int primary key. If the object is mapped to use
* non-integer PK or a compound PK, CayenneRuntimeException is thrown.
* <p>
* If this object is already cached in the ObjectStore, it is returned without a
* query. Otherwise a query is built and executed against the database.
* </p>
*
* @see #objectForPK(ObjectContext, ObjectId)
*/
public static <T> T objectForPK(
ObjectContext context,
Class<T> dataObjectClass,
int pk) {
return (T) objectForPK(context, buildId(context, dataObjectClass, Integer
.valueOf(pk)));
}
/**
* Returns an object matching an Object primary key. If the object is mapped to use a
* compound PK, CayenneRuntimeException is thrown.
* <p>
* If this object is already cached in the ObjectStore, it is returned without a
* query. Otherwise a query is built and executed against the database.
* </p>
*
* @see #objectForPK(ObjectContext, ObjectId)
*/
public static <T> T objectForPK(
ObjectContext context,
Class<T> dataObjectClass,
Object pk) {
return (T) objectForPK(context, buildId(context, dataObjectClass, pk));
}
/**
* Returns an object matching a primary key. PK map parameter should use database PK
* column names as keys.
* <p>
* If this object is already cached in the ObjectStore, it is returned without a
* query. Otherwise a query is built and executed against the database.
* </p>
*
* @see #objectForPK(ObjectContext, ObjectId)
*/
public static <T> T objectForPK(
ObjectContext context,
Class<T> dataObjectClass,
Map<String, ?> pk) {
ObjEntity entity = context.getEntityResolver().lookupObjEntity(dataObjectClass);
if (entity == null) {
throw new CayenneRuntimeException("Non-existent ObjEntity for class: "
+ dataObjectClass);
}
return (T) objectForPK(context, new ObjectId(entity.getName(), pk));
}
/**
* Returns an object matching an int primary key. If the object is mapped to use
* non-integer PK or a compound PK, CayenneRuntimeException is thrown.
* <p>
* If this object is already cached in the ObjectStore, it is returned without a
* query. Otherwise a query is built and executed against the database.
* </p>
*
* @see #objectForPK(ObjectContext, ObjectId)
*/
public static Object objectForPK(ObjectContext context, String objEntityName, int pk) {
return objectForPK(context, buildId(context, objEntityName, Integer.valueOf(pk)));
}
/**
* Returns an object matching an Object primary key. If the object is mapped to use a
* compound PK, CayenneRuntimeException is thrown.
* <p>
* If this object is already cached in the ObjectStore, it is returned without a
* query. Otherwise a query is built and executed against the database.
* </p>
*
* @see #objectForPK(ObjectContext, ObjectId)
*/
public static Object objectForPK(
ObjectContext context,
String objEntityName,
Object pk) {
return objectForPK(context, buildId(context, objEntityName, pk));
}
/**
* Returns an object matching a primary key. PK map parameter should use database PK
* column names as keys.
* <p>
* If this object is already cached in the ObjectStore, it is returned without a
* query. Otherwise a query is built and executed against the database.
* </p>
*
* @see #objectForPK(ObjectContext, ObjectId)
*/
public static Object objectForPK(
ObjectContext context,
String objEntityName,
Map<String, ?> pk) {
if (objEntityName == null) {
throw new IllegalArgumentException("Null ObjEntity name.");
}
return objectForPK(context, new ObjectId(objEntityName, pk));
}
/**
* Returns an object matching ObjectId. If this object is already cached in the
* ObjectStore, it is returned without a query. Otherwise a query is built and
* executed against the database.
*
* @return A persistent object that matched the id, null if no matching objects were
* found
* @throws CayenneRuntimeException if more than one object matched ObjectId.
*/
public static Object objectForPK(ObjectContext context, ObjectId id) {
return objectForQuery(context, new ObjectIdQuery(id, false, ObjectIdQuery.CACHE));
}
/**
* Returns an object or a DataRow that is a result of a given query. If query returns
* more than one object, an exception is thrown. If query returns no objects, null is
* returned.
*/
public static Object objectForQuery(ObjectContext context, Query query) {
List<?> objects = context.performQuery(query);
if (objects.size() == 0) {
return null;
}
else if (objects.size() > 1) {
throw new CayenneRuntimeException(
"Expected zero or one object, instead query matched: "
+ objects.size());
}
return objects.get(0);
}
static ObjectId buildId(ObjectContext context, String objEntityName, Object pk) {
if (pk == null) {
throw new IllegalArgumentException("Null PK");
}
if (objEntityName == null) {
throw new IllegalArgumentException("Null ObjEntity name.");
}
ObjEntity entity = context.getEntityResolver().getObjEntity(objEntityName);
if (entity == null) {
throw new CayenneRuntimeException("Non-existent ObjEntity: " + objEntityName);
}
Collection<String> pkAttributes = entity.getPrimaryKeyNames();
if (pkAttributes.size() != 1) {
throw new CayenneRuntimeException("PK contains "
+ pkAttributes.size()
+ " columns, expected 1.");
}
String attr = pkAttributes.iterator().next();
return new ObjectId(objEntityName, attr, pk);
}
static ObjectId buildId(ObjectContext context, Class<?> dataObjectClass, Object pk) {
if (pk == null) {
throw new IllegalArgumentException("Null PK");
}
if (dataObjectClass == null) {
throw new IllegalArgumentException("Null DataObject class.");
}
ObjEntity entity = context.getEntityResolver().lookupObjEntity(dataObjectClass);
if (entity == null) {
throw new CayenneRuntimeException("Unmapped DataObject Class: "
+ dataObjectClass.getName());
}
Collection<String> pkAttributes = entity.getPrimaryKeyNames();
if (pkAttributes.size() != 1) {
throw new CayenneRuntimeException("PK contains "
+ pkAttributes.size()
+ " columns, expected 1.");
}
String attr = pkAttributes.iterator().next();
return new ObjectId(entity.getName(), attr, pk);
}
private Cayenne() {
}
}