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 }