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 }