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    }