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 }