/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ package org.apache.isis.applib.util; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Objects.ToStringHelper; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; public class ObjectContracts { //region > compare /** * Evaluates which of p and q is first. * * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! * @param propertyNames - the property name or names, CSV format. If multiple properties, use the {@link #compare(Object, Object, String...) varargs} overloaded version of this method. */ @Deprecated @SuppressWarnings("unchecked") public static <T> int compare(T p, T q, String propertyNames) { final Iterable<String> propertyNamesIter = csvToIterable(propertyNames); return compare(p, q, propertyNamesIter); } /** * Evaluates which of p and q is first. * * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! */ @Deprecated @SuppressWarnings("unchecked") public static <T> int compare(T p, T q, String... propertyNames) { final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames); return compare((T) p, (T) q, propertyNamesIter); } private static <T> int compare(final T p, final T q, final Iterable<String> propertyNamesIter) { final Iterable<Clause> clauses = clausesFor(propertyNamesIter); ComparisonChain chain = ComparisonChain.start(); for (final Clause clause : clauses) { final Comparable<T> propertyValueOfP = (Comparable<T>) clause.getValueOf(p); final Comparable<T> propertyValueOfQ = (Comparable<T>) clause.getValueOf(q); chain = chain.compare(propertyValueOfP, propertyValueOfQ, clause.getDirection().getOrdering()); } return chain.result(); } //endregion //region > compareBy /** * Returns a {@link Comparator} to evaluate objects by their property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! * @param propertyNames - the property name or names, CSV format. If multiple properties, use the {@link #compareBy(String...)} varargs} overloaded version of this method. */ @Deprecated @SuppressWarnings("unchecked") public static <T> Comparator<T> compareBy(final String propertyNames){ return new Comparator<T>() { @Override public int compare(T p, T q) { return ObjectContracts.compare(p, q, propertyNames); } }; } /** * Returns a {@link Comparator} to evaluate objects by their property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! */ @Deprecated @SuppressWarnings("unchecked") public static <T> Comparator<T> compareBy(final String... propertyNames){ return new Comparator<T>() { @Override public int compare(T p, T q) { return ObjectContracts.compare(p, q, propertyNames); } }; } //endregion //region > toString /** * Returns a string representation of the object consisting of the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! * @param propertyNames - the property name or names, CSV format. If multiple properties, use the {@link #toString(Object, String...)} varargs} overloaded version of this method. */ @Deprecated public static String toString(Object p, String propertyNames) { return new ObjectContracts().toStringOf(p, propertyNames); } /** * Returns a string representation of the object consisting of the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! */ @Deprecated public static String toString(Object p, String... propertyNames) { return new ObjectContracts().toStringOf(p, propertyNames); } //endregion //region > hashCode /** * Returns the hashCode for the object using the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! * @param propertyNames - the property name or names, CSV format. If multiple properties, use the {@link #hashCode(Object, String...)} varargs} overloaded version of this method. */ @Deprecated public static int hashCode(Object obj, String propertyNames) { final Iterable<String> propertyNamesIter = csvToIterable(propertyNames); return hashCode(obj, propertyNamesIter); } /** * Returns the hashCode for the object using the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! */ @Deprecated public static int hashCode(Object obj, String... propertyNames) { final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames); return hashCode(obj, propertyNamesIter); } private static int hashCode(final Object obj, final Iterable<String> propertyNamesIter) { final List<Object> propertyValues = Lists.newArrayList(); for (final Clause clause : clausesFor(propertyNamesIter)) { final Object propertyValue = clause.getValueOf(obj); if(propertyValue != null) { propertyValues.add(propertyValue); } } return Objects.hashCode(propertyValues.toArray()); } //endregion //region > equals /** * Returns whether two objects are equal, considering just the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! * @param propertyNames - the property name or names, CSV format. If multiple properties, use the {@link #equals(Object, Object, String...)} varargs} overloaded version of this method. */ @Deprecated public static boolean equals(Object p, Object q, String propertyNames) { if(p==null && q==null) { return true; } if(p==null || q==null) { return false; } if(p.getClass() != q.getClass()) { return false; } final Iterable<String> propertyNamesIter = csvToIterable(propertyNames); return equals(p, q, propertyNamesIter); } /** * Returns whether two objects are equal, considering just the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! */ @Deprecated public static boolean equals(Object p, Object q, String... propertyNames) { if(p==null && q==null) { return true; } if(p==null || q==null) { return false; } if(p.getClass() != q.getClass()) { return false; } final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames); return equals(p, q, propertyNamesIter); } private static boolean equals(final Object p, final Object q, final Iterable<String> propertyNamesIter) { final Iterable<Clause> clauses = clausesFor(propertyNamesIter); for (final Clause clause : clauses) { final Object pValue = clause.getValueOf(p); final Object qValue = clause.getValueOf(q); if(!Objects.equal(pValue, qValue)) { return false; } } return true; } //endregion //region > helpers private static Iterable<Clause> clausesFor(final Iterable<String> iterable) { return Iterables.transform(iterable, new Function<String, Clause>() { @Override public Clause apply(String input) { return Clause.parse(input); } }); } private static Iterable<String> csvToIterable(final String propertyNames) { return Splitter.on(',').split(propertyNames); } private static List<String> varargsToIterable(final String[] iterable) { return Arrays.asList(iterable); } //endregion //region > toStringOf public interface ToStringEvaluator { boolean canEvaluate(Object o); String evaluate(Object o); } private final List<ToStringEvaluator> evaluators = Lists.newArrayList(); public ObjectContracts with(ToStringEvaluator evaluator) { evaluators.add(evaluator); return this; } /** * Returns a string representation of two objects, considering just the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! * @param propertyNames - the property name or names, CSV format. If multiple properties, use the {@link #toString(Object, String...)} varargs} overloaded version of this method. */ @Deprecated public String toStringOf(Object p, String propertyNames) { final Iterable<String> propertyNamesIter = csvToIterable(propertyNames); return toStringOf(p, propertyNamesIter); } /** * Returns a string representation of two objects, considering just the specified property name(s). * @deprecated - please be aware that this utility heavily uses reflection. We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention! */ @Deprecated public String toStringOf(Object p, String... propertyNames) { final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames); return toStringOf(p, propertyNamesIter); } private String toStringOf(final Object p, final Iterable<String> propertyNamesIter) { final ToStringHelper stringHelper = Objects.toStringHelper(p); for (final Clause clause : clausesFor(propertyNamesIter)) { stringHelper.add(clause.getPropertyName(), asString(clause, p)); } return stringHelper.toString(); } private String asString(final Clause clause, Object p) { final Object value = clause.getValueOf(p); if(value == null) { return null; } for (ToStringEvaluator evaluator : evaluators) { if(evaluator.canEvaluate(value)) { return evaluator.evaluate(value); } } return value.toString(); } //endregion } class Clause { private static Pattern pattern = Pattern.compile("\\W*(\\w+)\\W*(asc|asc nullsFirst|asc nullsLast|desc|desc nullsFirst|desc nullsLast)?\\W*"); enum Direction { ASC { @Override public Comparator<Comparable<?>> getOrdering() { return Ordering.natural().nullsFirst(); } }, ASC_NULLS_LAST { @Override public Comparator<Comparable<?>> getOrdering() { return Ordering.natural().nullsLast(); } }, DESC { @Override public Comparator<Comparable<?>> getOrdering() { return Ordering.natural().nullsLast().reverse(); } }, DESC_NULLS_LAST { @Override public Comparator<Comparable<?>> getOrdering() { return Ordering.natural().nullsFirst().reverse(); } }; public abstract Comparator<Comparable<?>> getOrdering(); public static Direction valueOfElseAsc(String str) { if("asc".equals(str)) return ASC; if("asc nullsFirst".equals(str)) return ASC; if("asc nullsLast".equals(str)) return ASC_NULLS_LAST; if("desc".equals(str)) return DESC; if("desc nullsFirst".equals(str)) return DESC; if("desc nullsLast".equals(str)) return DESC_NULLS_LAST; return ASC; } } private String propertyName; private Direction direction; static Clause parse(String input) { final Matcher matcher = pattern.matcher(input); if(!matcher.matches()) { return null; } return new Clause(matcher.group(1), Direction.valueOfElseAsc(matcher.group(2))); } Clause(String propertyName, Direction direction) { this.propertyName = propertyName; this.direction = direction; } String getPropertyName() { return propertyName; } Direction getDirection() { return direction; } public Object getValueOf(Object obj) { if(obj == null) { return null; } final String methodNameSuffix = upperFirst(propertyName); final String getMethodName = "get" + methodNameSuffix; try { final Method getterMethod = obj.getClass().getMethod(getMethodName); return getterMethod.invoke(obj); } catch (NoSuchMethodException e) { final String isMethodName = "is" + methodNameSuffix; try { final Method getterMethod = obj.getClass().getMethod(isMethodName); return getterMethod.invoke(obj); } catch (NoSuchMethodException ex) { throw new IllegalArgumentException("No such method ' " + getMethodName + "' or '" + isMethodName + "'", e); } catch (Exception e1) { // some other reason; for example, a JDOUserException if the object has been deleted and interaction with its properties is not permitted. throw new RuntimeException(e1); } } catch (Exception e) { // some other reason; for example, a JDOUserException if the object has been deleted and interaction with its properties is not permitted. throw new RuntimeException(e); } } private static String upperFirst(final String str) { if (Strings.isNullOrEmpty(str)) { return str; } if (str.length() == 1) { return str.toUpperCase(); } return str.substring(0, 1).toUpperCase() + str.substring(1); } }