/* * Copyright (c) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 David Berkman * * This file is part of the SmallMind Code Project. * * The SmallMind Code Project is free software, you can redistribute * it and/or modify it under either, at your discretion... * * 1) The terms of GNU Affero General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * ...or... * * 2) The terms of the Apache License, Version 2.0. * * The SmallMind Code Project is distributed in the hope that it will * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License or Apache License for more details. * * You should have received a copy of the GNU Affero General Public License * and the Apache License along with the SmallMind Code Project. If not, see * <http://www.gnu.org/licenses/> or <http://www.apache.org/licenses/LICENSE-2.0>. * * Additional permission under the GNU Affero GPL version 3 section 7 * ------------------------------------------------------------------ * If you modify this Program, or any covered work, by linking or * combining it with other code, such other code is not for that reason * alone subject to any of the requirements of the GNU Affero GPL * version 3. */ package org.smallmind.nutsnbolts.reflection.bean; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.concurrent.ConcurrentHashMap; import org.smallmind.nutsnbolts.reflection.type.converter.DefaultStringConverterFactory; import org.smallmind.nutsnbolts.reflection.type.converter.StringConversionException; import org.smallmind.nutsnbolts.reflection.type.converter.StringConverter; import org.smallmind.nutsnbolts.reflection.type.converter.StringConverterFactory; public class BeanUtility { private static final ConcurrentHashMap<MethodKey, Method> GETTER_MAP = new ConcurrentHashMap<MethodKey, Method>(); private static final ConcurrentHashMap<MethodKey, MethodTool> SETTER_MAP = new ConcurrentHashMap<MethodKey, MethodTool>(); public static Object convertFromString (StringConverterFactory stringConverterFactory, Class conversionClass, String value, boolean nullable) throws StringConversionException, BeanInvocationException { if (value == null) { if (!nullable) { throw new NullPointerException("Null value in a non-nullable conversion"); } return null; } return stringConverterFactory.getStringConverter(conversionClass).convert(value); } public static Object executeGet (Object target, String methodName, boolean nullable) throws BeanAccessException, BeanInvocationException { Method getterMethod; Object currentTarget; String[] methodComponents; // Split the method into dot-notated segments methodComponents = methodName.split("\\.", -1); currentTarget = target; try { // Every segment but the last is taken as a getter method for (int count = 0; count < methodComponents.length - 1; count++) { if ((currentTarget = (getterMethod = acquireGetterMethod(currentTarget, methodComponents[count])).invoke(currentTarget)) == null) { if (!nullable) { throw new BeanAccessException("The 'getter' method(%s) in chain(%s) returned a 'null' component", getterMethod.getName(), methodName); } return null; } } // As this executes a 'get' the last segment is taken as a getter return acquireGetterMethod(currentTarget, methodComponents[methodComponents.length - 1]).invoke(currentTarget); } catch (BeanAccessException beanAccessException) { throw beanAccessException; } catch (Exception exception) { throw new BeanInvocationException(exception); } } public static Class<?> executeSet (Object target, String methodName, String value) throws BeanAccessException, BeanInvocationException { return executeSet(DefaultStringConverterFactory.getInstance(), target, methodName, value); } public static Class<?> executeSet (StringConverterFactory stringConverterFactory, Object target, String methodName, String value) throws BeanAccessException, BeanInvocationException { MethodTool setterTool; Method getterMethod; Object currentTarget; String[] methodComponents; // Split the method into dot-notated segments methodComponents = methodName.split("\\.", -1); currentTarget = target; try { // Every segment but the last is taken as a getter method for (int count = 0; count < methodComponents.length - 1; count++) { if ((currentTarget = (getterMethod = acquireGetterMethod(currentTarget, methodComponents[count])).invoke(currentTarget)) == null) { throw new BeanAccessException("The 'getter' method(%s) in chain(%s) returned a 'null' component", getterMethod.getName(), methodName); } } // As this executes a 'set' the last segment is taken as a setter, and setters are stored with a String converter that returns the setter's proper parameter type setterTool = acquireSetterTool(stringConverterFactory, currentTarget, methodComponents[methodComponents.length - 1]); setterTool.getMethod().invoke(currentTarget, ((value == null) || (value.length() == 0)) ? null : setterTool.getConverter().convert(value)); return setterTool.getConverter().getType(); } catch (BeanAccessException beanAccessException) { throw beanAccessException; } catch (Exception exception) { throw new BeanInvocationException(exception); } } private static Method acquireGetterMethod (Object target, String name) throws BeanAccessException { Method getterMethod; MethodKey methodKey; methodKey = new MethodKey(target.getClass(), name); // Check if we've already got it if ((getterMethod = GETTER_MAP.get(methodKey)) == null) { try { // Is there a method with a proper getter name 'getXXX' getterMethod = target.getClass().getMethod(asGetterName(name)); } catch (NoSuchMethodException noGetterException) { try { // If not, is there a boolean version 'isXXX' getterMethod = target.getClass().getMethod(asIsName(name)); if (!(Boolean.class.equals(getterMethod.getReturnType()) || boolean.class.equals(getterMethod.getReturnType()))) { throw new BeanAccessException("Found an 'is' method(%s) on class(%s), but it doesn't return a 'boolean' type", getterMethod.getName(), target.getClass().getName()); } } catch (NoSuchMethodException noIsException) { throw new BeanAccessException("No 'getter' method(%s or %s) found on class(%s)", asGetterName(name), asIsName(name), target.getClass().getName()); } } GETTER_MAP.put(methodKey, getterMethod); } return getterMethod; } private static MethodTool acquireSetterTool (StringConverterFactory stringConverterFactory, Object target, String name) throws StringConversionException, BeanAccessException { MethodTool setterTool; MethodKey methodKey; String setterName = asSetterName(name); methodKey = new MethodKey(target.getClass(), name); // Check if we've already got it if ((setterTool = SETTER_MAP.get(methodKey)) == null) { // Look for a properly named method for (Method possibleMethod : target.getClass().getMethods()) { // Make sure the setter takes a single parameter, and get the String converter for it if (possibleMethod.getName().equals(setterName) && (possibleMethod.getParameterTypes().length == 1)) { SETTER_MAP.put(methodKey, setterTool = new MethodTool(possibleMethod, stringConverterFactory.getStringConverter(getParameterClass(possibleMethod)))); break; } } } if (setterTool == null) { throw new BeanAccessException("No 'setter' method(%s) found on class(%s)", setterName, target.getClass().getName()); } return setterTool; } private static Class getParameterClass (Method setterMethod) { Annotation[][] parameterAnnotations; if ((parameterAnnotations = setterMethod.getParameterAnnotations()).length > 0) { for (Annotation annotation : parameterAnnotations[0]) { if (annotation instanceof TypeHint) { return ((TypeHint)annotation).value(); } } } return setterMethod.getParameterTypes()[0]; } private static String asGetterName (String name) { StringBuilder getterBuilder = new StringBuilder(name); getterBuilder.setCharAt(0, Character.toUpperCase(getterBuilder.charAt(0))); getterBuilder.insert(0, "get"); return getterBuilder.toString(); } private static String asIsName (String name) { StringBuilder isBuilder = new StringBuilder(name); isBuilder.setCharAt(0, Character.toUpperCase(isBuilder.charAt(0))); isBuilder.insert(0, "is"); return isBuilder.toString(); } private static String asSetterName (String name) { StringBuilder setterBuilder = new StringBuilder(name); setterBuilder.setCharAt(0, Character.toUpperCase(setterBuilder.charAt(0))); setterBuilder.insert(0, "set"); return setterBuilder.toString(); } private static class MethodTool { private Method method; private StringConverter converter; private MethodTool (Method method, StringConverter converter) { this.method = method; this.converter = converter; } public Method getMethod () { return method; } public StringConverter getConverter () { return converter; } } private static class MethodKey { private Class methodClass; private String methodName; private MethodKey (Class methodClass, String methodName) { this.methodClass = methodClass; this.methodName = methodName; } public Class getMethodClass () { return methodClass; } public String getMethodName () { return methodName; } @Override public int hashCode () { return methodClass.hashCode() ^ methodName.hashCode(); } @Override public boolean equals (Object obj) { return (obj instanceof MethodKey) && methodClass.equals(((MethodKey)obj).getMethodClass()) && methodName.equals(((MethodKey)obj).getMethodName()); } } }