/*****************************************************************
* 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.reflect;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.map.Entity;
/**
* Utility methods to quickly access object properties. This class supports
* simple and nested properties and also conversion of property values to match
* property type. No converter customization is provided yet, so only basic
* converters for Strings, Numbers and primitives are available.
*
* @since 1.2
*/
public class PropertyUtils {
private static final ConcurrentMap<String, Accessor> PATH_ACCESSORS = new ConcurrentHashMap<>();
private static final ConcurrentMap<Class<?>, ConcurrentMap<String, Accessor>> SEGMENT_ACCESSORS = new ConcurrentHashMap<>();
/**
* Compiles an accessor that can be used for fast access for the nested
* property of the objects of a given class.
*
* @since 4.0
*/
public static Accessor accessor(String nestedPropertyName) {
if (nestedPropertyName == null) {
throw new IllegalArgumentException("Null property name.");
}
if (nestedPropertyName.length() == 0) {
throw new IllegalArgumentException("Empty property name.");
}
// PathAccessor is simply a chain of path segment wrappers. The actual
// accessor is resolved (with caching) during evaluation. Otherwise we
// won't be able to handle subclasses of declared property types...
// TODO: perhaps Java 7 MethodHandles are the answer to truly "compiled"
// path accessor?
return compilePathAccessor(nestedPropertyName);
}
static Accessor compilePathAccessor(String path) {
Accessor accessor = PATH_ACCESSORS.get(path);
if (accessor == null) {
int dot = path.indexOf(Entity.PATH_SEPARATOR);
if (dot == 0 || dot == path.length() - 1) {
throw new IllegalArgumentException("Invalid path: " + path);
}
String segment = dot < 0 ? path : path.substring(0, dot);
Accessor remainingAccessor = dot < 0 ? null : compilePathAccessor(path.substring(dot + 1));
Accessor newAccessor = new PathAccessor(segment, remainingAccessor);
Accessor existingAccessor = PATH_ACCESSORS.putIfAbsent(path, newAccessor);
accessor = existingAccessor != null ? existingAccessor : newAccessor;
}
return accessor;
}
static Accessor getOrCreateSegmentAccessor(Class<?> objectClass, String propertyName) {
ConcurrentMap<String, Accessor> forType = SEGMENT_ACCESSORS.get(objectClass);
if (forType == null) {
ConcurrentMap<String, Accessor> newPropAccessors = new ConcurrentHashMap<String, Accessor>();
ConcurrentMap<String, Accessor> existingPropAccessors = SEGMENT_ACCESSORS.putIfAbsent(objectClass,
newPropAccessors);
forType = existingPropAccessors != null ? existingPropAccessors : newPropAccessors;
}
Accessor a = forType.get(propertyName);
if (a == null) {
Accessor newA = createSegmentAccessor(objectClass, propertyName);
Accessor existingA = forType.putIfAbsent(propertyName, newA);
a = existingA != null ? existingA : newA;
}
return a;
}
static Accessor createSegmentAccessor(Class<?> objectClass, String propertyName) {
if (Map.class.isAssignableFrom(objectClass)) {
return new MapAccessor(propertyName);
} else {
return new BeanAccessor(objectClass, propertyName, null);
}
}
/**
* Returns object property using JavaBean-compatible introspection with one
* addition - a property can be a dot-separated property name path.
*/
public static Object getProperty(Object object, String nestedPropertyName) throws CayenneRuntimeException {
return accessor(nestedPropertyName).getValue(object);
}
/**
* Sets object property using JavaBean-compatible introspection with one
* addition - a property can be a dot-separated property name path. Before
* setting a value attempts to convert it to a type compatible with the
* object property. Automatic conversion is supported between strings and
* basic types like numbers or primitives.
*/
public static void setProperty(Object object, String nestedPropertyName, Object value)
throws CayenneRuntimeException {
accessor(nestedPropertyName).setValue(object, value);
}
/**
* "Normalizes" passed type, converting primitive types to their object
* counterparts.
*/
static Class<?> normalizeType(Class<?> type) {
if (type.isPrimitive()) {
String className = type.getName();
if ("byte".equals(className)) {
return Byte.class;
} else if ("int".equals(className)) {
return Integer.class;
} else if ("long".equals(className)) {
return Long.class;
} else if ("short".equals(className)) {
return Short.class;
} else if ("char".equals(className)) {
return Character.class;
} else if ("double".equals(className)) {
return Double.class;
} else if ("float".equals(className)) {
return Float.class;
} else if ("boolean".equals(className)) {
return Boolean.class;
}
}
return type;
}
/**
* Returns default value that should be used for nulls. For non-primitive
* types, null is returned. For primitive types a default such as zero or
* false is returned.
*/
static Object defaultNullValueForType(Class<?> type) {
if (type != null && type.isPrimitive()) {
String className = type.getName();
if ("byte".equals(className)) {
return (byte) 0;
} else if ("int".equals(className)) {
return 0;
} else if ("long".equals(className)) {
return 0L;
} else if ("short".equals(className)) {
return (short) 0;
} else if ("char".equals(className)) {
return (char) 0;
} else if ("double".equals(className)) {
return 0.0d;
} else if ("float".equals(className)) {
return 0.0f;
} else if ("boolean".equals(className)) {
return Boolean.FALSE;
}
}
return null;
}
private PropertyUtils() {
super();
}
static final class PathAccessor implements Accessor {
private static final long serialVersionUID = 2056090443413498626L;
private final String segmentName;
private final Accessor nextAccessor;
public PathAccessor(String segmentName, Accessor nextAccessor) {
// trim outer join component
if(segmentName.endsWith(Entity.OUTER_JOIN_INDICATOR)) {
this.segmentName = segmentName.substring(0, segmentName.length() - 1);
} else {
this.segmentName = segmentName;
}
this.nextAccessor = nextAccessor;
}
@Override
public String getName() {
return segmentName;
}
@Override
public Object getValue(Object object) throws PropertyException {
if (object == null) {
return null;
}
Object value = getOrCreateSegmentAccessor(object.getClass(), segmentName).getValue(object);
return nextAccessor != null ? nextAccessor.getValue(value) : value;
}
@Override
public void setValue(Object object, Object newValue) throws PropertyException {
if (object == null) {
return;
}
Accessor segmentAccessor = getOrCreateSegmentAccessor(object.getClass(), segmentName);
if (nextAccessor != null) {
nextAccessor.setValue(segmentAccessor.getValue(object), newValue);
} else {
segmentAccessor.setValue(object, newValue);
}
}
}
}