/* * Created on Jun 26, 2010 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * * Copyright @2010-2013 the original author or authors. */ package org.fest.assertions; import org.fest.util.IntrospectionError; import org.fest.util.Preconditions; import org.fest.util.VisibleForTesting; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.beans.PropertyDescriptor; import java.util.Collection; import java.util.List; import static org.fest.util.Collections.isNullOrEmpty; import static org.fest.util.Collections.nonNullElementsIn; import static org.fest.util.Introspection.getProperty; import static org.fest.util.Lists.emptyList; import static org.fest.util.Lists.newArrayList; import static org.fest.util.Preconditions.checkNotNull; import static org.fest.util.Strings.quote; /** * Utility methods for properties access. * * @author Joel Costigliola * @author Alex Ruiz * @since 1.3 */ final class PropertySupport { private static final String SEPARATOR = "."; private static final PropertySupport INSTANCE = new PropertySupport(); private final JavaBeanDescriptor javaBeanDescriptor; private PropertySupport() { this(new JavaBeanDescriptor()); } PropertySupport(@Nonnull JavaBeanDescriptor javaBeanDescriptor) { this.javaBeanDescriptor = javaBeanDescriptor; } static @Nonnull PropertySupport instance() { return INSTANCE; } /** * Returns a list containing the values of the given property name, from the elements of the given collection. If the * given collection is empty or {@code null}, this method will return an empty collection. * <p/> * For example, given the nested property "address.street.number", this method will: * <ol> * <li>extract a collection of "address" from the given collection (remaining property is 'street.number')</li> * <li>extract a collection of "street" from the "address" collection (remaining property is 'number')</li> * <li>extract a collection of "number" from the "street" collection</li> * </ol> * * @param propertyName the name of the property. It may be a nested property. * @param target the given collection. * @return a list containing the values of the given property name, from the elements of the given collection. * @throws NullPointerException if given property name is {@code null}. * @throws IntrospectionError if an element in the given collection does not have a matching property. */ @Nonnull List<Object> propertyValues(@Nonnull String propertyName, @Nullable Collection<?> target) { if (isNullOrEmpty(target)) { return emptyList(); } // ignore null elements as we can't extract a property from a null object Collection<?> nonNullElements = nonNullElementsIn(target); if (isNestedProperty(propertyName)) { String firstProperty = firstPropertyIfNested(propertyName); List<Object> firstPropertyValues = propertyValues(firstProperty, nonNullElements); // extract next sub property values until reaching a last sub property return propertyValues(removeFirstPropertyIfNested(propertyName), firstPropertyValues); } return simplePropertyValues(propertyName, nonNullElements); } private @Nonnull List<Object> simplePropertyValues(@Nonnull String propertyName, @Nonnull Collection<?> target) { List<Object> propertyValues = newArrayList(); for (Object e : target) { if (e != null) { propertyValues.add(propertyValue(propertyName, e)); } } return propertyValues; } /** * Returns {@code true} if property is nested, {@code false} otherwise. * <p/> * Examples: * <pre> * isNestedProperty("address.street"); // true * isNestedProperty("address.street.name"); // true * isNestedProperty("person"); // false * isNestedProperty(".name"); // false * isNestedProperty("person."); // false * isNestedProperty("person.name."); // false * isNestedProperty(".person.name"); // false * isNestedProperty("."); // false * isNestedProperty(""); // false * </pre> * * @param propertyName the given property name. * @return {@code true} if property is nested, {@code false} otherwise. * @throws NullPointerException if given property name is {@code null}. */ boolean isNestedProperty(String propertyName) { Preconditions.checkNotNull(propertyName); return propertyName.contains(SEPARATOR) && !propertyName.startsWith(SEPARATOR) && !propertyName.endsWith(SEPARATOR); } /** * Removes the first property from the given property name only if the given property name belongs to a nested * property. For example, given the nested property "address.street.name", this method will return "street.name". This * method returns an empty {@code String} if the given property name does not belong to a nested property. * * @param propertyName the given property name. * @return the given property name without its first property, if the property name belongs to a nested property; * otherwise, it will return an empty {@code String}. * @throws NullPointerException if given property name is {@code null}. */ @Nonnull String removeFirstPropertyIfNested(@Nonnull String propertyName) { if (!isNestedProperty(propertyName)) { return ""; } return checkNotNull(propertyName.substring(propertyName.indexOf(SEPARATOR) + 1)); } /** * Returns the first property from the given property name only if the given property name belongs to a nested * property. For example, given the nested property "address.street.name", this method will return "address". This * method returns the given property name unchanged if it does not belong to a nested property. * * @param propertyName the given property name. * @return the first property from the given property name, if the property name belongs to a nested property; * otherwise, it will return the given property name unchanged. * @throws NullPointerException if given property name is {@code null}. */ @Nonnull String firstPropertyIfNested(@Nonnull String propertyName) { if (!isNestedProperty(propertyName)) { return propertyName; } return checkNotNull(propertyName.substring(0, propertyName.indexOf(SEPARATOR))); } private @Nullable Object propertyValue(@Nonnull String propertyName, @Nonnull Object target) { PropertyDescriptor descriptor = getProperty(propertyName, target); return propertyValue(descriptor, propertyName, target); } @VisibleForTesting @Nullable Object propertyValue( @Nonnull PropertyDescriptor descriptor, @Nonnull String propertyName, @Nonnull Object target) { try { return javaBeanDescriptor.invokeReadMethod(descriptor, target); } catch (Exception e) { throw new IntrospectionError("Unable to obtain the value in property " + quote(propertyName), e); } } }