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    }