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