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.emptyList;
020 import static java.util.Collections.unmodifiableList;
021 import static org.fest.util.Collections.isEmpty;
022 import static org.fest.util.Collections.nonNullElements;
023 import static org.fest.util.Introspection.descriptorForProperty;
024
025 import java.beans.PropertyDescriptor;
026 import java.util.ArrayList;
027 import java.util.Collection;
028 import java.util.List;
029
030 import org.fest.util.IntrospectionError;
031 import org.fest.util.VisibleForTesting;
032
033 /**
034 * Utility methods for properties access.
035 *
036 * @author Joel Costigliola
037 * @author Alex Ruiz
038 * @author Nicolas François
039 */
040 public class PropertySupport {
041
042 private static final String SEPARATOR = ".";
043
044 private static final PropertySupport INSTANCE = new PropertySupport();
045
046 /**
047 * Returns the singleton instance of this class.
048 * @return the singleton instance of this class.
049 */
050 public static PropertySupport instance() {
051 return INSTANCE;
052 }
053
054 @VisibleForTesting
055 JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor();
056
057 @VisibleForTesting
058 PropertySupport() {}
059
060 /**
061 * Returns a <code>{@link List}</code> containing the values of the given property name, from the elements of the
062 * given <code>{@link Collection}</code>. If the given {@code Collection} is empty or {@code null}, this method will
063 * return an empty {@code List}. This method supports nested properties (e.g. "address.street.number").
064 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
065 * for {@code null} or empty.
066 * @param target the given {@code Collection}.
067 * @return a {@code List} containing the values of the given property name, from the elements of the given
068 * {@code Collection}.
069 * @throws IntrospectionError if an element in the given {@code Collection} does not have a property with a matching
070 * name.
071 */
072 public List<Object> propertyValues(String propertyName, Collection<?> target) {
073 // ignore null elements as we can't extract a property from a null object
074 Collection<?> cleanedUp = nonNullElements(target);
075 if (isEmpty(cleanedUp)) return emptyList();
076 if (isNestedProperty(propertyName)) {
077 String firstPropertyName = popPropertyNameFrom(propertyName);
078 List<Object> propertyValues = propertyValues(firstPropertyName, cleanedUp);
079 // extract next sub-property values until reaching the last sub-property
080 return propertyValues(nextPropertyNameFrom(propertyName), propertyValues);
081 }
082 return simplePropertyValues(propertyName, cleanedUp);
083 }
084
085 /**
086 * Static variant of {@link #propertyValue(String, Object)} for synthetic sugar.
087 * <p>
088 * Returns a <code>{@link List}</code> containing the values of the given property name, from the elements of the
089 * given <code>{@link Collection}</code>. If the given {@code Collection} is empty or {@code null}, this method will
090 * return an empty {@code List}. This method supports nested properties (e.g. "address.street.number").
091 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
092 * for {@code null} or empty.
093 * @param target the given {@code Collection}.
094 * @return a {@code List} containing the values of the given property name, from the elements of the given
095 * {@code Collection}.
096 * @throws IntrospectionError if an element in the given {@code Collection} does not have a property with a matching
097 * name.
098 */
099 public static List<Object> propertyValuesOf(String propertyName, Collection<?> target) {
100 return instance().propertyValues(propertyName, target);
101 }
102
103 /**
104 * Returns a <code>{@link List}</code> containing the values of the given property name, from the elements of the
105 * given array. If the given array is empty or {@code null}, this method will return an empty {@code List}. This
106 * method supports nested properties (e.g. "address.street.number").
107 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
108 * for {@code null} or empty.
109 * @param target the given array.
110 * @return a {@code List} containing the values of the given property name, from the elements of the given array.
111 * @throws IntrospectionError if an element in the given array does not have a property with a matching name.
112 */
113 public static List<Object> propertyValuesOf(String propertyName, Object[] target) {
114 return instance().propertyValues(propertyName, asList(target));
115 }
116
117 /**
118 * Static varient of {@link #propertyValue(String, Object, Class)} for synthetic sugar.
119 * <p>
120 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
121 * for {@code null} or empty.
122 * @param target the given object
123 * @param clazz type of property
124 * @return a the values of the given property name
125 * @throws IntrospectionError if the given target does not have a property with a matching name.
126 */
127 public static <T> T propertyValueOf(String propertyName, Object target, Class<T> clazz){
128 return instance().propertyValue(propertyName, target, clazz);
129 }
130
131 /**
132 * Return the value of property from a target object.
133 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
134 * for {@code null} or empty.
135 * @param target the given object
136 * @param clazz type of property
137 * @return a the values of the given property name
138 * @throws IntrospectionError if the given target does not have a property with a matching name.
139 */
140 @SuppressWarnings("unchecked")
141 public <T> T propertyValue(String propertyName, Object target, Class<T> clazz){
142 return (T) propertyValue(propertyName, target);
143 }
144
145 private List<Object> simplePropertyValues(String propertyName, Collection<?> target) {
146 List<Object> propertyValues = new ArrayList<Object>();
147 for (Object e : target)
148 propertyValues.add(propertyValue(propertyName, e));
149 return unmodifiableList(propertyValues);
150 }
151
152 private String popPropertyNameFrom(String propertyNameChain) {
153 if (!isNestedProperty(propertyNameChain)) return propertyNameChain;
154 return propertyNameChain.substring(0, propertyNameChain.indexOf(SEPARATOR));
155 }
156
157 private String nextPropertyNameFrom(String propertyNameChain) {
158 if (!isNestedProperty(propertyNameChain)) return "";
159 return propertyNameChain.substring(propertyNameChain.indexOf(SEPARATOR) + 1);
160 }
161
162 /*
163 * isNestedProperty("address.street"); // true isNestedProperty("address.street.name"); // true
164 * isNestedProperty("person"); // false isNestedProperty(".name"); // false isNestedProperty("person."); // false
165 * isNestedProperty("person.name."); // false isNestedProperty(".person.name"); // false isNestedProperty("."); //
166 * false isNestedProperty(""); // false
167 */
168 private boolean isNestedProperty(String propertyName) {
169 return propertyName.contains(SEPARATOR) && !propertyName.startsWith(SEPARATOR) && !propertyName.endsWith(SEPARATOR);
170 }
171
172 private Object propertyValue(String propertyName, Object target) {
173 PropertyDescriptor descriptor = descriptorForProperty(propertyName, target);
174 try {
175 return javaBeanDescriptor.invokeReadMethod(descriptor, target);
176 } catch (Throwable unexpected) {
177 String msg = format("Unable to obtain the value of the property <'%s'> from <%s>", propertyName, target);
178 throw new IntrospectionError(msg, unexpected);
179 }
180 }
181
182 /**
183 * Returns the value of the given property name given target. If the given object is {@code null}, this method will
184 * return null.<br>
185 * This method supports nested properties (e.g. "address.street.number").
186 * @param propertyName the name of the property. It may be a nested property. It is left to the clients to validate
187 * for {@code null} or empty.
188 * @param target the given Object to extract property from.
189 * @return the value of the given property name given target.
190 * @throws IntrospectionError if target object does not have a property with a matching name.
191 */
192 public Object propertyValueOf(String propertyName, Object target) {
193 // returns null if target is null as we can't extract a property from a null object
194 if (target == null) return null;
195
196 if (isNestedProperty(propertyName)) {
197 String firstPropertyName = popPropertyNameFrom(propertyName);
198 Object propertyValue = propertyValue(firstPropertyName, target);
199 // extract next sub-property values until reaching the last sub-property
200 return propertyValueOf(nextPropertyNameFrom(propertyName), propertyValue);
201 }
202 return propertyValue(propertyName, target);
203 }
204
205 }