/* * Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com] * 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. */ package de.ks.reflection; import com.google.common.primitives.Primitives; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.Proxy; import javassist.util.proxy.ProxyFactory; import org.objenesis.ObjenesisStd; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; public class PropertyPath { private static final ObjenesisStd objenesis = new ObjenesisStd(); public static <T> String methodName(Class<T> clazz, Consumer<T> consumer) { PropertyPath path = new PropertyPath(clazz); consumer.accept(path.build()); return path.getLastMethodName(); } public static <T> String property(Class<T> clazz, Consumer<T> consumer) { PropertyPath path = new PropertyPath(clazz); consumer.accept(path.build()); if (path.getPropertyPath().isEmpty()) { String methodName = path.getLastMethodName(); if (methodName.startsWith("get")) { return toFirstLowerCase(methodName, 3); } else if (methodName.startsWith("is")) { return toFirstLowerCase(methodName, 2); } throw new IllegalArgumentException("Could not find property for " + path); } else { return path.getPropertyPath(); } } public static String toFirstLowerCase(String methodName, int start) { String substring = methodName.substring(start); if (substring.length() == 1) { return substring.toLowerCase(Locale.ROOT); } else { return substring.substring(0, 1).toLowerCase(Locale.ROOT) + substring.substring(1); } } public static <T> PropertyPath of(Class<T> clazz, Consumer<T> consumer) { PropertyPath path = new PropertyPath(clazz); consumer.accept(path.build()); return path; } public static <T> PropertyPath ofTypeSafe(Class<T> clazz, Function<T, ?> function) { PropertyPath path = new PropertyPath(clazz); function.apply(path.build()); return path; } public static <T> PropertyPath of(Class<T> clazz) { return new PropertyPath(clazz); } private static final Logger log = LoggerFactory.getLogger(PropertyPath.class); protected Class<?> root; protected List<Method> methodPath = new ArrayList<>(25); protected List<String> stringPath = new ArrayList<>(25); protected List<String> fieldPath = new ArrayList<>(25); protected boolean setter; protected boolean getter; protected Class<?>[] parameterTypes; protected Class<?> returnType; protected Field field; public PropertyPath(Class<?> root) { this.root = root; } @SuppressWarnings("unchecked") public <T> T build() { stringPath.clear(); fieldPath.clear(); setter = false; getter = false; parameterTypes = null; returnType = null; field = null; return (T) callBack(root); } @SuppressWarnings("unchecked") protected Object callBack(Class<?> clazz) { if (Modifier.isFinal(clazz.getModifiers())) { return null; } ProxyFactory factory = new ProxyFactory(); factory.setSuperclass(clazz); Class proxy = factory.createClass(); Object retval = objenesis.newInstance(proxy); ((Proxy) retval).setHandler(new MethodHandler() { @Override public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { String methodName = thisMethod.getName(); switch (methodName) { case "finalize": return null; case "toString": return null; default: break; } stringPath.add(methodName); methodPath.add(thisMethod); setter = isSetter(thisMethod); getter = isGetter(thisMethod); parameterTypes = thisMethod.getParameterTypes(); returnType = thisMethod.getReturnType(); field = discoverField(thisMethod); if (field != null) { fieldPath.add(field.getName()); } Class<?> returnType = thisMethod.getReturnType(); if (boolean.class.equals(Primitives.unwrap(returnType))) { return false; } else if (int.class.equals(Primitives.unwrap(returnType))) { return 42; } else if (long.class.equals(Primitives.unwrap(returnType))) { return 42; } else if (short.class.equals(Primitives.unwrap(returnType))) { return 42; } else if (char.class.equals(Primitives.unwrap(returnType))) { return 42; } else if (byte.class.equals(Primitives.unwrap(returnType))) { return 4; } else if (float.class.equals(Primitives.unwrap(returnType))) { return 42F; } else if (double.class.equals(Primitives.unwrap(returnType))) { return 42D; } if (returnType.equals(Void.TYPE)) { return null; } else if (isSetter()) { return null; } else if (returnType.getPackage().getName().startsWith("java")) {//sadly, sadly, sadly javassist move java classes to new packages which are then incompatible... return null; } else { return callBack(thisMethod.getReturnType()); } } }); return retval; } public static Logger getLog() { return log; } @SuppressWarnings("unchecked") public <T> Class<T> getRoot() { return (Class<T>) root; } public Class<?>[] getParameterTypes() { return parameterTypes; } public Class<?> getReturnType() { return returnType; } public boolean isSetter() { return setter; } public boolean isGetter() { return getter; } public boolean isFieldAvailable() { return stringPath.size() == fieldPath.size(); } public void setValue(Object source, Object value) { Object instance = source; Method lastMethod = methodPath.get(methodPath.size() - 1); for (Method method : methodPath) { if (method.equals(lastMethod)) { break; } try { instance = method.invoke(instance); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error("Could not follow path {}: ", this, e); return; } } try { String methodName = lastMethod.getName().toLowerCase(Locale.ROOT); boolean isBooleanGetter = methodName.startsWith("is"); boolean isSimpleGetter = methodName.startsWith("get"); if (lastMethod.getParameterTypes().length == 0 && isBooleanGetter || isSimpleGetter) { int index = isBooleanGetter ? 2 : 3; Class<?> declaringClass = lastMethod.getDeclaringClass(); Optional<Method> methodOptional = Arrays.asList(declaringClass.getDeclaredMethods()).stream()// .filter((m) -> m.getName().startsWith("set") && m.getName().toLowerCase(Locale.ROOT).endsWith(methodName.substring(index)))// .findFirst(); if (methodOptional.isPresent()) { Method method = methodOptional.get(); method.setAccessible(true); method.invoke(instance, value); } } else { lastMethod.setAccessible(true); lastMethod.invoke(instance, value); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error("Could not invoke setter on path {}: ", this, e); } } @SuppressWarnings("unchecked") public <U> U getValue(Object source) { if (!isGetter()) { log.error("Declared path [{}]is no getter", this); return null; } Object instance = source; for (Method method : methodPath) { if (instance == null) { return null; } try { method.setAccessible(true); instance = method.invoke(instance); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error("Could not follow path {}: ", this, e); return null; } } return (U) instance; } public void walk(Object source) { Object instance = source; for (Method method : methodPath) { if (instance == null) { return; } if (method.getParameterTypes().length > 0) { return; } try { instance = method.invoke(instance); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error("Could not follow path {}: ", this, e); return; } } } protected boolean isGetter(Method method) { if (method.getParameterTypes().length > 0) { return false; } String name = method.getName(); Class<?> returnType = method.getReturnType(); if (name.startsWith("is")) { if (Boolean.TYPE.equals(returnType) || (Boolean.class.equals(returnType))) { return true; } } if (name.startsWith("get") && !Void.TYPE.equals(returnType)) { return true; } return false; } protected boolean isSetter(Method method) { if (method.getParameterTypes().length != 1) { return false; } if (method.getName().startsWith("set")) { return true; } else { return false; } } protected Field discoverField(Method method) { String methodName = method.getName(); if (methodName.startsWith("get") || methodName.startsWith("set")) { methodName = methodName.substring(3); } else if (methodName.startsWith("is")) { methodName = methodName.substring(2); } methodName = methodName.toLowerCase(Locale.ROOT); List<Field> fieldsRecursive = ReflectionUtil.getAllFields(method.getDeclaringClass()); for (Field field : fieldsRecursive) { if (field.getName().toLowerCase(Locale.ROOT).equals(methodName)) { return field; } } return null; } public String getStringFieldPath() { StringBuilder builder = new StringBuilder(); builder.append(root.getSimpleName()).append("."); for (String fieldName : fieldPath) { builder.append(fieldName).append("."); } String path = builder.toString(); return path.substring(0, path.length() - 1); } public String getPropertyPath() { StringBuilder builder = new StringBuilder(); for (String fieldName : fieldPath) { builder.append(fieldName).append("."); } String path = builder.toString(); if (path.isEmpty()) { return path; } else { return path.substring(0, path.length() - 1); } } public Method getLastMethod() { return methodPath.get(methodPath.size() - 1); } public String getLastMethodName() { return methodPath.get(methodPath.size() - 1).getName(); } public String toLocalizationPath() { StringBuilder builder = new StringBuilder(); builder.append(root.getSimpleName()).append("."); for (String fieldName : fieldPath) { builder.append(fieldName).append("."); } String path = builder.toString(); return path.substring(0, path.length() - 1); } @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + ((methodPath == null) ? 0 : methodPath.hashCode()); result = (prime * result) + ((root == null) ? 0 : root.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof PropertyPath)) { return false; } PropertyPath other = (PropertyPath) obj; if (methodPath == null) { if (other.methodPath != null) { return false; } } else if (!methodPath.equals(other.methodPath)) { return false; } if (root == null) { if (other.root != null) { return false; } } else if (!root.equals(other.root)) { return false; } return true; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(root.getSimpleName()).append("."); for (String methodName : stringPath) { builder.append(methodName).append("()."); } String path = builder.toString(); return path.substring(0, path.length() - 3); } }