| /***************************************************************** |
| * 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() { |
| } |
| } |