org.onebusaway.collections.PropertyPathExpression Maven / Gradle / Ivy
/**
* Copyright (C) 2011 Brian Ferris
* Copyright (C) 2011 Google, Inc.
*
* Licensed 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.onebusaway.collections;
import java.lang.reflect.Method;
/**
* Simple support for Java bean property path expression parsing and evaluation.
*
* Consider a simple Order bean class with a property named {@code customer} of
* type Customer that has its own property named {@code name}. A path expression
* of {@code customer.name}, evaluated on an Order instance will internally make
* a call to the {@code getCustomer()} method on the Order object, and then make
* a call to the {@code getName()} method on the Customer object returned in the
* previous method call. The result of the expression will be the cusomter's
* name.
*
* Instances of {@link PropertyPathExpression} are thread-safe for concurrent
* use across threads with one restriction. A call to {@link #initialize(Class)}
* must be made in advance of concurrent access to ensure that class
* introspection has been completed.
*
* @author bdferris
*
*/
public final class PropertyPathExpression {
private String[] _properties;
private transient Method[] _methods = null;
/**
* A static convenience method for evaluating a property path expression on a
* target object. If you need to repeatedly evaluate the same property path
* expression, consider creating a {@link PropertyPathExpression} object
* directly so that bean introspection information can be cached.
*
* @param target the target bean instance to evaluate against
* @param query the property path expression to evaluate
* @return the result of the evaluation of the property path expression
*/
public static Object evaluate(Object target, String query) {
return new PropertyPathExpression(query).invoke(target);
}
/**
*
* @param query the property path expression to evaluate
*/
public PropertyPathExpression(String query) {
_properties = query.split("\\.");
}
public String getPath() {
StringBuilder b = new StringBuilder();
for (int i = 0; i < _properties.length; i++) {
if (i > 0)
b.append('.');
b.append(_properties[i]);
}
return b.toString();
}
/**
* Opportunistically complete and cache bean introspection given a source
* value target type.
*
* @param sourceValueType the class of objects that will be passed in calls to
* {@link #invoke(Object)}
* @return the final return type of the evaluated path expression
* @throws IllegalStateException on introspection errors
*/
public Class> initialize(Class> sourceValueType) {
if (_methods != null) {
if (_methods.length == 0)
return sourceValueType;
return _methods[_methods.length - 1].getReturnType();
}
_methods = new Method[_properties.length];
for (int i = 0; i < _properties.length; i++) {
Method m = null;
String name = _properties[i];
String methodName = "get" + name.substring(0, 1).toUpperCase()
+ name.substring(1);
try {
m = sourceValueType.getMethod(methodName);
} catch (Exception ex) {
throw new IllegalStateException("error introspecting class: "
+ sourceValueType, ex);
}
if (m == null)
throw new IllegalStateException("could not find property: "
+ _properties[i]);
m.setAccessible(true);
_methods[i] = m;
sourceValueType = m.getReturnType();
}
return sourceValueType;
}
/**
* Returns the type of the parent class containing the property to be
* evaluated. For simple property path expressions containing just one
* property, the parent class will be equal to the "sourceValueType"
* parameter. For compound property path expressions, the parent class is
* equal to the class from which the property value will ultimately be
* accessed.
*
* @param sourceValueType
* @return
*/
public Class> getParentType(Class> sourceValueType) {
initialize(sourceValueType);
if (_methods.length < 2) {
return sourceValueType;
}
return _methods[_methods.length - 2].getReturnType();
}
/**
* @return the last property in the compound property path expression
*/
public String getLastProperty() {
return _properties[_properties.length - 1];
}
/**
* Invoke the property path expression against the specified object value
*
* @param value the target bean to start the property path expression against
* @return the result of the property path expression evaluation
* @throws IllegalStateException on introspection and evaluation errors
*/
public Object invoke(Object value) {
if (_methods == null)
initialize(value.getClass());
for (int i = 0; i < _properties.length; i++) {
Method m = _methods[i];
try {
value = m.invoke(value);
} catch (Exception ex) {
throw new IllegalStateException("error invoking property reader: obj="
+ value + " property=" + _properties[i], ex);
}
}
return value;
}
}