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 target the given Object to extract property from.
194       * @return the value of the given property name given target.
195       * @throws IntrospectionError if target object does not have a property with a matching name.
196       */
197      public <T> T propertyValueOf(String propertyName, Class<T> clazz, Object target) {
198        // returns null if target is null as we can't extract a property from a null object
199        if (target == null) return null;
200    
201        if (isNestedProperty(propertyName)) {
202          String firstPropertyName = popPropertyNameFrom(propertyName);
203          Object propertyValue = propertyValue(firstPropertyName, Object.class, target);
204          // extract next sub-property values until reaching the last sub-property
205          return propertyValueOf(nextPropertyNameFrom(propertyName), clazz, propertyValue);
206        }
207        return propertyValue(propertyName, clazz, target);
208      }
209    
210    }