001 /*
002 * Created on Jun 26, 2010
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
005 * the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
010 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
011 * specific language governing permissions and limitations under the License.
012 *
013 * Copyright @2010-2011 the original author or authors.
014 */
015 package org.fest.assertions.internal;
016
017 import static java.lang.String.format;
018 import static java.util.Arrays.asList;
019 import static java.util.Collections.*;
020
021 import static org.fest.util.Collections.*;
022 import static org.fest.util.Introspection.descriptorForProperty;
023
024 import java.beans.PropertyDescriptor;
025 import java.util.ArrayList;
026 import java.util.Collection;
027 import java.util.List;
028
029 import org.fest.util.IntrospectionError;
030 import org.fest.util.VisibleForTesting;
031
032 /**
033 * Utility methods for properties access.
034 *
035 * @author Joel Costigliola
036 * @author Alex Ruiz
037 * @author Nicolas François
038 */
039 public class PropertySupport {
040
041 private static final String SEPARATOR = ".";
042
043 private static final PropertySupport INSTANCE = new PropertySupport();
044
045 /**
046 * Returns the singleton instance of this class.
047 * @return the singleton instance of this class.
048 */
049 public static PropertySupport instance() {
050 return INSTANCE;
051 }
052
053 @VisibleForTesting
054 JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor();
055
056 @VisibleForTesting
057 PropertySupport() {}
058
059 /**
060 * Returns a <code>{@link List}</code> containing the values of the given property name, from the elements of the
061 * given <code>{@link Collection}</code>. If the given {@code Collection} is empty or {@code null}, this method will
062 * return an empty {@code List}. This method supports nested properties (e.g. "address.street.number").
063 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
064 * for {@code null} or empty.
065 * @param target the given {@code Collection}.
066 * @return a {@code List} containing the values of the given property name, from the elements of the given
067 * {@code Collection}.
068 * @throws IntrospectionError if an element in the given {@code Collection} does not have a property with a matching
069 * name.
070 */
071 public <T> List<T> propertyValues(String propertyName, Class<T> clazz, Collection<?> target) {
072 // ignore null elements as we can't extract a property from a null object
073 Collection<?> cleanedUp = nonNullElements(target);
074 if (isEmpty(cleanedUp)) return emptyList();
075 if (isNestedProperty(propertyName)) {
076 String firstPropertyName = popPropertyNameFrom(propertyName);
077 List<Object> propertyValues = propertyValues(firstPropertyName, Object.class, cleanedUp);
078 // extract next sub-property values until reaching the last sub-property
079 return propertyValues(nextPropertyNameFrom(propertyName), clazz, propertyValues);
080 }
081 return simplePropertyValues(propertyName, clazz, cleanedUp);
082 }
083
084 /**
085 * Static variant of {@link #propertyValues(String, Class, Collection)} for syntactic sugar.
086 * <p>
087 * Returns a <code>{@link List}</code> containing the values of the given property name, from the elements of the
088 * given <code>{@link Collection}</code>. If the given {@code Collection} is empty or {@code null}, this method will
089 * return an empty {@code List}. This method supports nested properties (e.g. "address.street.number").
090 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
091 * for {@code null} or empty.
092 * @param target the given {@code Collection}.
093 * @return a {@code List} containing the values of the given property name, from the elements of the given
094 * {@code Collection}.
095 * @throws IntrospectionError if an element in the given {@code Collection} does not have a property with a matching
096 * name.
097 */
098 public static <T> List<T> propertyValuesOf(String propertyName, Collection<?> target, Class<T> clazz) {
099 return instance().propertyValues(propertyName, clazz, target);
100 }
101
102 /**
103 * Returns a <code>{@link List}</code> containing the values of the given property name, from the elements of the
104 * given array. If the given array is empty or {@code null}, this method will return an empty {@code List}. This
105 * method supports nested properties (e.g. "address.street.number").
106 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
107 * for {@code null} or empty.
108 * @param target the given array.
109 * @return a {@code List} containing the values of the given property name, from the elements of the given array.
110 * @throws IntrospectionError if an element in the given array does not have a property with a matching name.
111 */
112 public static <T> List<T> propertyValuesOf(String propertyName, Object[] target, Class<T> clazz) {
113 return instance().propertyValues(propertyName, clazz, asList(target));
114 }
115
116 /**
117 * Static variant of {@link #propertyValue(String, Class, Object)} for syntactic sugar.
118 * <p>
119 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
120 * for {@code null} or empty.
121 * @param target the given object
122 * @param clazz type of property
123 * @return a the values of the given property name
124 * @throws IntrospectionError if the given target does not have a property with a matching name.
125 */
126 public static <T> T propertyValueOf(String propertyName, Object target, Class<T> clazz) {
127 return instance().propertyValue(propertyName, clazz, target);
128 }
129
130 private <T> List<T> simplePropertyValues(String propertyName, Class<T> clazz, Collection<?> target) {
131 List<T> propertyValues = new ArrayList<T>();
132 for (Object e : target)
133 propertyValues.add(propertyValue(propertyName, clazz, e));
134 return unmodifiableList(propertyValues);
135 }
136
137 private String popPropertyNameFrom(String propertyNameChain) {
138 if (!isNestedProperty(propertyNameChain)) return propertyNameChain;
139 return propertyNameChain.substring(0, propertyNameChain.indexOf(SEPARATOR));
140 }
141
142 private String nextPropertyNameFrom(String propertyNameChain) {
143 if (!isNestedProperty(propertyNameChain)) return "";
144 return propertyNameChain.substring(propertyNameChain.indexOf(SEPARATOR) + 1);
145 }
146
147 /**
148 * <pre>
149 * isNestedProperty("address.street"); // true
150 * isNestedProperty("address.street.name"); // true
151 * isNestedProperty("person"); // false
152 * isNestedProperty(".name"); // false
153 * isNestedProperty("person."); // false
154 * isNestedProperty("person.name."); // false
155 * isNestedProperty(".person.name"); // false
156 * isNestedProperty("."); // false
157 * isNestedProperty(""); // false
158 * </pre>
159 */
160 private boolean isNestedProperty(String propertyName) {
161 return propertyName.contains(SEPARATOR) && !propertyName.startsWith(SEPARATOR) && !propertyName.endsWith(SEPARATOR);
162 }
163
164 /**
165 * Return the value of property from a target object.
166 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
167 * for {@code null} or empty.
168 * @param target the given object
169 * @param clazz type of property
170 * @return a the values of the given property name
171 * @throws IntrospectionError if the given target does not have a property with a matching name.
172 */
173 public <T> T propertyValue(String propertyName, Class<T> clazz, Object target) {
174 PropertyDescriptor descriptor = descriptorForProperty(propertyName, target);
175 try {
176 return clazz.cast(javaBeanDescriptor.invokeReadMethod(descriptor, target));
177 } catch (ClassCastException e) {
178 String msg = format("Unable to obtain the value of the property <'%s'> from <%s> - wrong property type specified <%s>",
179 propertyName, target, clazz);
180 throw new IntrospectionError(msg, e);
181 } catch (Throwable unexpected) {
182 String msg = format("Unable to obtain the value of the property <'%s'> from <%s>", propertyName, target);
183 throw new IntrospectionError(msg, unexpected);
184 }
185 }
186
187 /**
188 * Returns the value of the given property name given target. If the given object is {@code null}, this method will
189 * return null.<br>
190 * This method supports nested properties (e.g. "address.street.number").
191 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
192 * for {@code null} or empty.
193 * @param clazz the class of property.
194 * @param target the given Object to extract property from.
195 * @return the value of the given property name given target.
196 * @throws IntrospectionError if target object does not have a property with a matching name.
197 */
198 public <T> T propertyValueOf(String propertyName, Class<T> clazz, Object target) {
199 // returns null if target is null as we can't extract a property from a null object
200 if (target == null) return null;
201
202 if (isNestedProperty(propertyName)) {
203 String firstPropertyName = popPropertyNameFrom(propertyName);
204 Object propertyValue = propertyValue(firstPropertyName, Object.class, target);
205 // extract next sub-property values until reaching the last sub-property
206 return propertyValueOf(nextPropertyNameFrom(propertyName), clazz, propertyValue);
207 }
208 return propertyValue(propertyName, clazz, target);
209 }
210
211 }