All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fitbur.assertj.util.introspection.FieldSupport Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
/**
 * 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 2012-2016 the original author or authors.
 */
package com.fitbur.assertj.util.introspection;

import static java.lang.String.format;
import static java.lang.reflect.Modifier.isPublic;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static com.fitbur.assertj.util.ArrayWrapperList.wrap;
import static com.fitbur.assertj.util.IterableUtil.isNullOrEmpty;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Utility methods for fields access.
 *
 * @author Joel Costigliola
 */
public enum FieldSupport {

  EXTRACTION(true),
  EXTRACTION_OF_PUBLIC_FIELD_ONLY(false),
  COMPARISON(true);

  private static final String SEPARATOR = ".";

  private boolean allowUsingPrivateFields;

  /**
   * Returns the instance dedicated to extraction of fields.
   *
   * @return the instance dedicated to extraction of fields.
   */
  public static FieldSupport extraction() {
	return EXTRACTION;
  }

  /**
   * Returns the instance dedicated to comparison of fields.
   *
   * @return the instance dedicated to comparison of fields.
   */
  public static FieldSupport comparison() {
	return COMPARISON;
  }
  
  /**
   * Build a new {@link FieldSupport}
   *
   * @param allowUsingPrivateFields whether to read private fields or not.
   */
  FieldSupport(boolean allowUsingPrivateFields) {
	this.allowUsingPrivateFields = allowUsingPrivateFields;
  }
  
  public boolean isAllowedToUsePrivateFields() {
	return allowUsingPrivateFields;
  }

  /**
   * Sets whether the use of private fields is allowed.
   * If a method tries to extract/compare private fields and is not allowed to, it will fail with an exception.
   *
   * @param allowUsingPrivateFields allow private fields extraction and comparison. Default {@code true}.
   */
  public void setAllowUsingPrivateFields(boolean allowUsingPrivateFields) {
	this.allowUsingPrivateFields = allowUsingPrivateFields;
  }

  /**
   * Returns a {@link List} containing the values of the given field name, from the elements of the given
   * {@link Iterable}. If the given {@code Iterable} is empty or {@code null}, this method will return an
   * empty {@code List}. This method supports nested fields (e.g. "address.street.number").
   *
   * @param fieldName the name of the field. It may be a nested field. It is left to the clients to validate for
   *          {@code null} or empty.
   * @param fieldClass the expected type of the given field.
   * @param target the given {@code Iterable}.
   * @return an {@code Iterable} containing the values of the given field name, from the elements of the given
   *         {@code Iterable}.
   * @throws IntrospectionError if an element in the given {@code Iterable} does not have a field with a matching name.
   */
  public  List fieldValues(String fieldName, Class fieldClass, Iterable target) {
	if (isNullOrEmpty(target)) return emptyList();

	if (isNestedField(fieldName)) {
	  String firstFieldName = popFieldNameFrom(fieldName);
	  Iterable fieldValues = fieldValues(firstFieldName, Object.class, target);
	  // extract next sub-field values until reaching the last sub-field
	  return fieldValues(nextFieldNameFrom(fieldName), fieldClass, fieldValues);
	}
	return simpleFieldValues(fieldName, fieldClass, target);
  }

  public List fieldValues(String fieldName, Iterable target) {
	return fieldValues(fieldName, Object.class, target);
  }

  /**
   * Returns a {@link List} containing the values of the given field name, from the elements of the given
   * {@link Iterable}. If the given {@code Iterable} is empty or {@code null}, this method will return an
   * empty {@code List}. This method supports nested fields (e.g. "address.street.number").
   *
   * @param fieldName the name of the field. It may be a nested field. It is left to the clients to validate for
   *          {@code null} or empty.
   * @param fieldClass the expected type of the given field.
   * @param target the given {@code Iterable}.
   * @return an {@code Iterable} containing the values of the given field name, from the elements of the given
   *         {@code Iterable}.
   * @throws IntrospectionError if an element in the given {@code Iterable} does not have a field with a matching name.
   */
  public  List fieldValues(String fieldName, Class fieldClass, Object[] target) {
	return fieldValues(fieldName, fieldClass, wrap(target));
  }

  private  List simpleFieldValues(String fieldName, Class clazz, Iterable target) {
	List fieldValues = new ArrayList<>();
	for (Object e : target) {
	  fieldValues.add(e == null ? null : fieldValue(fieldName, clazz, e));
	}
	return unmodifiableList(fieldValues);
  }

  private String popFieldNameFrom(String fieldNameChain) {
	if (!isNestedField(fieldNameChain)) {
	  return fieldNameChain;
	}
	return fieldNameChain.substring(0, fieldNameChain.indexOf(SEPARATOR));
  }

  private String nextFieldNameFrom(String fieldNameChain) {
	if (!isNestedField(fieldNameChain)) {
	  return "";
	}
	return fieldNameChain.substring(fieldNameChain.indexOf(SEPARATOR) + 1);
  }

  /**
   * 
 isNestedField("address.street"); // true
   * isNestedField("address.street.name"); // true
   * isNestedField("person"); // false
   * isNestedField(".name"); // false
   * isNestedField("person."); // false
   * isNestedField("person.name."); // false
   * isNestedField(".person.name"); // false
   * isNestedField("."); // false
   * isNestedField(""); // false
*/ private boolean isNestedField(String fieldName) { return fieldName.contains(SEPARATOR) && !fieldName.startsWith(SEPARATOR) && !fieldName.endsWith(SEPARATOR); } /** * Return the value of field from a target object. *

* Return null if field is nested and one of the nested value is null, ex : *

 fieldValue(race.name, String.class, frodo); // will return null if frodo.race is null
* * @param fieldName the name of the field. It may be a nested field. It is left to the clients to validate for * {@code null} or empty. * @param target the given object * @param fieldClass type of field * @return the value of the given field name * @throws IntrospectionError if the given target does not have a field with a matching name. */ public T fieldValue(String fieldName, Class fieldClass, Object target) { if (target == null) return null; if (isNestedField(fieldName)) { String outerFieldName = popFieldNameFrom(fieldName); Object outerFieldValue = readSimpleField(outerFieldName, Object.class, target); // extract next sub-field values until reaching the last sub-field return fieldValue(nextFieldNameFrom(fieldName), fieldClass, outerFieldValue); } return readSimpleField(fieldName, fieldClass, target); } private T readSimpleField(String fieldName, Class clazz, Object target) { try { Object readField = FieldUtils.readField(target, fieldName, allowUsingPrivateFields); return clazz.cast(readField); } catch (ClassCastException e) { String msg = format("Unable to obtain the value of the field <'%s'> from <%s> - wrong field type specified <%s>", fieldName, target, clazz); throw new IntrospectionError(msg, e); } catch (IllegalAccessException iae) { String msg = format("Unable to obtain the value of the field <'%s'> from <%s>, check that field is public.", fieldName, target); throw new IntrospectionError(msg, iae); } catch (Throwable unexpected) { String msg = format("Unable to obtain the value of the field <'%s'> from <%s>", fieldName, target); throw new IntrospectionError(msg, unexpected); } } public boolean isAllowedToRead(Field field) { if (allowUsingPrivateFields) return true; // only read public field return isPublic(field.getModifiers()); } }