diff --git a/.gitignore b/.gitignore
index 744289d..23e7141 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,32 @@
# Project exclude paths
-/target/
\ No newline at end of file
+/target/
+
+#eclipse
+
+*target*
+*.jar
+*.war
+*.ear
+*.class
+
+# eclipse specific git ignore
+*.pydevproject
+.project
+.metadata
+bin/**
+tmp/**
+tmp/**/*
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3c3615f..06489ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,110 +1,123 @@
- 4.0.0
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ 4.0.0
- com.github.throwable.beanref
- beanref
- 0.2
- beanref
- A simple library that allows to access your POJOs' properties statically
- https://github.com/throwable/beanref
- jar
+ com.github.throwable.beanref
+ beanref
+ 0.2
+ beanref
+ A simple library that allows to access your POJOs' properties statically
+ https://github.com/throwable/beanref
+ jar
-
- scm:git:git://github.com/throwable/beanref.git
- scm:git:git://github.com/throwable/beanref.git
- https://github.com/throwable/beanref
- HEAD
-
+
+ scm:git:git://github.com/throwable/beanref.git
+ scm:git:git://github.com/throwable/beanref.git
+ https://github.com/throwable/beanref
+ HEAD
+
-
-
- owner
- Anton Kuranov
- ant.kuranov@gmail.com
- UTC+2
-
-
+
+
+ owner
+ Anton Kuranov
+ ant.kuranov@gmail.com
+ UTC+2
+
+
-
-
- MIT
- https://opensource.org/licenses/MIT
- repo
-
-
+
+
+ MIT
+ https://opensource.org/licenses/MIT
+ repo
+
+
-
- 1.8
- 1.8
- UTF-8
-
+
+ 11
+ 11
+ UTF-8
+ 2.9.3
+
-
-
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
-
-
- ossrh
- https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
-
-
+
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
-
-
- junit
- junit
- 4.13.2
- test
-
-
+
+
+ com.github.regbo
+ lfp-picoservice
+ f91ff54
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.1.0
-
-
- attach-sources
-
- jar
-
-
-
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.1.1
-
-
- attach-javadocs
-
- jar
-
-
-
-
-
- org.apache.maven.plugins
- maven-gpg-plugin
- 1.6
-
-
- sign-artifacts
- verify
-
- sign
-
-
-
-
-
-
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.1.0
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.1.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
diff --git a/src/main/java/com/github/throwable/beanref/BeanProperty.java b/src/main/java/com/github/throwable/beanref/BeanProperty.java
index 5fa1d90..9cbab05 100644
--- a/src/main/java/com/github/throwable/beanref/BeanProperty.java
+++ b/src/main/java/com/github/throwable/beanref/BeanProperty.java
@@ -1,109 +1,126 @@
package com.github.throwable.beanref;
+import java.lang.reflect.Method;
import java.util.Objects;
+import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
+import com.github.throwable.beanref.BeanPropertyResolver.SetterWriteAccessor;
+import com.github.throwable.beanref.DynamicBeanPropertyResolver.GetterReadAccessor;
+
/**
* BeanProperty represents a direct reference to a bean's property.
+ *
* @param bean class
* @param property's type
*/
-public class BeanProperty extends BeanPath
-{
- private final Class beanClass;
- private final Class type;
- private final String name;
- private final Function readAccessor;
- /* Nullable */
- private final BiConsumer writeAccessor;
- /* Resolved lazily */
- private final Supplier*Nullable*/Supplier> instantiatorSupplier;
-
-
- BeanProperty(Class beanClass, Class type, String name,
- Function readAccessor,
- /*Nullable*/ BiConsumer writeAccessor,
- Supplier> instantiatorSupplier)
- {
- super();
- this.beanClass = beanClass;
- this.type = type;
- this.name = name;
- this.readAccessor = readAccessor;
- this.writeAccessor = writeAccessor;
- this.instantiatorSupplier = instantiatorSupplier;
- }
-
- @Override
- public Class getBeanClass() {
- return beanClass;
- }
-
- @Override
- public Class getType() {
- return type;
- }
-
- /**
- * @return the name of bean's property
- */
- public String getName() {
- return name;
- }
-
- @Override
- public String getPath() {
- return name;
- }
-
- /**
- * @return read accessor to a property
- */
- public Function getReadAccessor() {
- return readAccessor;
- }
-
- /**
- * @return write accessor to a property (mutator) or null if property is read-only
- */
- public BiConsumer getWriteAccessor() {
- return writeAccessor;
- }
-
- @Override
- public boolean isReadOnly() {
- return writeAccessor == null;
- }
-
- @Override
- public TYPE get(BEAN bean) {
- return readAccessor.apply(bean);
- }
-
- @Override
- public void set(BEAN bean, TYPE value) {
- if (isReadOnly())
- throw new ReadOnlyPropertyException("Property '" + toString() + "' is read-only");
- writeAccessor.accept(bean, value);
- }
-
- Supplier getInstantiator() {
- return instantiatorSupplier.get();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- BeanProperty, ?> that = (BeanProperty, ?>) o;
- return beanClass.equals(that.beanClass) &&
- name.equals(that.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(beanClass, name);
- }
+public class BeanProperty extends BeanPath {
+ private final Class beanClass;
+ private final Class type;
+ private final String name;
+ private final Function readAccessor;
+ /* Nullable */
+ private final BiConsumer writeAccessor;
+ /* Resolved lazily */
+ private final Supplier* Nullable */Supplier> instantiatorSupplier;
+
+ BeanProperty(Class beanClass, Class type, String name, Function readAccessor,
+ /* Nullable */ BiConsumer writeAccessor, Supplier> instantiatorSupplier) {
+ super();
+ this.beanClass = beanClass;
+ this.type = type;
+ this.name = name;
+ this.readAccessor = readAccessor;
+ this.writeAccessor = writeAccessor;
+ this.instantiatorSupplier = instantiatorSupplier;
+ }
+
+ @Override
+ public Class getBeanClass() {
+ return beanClass;
+ }
+
+ @Override
+ public Class getType() {
+ return type;
+ }
+
+ /**
+ * @return the name of bean's property
+ */
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getPath() {
+ return name;
+ }
+
+ /**
+ * @return read accessor to a property
+ */
+ public Function getReadAccessor() {
+ return readAccessor;
+ }
+
+ public Optional tryGetReadMethod() {
+ return Optional.ofNullable(getReadAccessor())
+ .filter(GetterReadAccessor.class::isInstance)
+ .map(GetterReadAccessor.class::cast)
+ .map(GetterReadAccessor::getGetterMethod);
+ }
+
+ /**
+ * @return write accessor to a property (mutator) or null if property is
+ * read-only
+ */
+ public BiConsumer getWriteAccessor() {
+ return writeAccessor;
+ }
+
+ public Optional tryGetWriteMethod() {
+ return Optional.ofNullable(getWriteAccessor())
+ .filter(SetterWriteAccessor.class::isInstance)
+ .map(SetterWriteAccessor.class::cast)
+ .map(SetterWriteAccessor::getSetterMethod);
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return writeAccessor == null;
+ }
+
+ @Override
+ public TYPE get(BEAN bean) {
+ return readAccessor.apply(bean);
+ }
+
+ @Override
+ public void set(BEAN bean, TYPE value) {
+ if (isReadOnly())
+ throw new ReadOnlyPropertyException("Property '" + toString() + "' is read-only");
+ writeAccessor.accept(bean, value);
+ }
+
+ Supplier getInstantiator() {
+ return instantiatorSupplier.get();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ BeanProperty, ?> that = (BeanProperty, ?>) o;
+ return beanClass.equals(that.beanClass) && name.equals(that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(beanClass, name);
+ }
}
diff --git a/src/main/java/com/github/throwable/beanref/BeanPropertyResolver.java b/src/main/java/com/github/throwable/beanref/BeanPropertyResolver.java
index 4f68f7d..d121412 100644
--- a/src/main/java/com/github/throwable/beanref/BeanPropertyResolver.java
+++ b/src/main/java/com/github/throwable/beanref/BeanPropertyResolver.java
@@ -1,336 +1,335 @@
package com.github.throwable.beanref;
+import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
-import java.lang.reflect.*;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
-final class BeanPropertyResolver {
- @SuppressWarnings("rawtypes")
- private static final ConcurrentHashMap resolvedPropertiesCache =
- new ConcurrentHashMap<>();
- @SuppressWarnings("rawtypes")
- private static final ConcurrentHashMap resolvedCollectionPropertiesCache =
- new ConcurrentHashMap<>();
-
-
- private BeanPropertyResolver() {}
-
-
- @SuppressWarnings({"unchecked", "rawtypes"})
- static BeanProperty resolveBeanProperty(MethodReferenceLambda methodReferenceLambda) {
- BeanProperty beanProperty = resolvedPropertiesCache.get(methodReferenceLambda);
- if (beanProperty == null) {
- beanProperty = resolveBeanPropertyImpl(methodReferenceLambda);
- beanProperty = Optional.ofNullable(
- resolvedPropertiesCache.putIfAbsent(methodReferenceLambda, beanProperty)
- ).orElse(beanProperty);
- }
- return beanProperty;
- }
-
- @SuppressWarnings({"unchecked", "rawtypes"})
- static BeanProperty resolveCollectionBeanProperty(
- MethodReferenceLambda> methodReferenceLambda,
- /*Nullable*/ Supplier> collectionInstantiator)
- {
- BeanProperty beanProperty = resolvedCollectionPropertiesCache.get(methodReferenceLambda);
- if (beanProperty == null) {
- beanProperty = resolveCollectionBeanPropertyImpl(methodReferenceLambda, collectionInstantiator);
- beanProperty = Optional.ofNullable(
- resolvedCollectionPropertiesCache.putIfAbsent(methodReferenceLambda, beanProperty)
- ).orElse(beanProperty);
- }
- return beanProperty;
- }
-
- private static BeanProperty resolveBeanPropertyImpl(MethodReferenceLambda methodReferenceLambda)
- {
- final SerializedLambda serialized = serialized(methodReferenceLambda);
- if (serialized.getImplMethodName().startsWith("lambda$"))
- throw new IllegalArgumentException("Not a method reference");
- final Class beanClass = getContainingClass(serialized);
- final Method getterMethod = findGetterMethod(beanClass, serialized.getImplMethodName());
- @SuppressWarnings("unchecked")
- final Class type = (Class) getterMethod.getReturnType();
- final String propertyName = resolvePropertyName(getterMethod.getName());
- final Method setterMethod = findSetterMethod(beanClass, propertyName, type, getterMethod.getName());
- final BiConsumer writeAccessor = setterMethod != null ?
- new SetterWriteAccessor<>(setterMethod) : null;
- return new BeanProperty<>(beanClass, type, propertyName,
- methodReferenceLambda, writeAccessor, new InstantiatorResolver<>(type));
- }
-
-
- private static BeanProperty resolveCollectionBeanPropertyImpl(
- MethodReferenceLambda> methodReferenceLambda,
- /*Nullable*/ Supplier> collectionInstantiator)
- {
- final SerializedLambda serialized = serialized(methodReferenceLambda);
- if (serialized.getImplMethodName().startsWith("lambda$"))
- throw new IllegalArgumentException("Not a method reference");
- final Class beanClass = getContainingClass(serialized);
- final Method getterMethod = findGetterMethod(beanClass, serialized.getImplMethodName());
- @SuppressWarnings("unchecked")
- final Class> type = (Class>) getterMethod.getReturnType();
- final ParameterizedType genericReturnType = (ParameterizedType) getterMethod.getGenericReturnType();
- if (genericReturnType.getActualTypeArguments().length != 1)
- throw new IllegalArgumentException("Can not determine parameter type for "
- + beanClass.getName() + "." + getterMethod.getName());
- @SuppressWarnings("unchecked")
- final Class elementType = (Class) genericReturnType.getActualTypeArguments()[0];
-
- final String propertyName = resolvePropertyName(getterMethod.getName());
- final Method setterMethod = findSetterMethod(beanClass, propertyName, type, getterMethod.getName());
- final BiConsumer> writeAccessor = setterMethod != null ?
- new SetterWriteAccessor<>(setterMethod) : null;
- final CollectionElementReadAccessor beantypeCollectionElementReadAccessor =
- new CollectionElementReadAccessor<>(methodReferenceLambda);
- final Supplier>> collectionInstantiatorResolver;
- if (collectionInstantiator == null && writeAccessor != null)
- collectionInstantiatorResolver = defaultCollectionInstantiatorResolver(type);
- else
- collectionInstantiatorResolver = () -> collectionInstantiator;
- final CollectionElementWriteAccessor beantypeCollectionElementWriteAccessor =
- new CollectionElementWriteAccessor<>(methodReferenceLambda, writeAccessor, collectionInstantiatorResolver);
- return new BeanProperty<>(beanClass, elementType, propertyName,
- beantypeCollectionElementReadAccessor, beantypeCollectionElementWriteAccessor,
- new InstantiatorResolver<>(elementType));
- }
-
-
- private static Method findGetterMethod(Class beanClass, String getterMethodName) {
- final Method getterMethod;
- try {
- getterMethod = beanClass.getMethod(getterMethodName);
- } catch (NoSuchMethodException e) {
- throw new IllegalBeanPathException(e);
- }
-
- if (getterMethod.getParameterCount() > 0)
- throw new IllegalBeanPathException("Illegal getter method: " + getterMethodName);
- if (Void.TYPE.equals(getterMethod.getReturnType()))
- throw new IllegalBeanPathException("Illegal getter method return type: " + getterMethodName +
- "(" + getterMethod.getReturnType() + ")");
- return getterMethod;
- }
-
-
- static String resolvePropertyName(String getterMethodName)
- {
- final String propertyName;
-
- if (getterMethodName.startsWith("get") && getterMethodName.length() > 3 &&
- Character.isUpperCase(getterMethodName.charAt(3)))
- {
- propertyName = Character.toLowerCase(getterMethodName.charAt(3)) + getterMethodName.substring(4);
- }
- else if (getterMethodName.startsWith("is") && getterMethodName.length() > 2 &&
- Character.isUpperCase(getterMethodName.charAt(2)))
- {
- propertyName = Character.toLowerCase(getterMethodName.charAt(2)) + getterMethodName.substring(3);
- }
- else {
- // non-canonical name: use method name as-is
- propertyName = getterMethodName;
- }
- return propertyName;
- }
-
-
- static /* Nullable */ Method findSetterMethod(Class beanClass, String propertyName, Class type, String getterMethodName) {
- if (!getterMethodName.equals(propertyName)) {
- // canonical getXXX(): search for setXXX()
- final String setterMethodName = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
- try {
- return beanClass.getMethod(setterMethodName, type);
- } catch (NoSuchMethodException e) {
- // Read-only
- return null;
- }
- } else {
- // non-canonical xxx(): search for xxx(value)
- try {
- return beanClass.getMethod(propertyName, type);
- } catch (NoSuchMethodException e) {
- // Read-only
- return null;
- }
- }
- }
-
-
- private static SerializedLambda serialized(Object lambda) {
- try {
- Method writeMethod = lambda.getClass().getDeclaredMethod("writeReplace");
- writeMethod.setAccessible(true);
- return (SerializedLambda) writeMethod.invoke(lambda);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
-
- @SuppressWarnings("unchecked")
- private static Class getContainingClass(SerializedLambda lambda) {
- try {
- String className = lambda.getImplClass().replaceAll("/", ".");
- //System.out.println(lambda.getInstantiatedMethodType());
- return (Class) Class.forName(className, true, Thread.currentThread().getContextClassLoader());
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
-
- public static class SetterWriteAccessor implements BiConsumer {
- private final Method setterMethod;
-
- public SetterWriteAccessor(Method setterMethod) {
- this.setterMethod = setterMethod;
- }
-
- @Override
- public void accept(BEAN bean, TYPE value) {
- try {
- setterMethod.invoke(bean, value);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- } catch (InvocationTargetException e) {
- if (e.getTargetException() instanceof RuntimeException)
- // unchecked: throw as is
- throw (RuntimeException) e.getTargetException();
- else
- // checked: wrap into runtime
- throw new RuntimeException(e.getTargetException());
- }
- }
- }
-
-
- public static class InstantiatorResolver implements Supplier> {
- private final Class type;
- @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
- private volatile Optional> resolvedInstantiator;
-
- public InstantiatorResolver(Class type) {
- this.type = type;
- }
-
- @Override
- public Supplier get() {
- //noinspection OptionalAssignedToNull
- if (resolvedInstantiator == null) {
- resolvedInstantiator = Optional.ofNullable(resolveInstantiator(type));
- }
- return resolvedInstantiator.orElse(null);
- }
-
- private static Supplier resolveInstantiator(Class type) {
- if ((type.getModifiers() & Modifier.ABSTRACT) != 0)
- return null;
- final Constructor constructor;
- try {
- constructor = type.getConstructor();
- } catch (NoSuchMethodException e) {
- return null;
- }
- return () -> {
- try {
- return constructor.newInstance();
- } catch (InstantiationException | IllegalAccessException e) {
- throw new RuntimeException(e);
- } catch (InvocationTargetException e) {
- if (e.getTargetException() instanceof RuntimeException)
- throw (RuntimeException) e.getTargetException();
- else
- throw new RuntimeException(e.getTargetException());
- }
- };
- }
- }
-
-
- private static Supplier>> defaultCollectionInstantiatorResolver(Class> collectionType) {
- if ((collectionType.getModifiers() & Modifier.ABSTRACT) != 0) {
- if (collectionType.isAssignableFrom(List.class))
- return () -> ArrayList::new;
- else if (collectionType.isAssignableFrom(Set.class))
- return () -> LinkedHashSet::new;
- else return null;
- } else {
- // instantiatable
- return new InstantiatorResolver<>(collectionType);
- }
- }
-
-
- public static class CollectionElementReadAccessor implements Function {
- private final Function> beanPropertyReadAccessor;
-
- public CollectionElementReadAccessor(Function> beanPropertyReadAccessor) {
- this.beanPropertyReadAccessor = beanPropertyReadAccessor;
- }
-
- @Override
- public TYPE apply(BEAN bean) {
- final Collection collection = beanPropertyReadAccessor.apply(bean);
- if (collection == null || collection.isEmpty())
- return null;
- if (collection instanceof List) {
- final List list = (List) collection;
- return list.get(list.size()-1);
- } else {
- // Always obtain last element
- final Iterator it = collection.iterator();
- TYPE elem = it.next();
- while (it.hasNext())
- elem = it.next();
- return elem;
- }
- }
- }
-
-
- public static class CollectionElementWriteAccessor implements BiConsumer {
- private final Function> beanPropertyReadAccessor;
- /* Nullable */
- private final BiConsumer> beanPropertyWriteAccessor;
- /* Nullable */
- private final Supplier>> collectionInstantiatorResolver;
-
- public CollectionElementWriteAccessor(Function> beanPropertyReadAccessor,
- /*Nullable*/BiConsumer> beanPropertyWriteAccessor,
- Supplier>> collectionInstantiatorResolver)
- {
- this.beanPropertyReadAccessor = beanPropertyReadAccessor;
- this.beanPropertyWriteAccessor = beanPropertyWriteAccessor;
- this.collectionInstantiatorResolver = collectionInstantiatorResolver;
- }
-
- @Override
- public void accept(BEAN bean, TYPE value) {
- Collection collection = beanPropertyReadAccessor.apply(bean);
- if (collection == null) {
- if (beanPropertyWriteAccessor == null)
- throw new IncompletePathException(new ReadOnlyPropertyException("Collection property is read-only"));
- final Supplier> collectionInstantiator = collectionInstantiatorResolver.get();
- if (collectionInstantiator == null)
- throw new IncompletePathException("Cannot instantiate new collection due to unknown type");
- collection = collectionInstantiator.get();
- try {
- beanPropertyWriteAccessor.accept(bean, collection);
- } catch (Exception e) {
- throw new IncompletePathException(e);
- }
- }
- if (value == null)
- collection.clear();
- else
- collection.add(value);
- }
- }
+import com.github.throwable.beanref.lfp.BeanRefCache;
+import com.github.throwable.beanref.lfp.BeanRefUtils;
+
+public class BeanPropertyResolver {
+
+ protected BeanPropertyResolver() {}
+
+ protected static BeanProperty resolveBeanProperty(
+ MethodReferenceLambda methodReferenceLambda) {
+ Objects.requireNonNull(methodReferenceLambda);
+ String hash = BeanRefUtils.hash(methodReferenceLambda);
+ Object key = Arrays.asList(hash, "resolveBeanProperty");
+ return BeanRefCache.instance().get(key, nil -> {
+ return resolveBeanPropertyImpl(methodReferenceLambda);
+ });
+ }
+
+ protected static BeanProperty resolveCollectionBeanProperty(
+ MethodReferenceLambda> methodReferenceLambda,
+ /* Nullable */ Supplier> collectionInstantiator) {
+ Objects.requireNonNull(methodReferenceLambda);
+ String hash = BeanRefUtils.hash(methodReferenceLambda,
+ Optional.ofNullable(collectionInstantiator)
+ .filter(Serializable.class::isInstance)
+ .map(Serializable.class::cast)
+ .orElse(null));
+ Object key = Arrays.asList(hash, "resolveCollectionBeanProperty");
+ return BeanRefCache.instance().get(key, nil -> {
+ return resolveCollectionBeanPropertyImpl(methodReferenceLambda, collectionInstantiator);
+ });
+ }
+
+ protected static BeanProperty resolveBeanPropertyImpl(
+ MethodReferenceLambda methodReferenceLambda) {
+ SerializedLambda serialized = serialized(methodReferenceLambda);
+ final Class beanClass = getContainingClass(serialized);
+ final Method getterMethod = findGetterMethod(beanClass, serialized.getImplMethodName());
+ @SuppressWarnings("unchecked")
+ final Class type = (Class) getterMethod.getReturnType();
+ final String propertyName = resolvePropertyName(getterMethod.getName());
+ final Method setterMethod = findSetterMethod(beanClass, propertyName, type, getterMethod.getName());
+ final BiConsumer writeAccessor = setterMethod != null ? new SetterWriteAccessor<>(setterMethod)
+ : null;
+ return new BeanProperty<>(beanClass, type, propertyName, methodReferenceLambda, writeAccessor,
+ new InstantiatorResolver<>(type));
+ }
+
+ protected static BeanProperty resolveCollectionBeanPropertyImpl(
+ MethodReferenceLambda> methodReferenceLambda,
+ /* Nullable */ Supplier> collectionInstantiator) {
+ SerializedLambda serialized = serialized(methodReferenceLambda);
+ final Class beanClass = getContainingClass(serialized);
+ final Method getterMethod = findGetterMethod(beanClass, serialized.getImplMethodName());
+ @SuppressWarnings("unchecked")
+ final Class> type = (Class>) getterMethod.getReturnType();
+ final ParameterizedType genericReturnType = (ParameterizedType) getterMethod.getGenericReturnType();
+ if (genericReturnType.getActualTypeArguments().length != 1)
+ throw new IllegalArgumentException(
+ "Can not determine parameter type for " + beanClass.getName() + "." + getterMethod.getName());
+ @SuppressWarnings("unchecked")
+ final Class elementType = (Class) genericReturnType.getActualTypeArguments()[0];
+
+ final String propertyName = resolvePropertyName(getterMethod.getName());
+ final Method setterMethod = findSetterMethod(beanClass, propertyName, type, getterMethod.getName());
+ final BiConsumer> writeAccessor = setterMethod != null
+ ? new SetterWriteAccessor<>(setterMethod)
+ : null;
+ final CollectionElementReadAccessor beantypeCollectionElementReadAccessor = new CollectionElementReadAccessor<>(
+ methodReferenceLambda);
+ final Supplier>> collectionInstantiatorResolver;
+ if (collectionInstantiator == null && writeAccessor != null)
+ collectionInstantiatorResolver = defaultCollectionInstantiatorResolver(type);
+ else
+ collectionInstantiatorResolver = () -> collectionInstantiator;
+ final CollectionElementWriteAccessor beantypeCollectionElementWriteAccessor = new CollectionElementWriteAccessor<>(
+ methodReferenceLambda, writeAccessor, collectionInstantiatorResolver);
+ return new BeanProperty<>(beanClass, elementType, propertyName, beantypeCollectionElementReadAccessor,
+ beantypeCollectionElementWriteAccessor, new InstantiatorResolver<>(elementType));
+ }
+
+ protected static Method findGetterMethod(Class beanClass, String getterMethodName) {
+ final Method getterMethod;
+ try {
+ getterMethod = beanClass.getMethod(getterMethodName);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalBeanPathException(e);
+ }
+
+ if (getterMethod.getParameterCount() > 0)
+ throw new IllegalBeanPathException("Illegal getter method: " + getterMethodName);
+ if (Void.TYPE.equals(getterMethod.getReturnType()))
+ throw new IllegalBeanPathException("Illegal getter method return type: " + getterMethodName + "("
+ + getterMethod.getReturnType() + ")");
+ return getterMethod;
+ }
+
+ protected static String resolvePropertyName(String getterMethodName) {
+ final String propertyName;
+
+ if (getterMethodName.startsWith("get") && getterMethodName.length() > 3
+ && Character.isUpperCase(getterMethodName.charAt(3))) {
+ propertyName = Character.toLowerCase(getterMethodName.charAt(3)) + getterMethodName.substring(4);
+ } else if (getterMethodName.startsWith("is") && getterMethodName.length() > 2
+ && Character.isUpperCase(getterMethodName.charAt(2))) {
+ propertyName = Character.toLowerCase(getterMethodName.charAt(2)) + getterMethodName.substring(3);
+ } else {
+ // non-canonical name: use method name as-is
+ propertyName = getterMethodName;
+ }
+ return propertyName;
+ }
+
+ protected static /* Nullable */ Method findSetterMethod(Class beanClass, String propertyName,
+ Class type, String getterMethodName) {
+ if (!getterMethodName.equals(propertyName)) {
+ // canonical getXXX(): search for setXXX()
+ final String setterMethodName = "set" + Character.toUpperCase(propertyName.charAt(0))
+ + propertyName.substring(1);
+ try {
+ return beanClass.getMethod(setterMethodName, type);
+ } catch (NoSuchMethodException e) {
+ // Read-only
+ return null;
+ }
+ } else {
+ // non-canonical xxx(): search for xxx(value)
+ try {
+ return beanClass.getMethod(propertyName, type);
+ } catch (NoSuchMethodException e) {
+ // Read-only
+ return null;
+ }
+ }
+ }
+
+ protected static SerializedLambda serialized(Object lambda) {
+ SerializedLambda serializedLambda;
+ try {
+ Method writeMethod = lambda.getClass().getDeclaredMethod("writeReplace");
+ writeMethod.setAccessible(true);
+ serializedLambda = (SerializedLambda) writeMethod.invoke(lambda);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ if (serializedLambda.getImplMethodName().startsWith("lambda$"))
+ throw new IllegalArgumentException("Not a method reference");
+ return serializedLambda;
+ }
+
+ protected static String getContainingClassName(SerializedLambda lambda) {
+ return lambda.getImplClass();
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static Class getContainingClass(SerializedLambda lambda) {
+ String className = getContainingClassName(lambda).replaceAll("/", ".");
+ return (Class) BeanRefUtils.classForName(className, false);
+ }
+
+ public static class SetterWriteAccessor implements BiConsumer {
+ private final Method setterMethod;
+
+ public SetterWriteAccessor(Method setterMethod) {
+ this.setterMethod = setterMethod;
+ }
+
+ @Override
+ public void accept(BEAN bean, TYPE value) {
+ try {
+ setterMethod.invoke(bean, value);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ if (e.getTargetException() instanceof RuntimeException)
+ // unchecked: throw as is
+ throw (RuntimeException) e.getTargetException();
+ else
+ // checked: wrap into runtime
+ throw new RuntimeException(e.getTargetException());
+ }
+ }
+
+ /**
+ * @return the setterMethod
+ */
+ public Method getSetterMethod() {
+ return setterMethod;
+ }
+ }
+
+ public static class InstantiatorResolver implements Supplier> {
+ private final Class type;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private volatile Optional> resolvedInstantiator;
+
+ public InstantiatorResolver(Class type) {
+ this.type = type;
+ }
+
+ @Override
+ public Supplier get() {
+ // noinspection OptionalAssignedToNull
+ if (resolvedInstantiator == null) {
+ resolvedInstantiator = Optional.ofNullable(resolveInstantiator(type));
+ }
+ return resolvedInstantiator.orElse(null);
+ }
+
+ private static Supplier resolveInstantiator(Class type) {
+ if ((type.getModifiers() & Modifier.ABSTRACT) != 0)
+ return null;
+ final Constructor constructor;
+ try {
+ constructor = type.getConstructor();
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ return () -> {
+ try {
+ return constructor.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ if (e.getTargetException() instanceof RuntimeException)
+ throw (RuntimeException) e.getTargetException();
+ else
+ throw new RuntimeException(e.getTargetException());
+ }
+ };
+ }
+ }
+
+ protected static Supplier>> defaultCollectionInstantiatorResolver(
+ Class> collectionType) {
+ if ((collectionType.getModifiers() & Modifier.ABSTRACT) != 0) {
+ if (collectionType.isAssignableFrom(List.class))
+ return () -> ArrayList::new;
+ else if (collectionType.isAssignableFrom(Set.class))
+ return () -> LinkedHashSet::new;
+ else
+ return null;
+ } else {
+ // instantiatable
+ return new InstantiatorResolver<>(collectionType);
+ }
+ }
+
+ public static class CollectionElementReadAccessor implements Function {
+ private final Function> beanPropertyReadAccessor;
+
+ public CollectionElementReadAccessor(Function> beanPropertyReadAccessor) {
+ this.beanPropertyReadAccessor = beanPropertyReadAccessor;
+ }
+
+ @Override
+ public TYPE apply(BEAN bean) {
+ final Collection collection = beanPropertyReadAccessor.apply(bean);
+ if (collection == null || collection.isEmpty())
+ return null;
+ if (collection instanceof List) {
+ final List list = (List) collection;
+ return list.get(list.size() - 1);
+ } else {
+ // Always obtain last element
+ final Iterator it = collection.iterator();
+ TYPE elem = it.next();
+ while (it.hasNext())
+ elem = it.next();
+ return elem;
+ }
+ }
+ }
+
+ public static class CollectionElementWriteAccessor implements BiConsumer {
+ private final Function> beanPropertyReadAccessor;
+ /* Nullable */
+ private final BiConsumer> beanPropertyWriteAccessor;
+ /* Nullable */
+ private final Supplier>> collectionInstantiatorResolver;
+
+ public CollectionElementWriteAccessor(Function> beanPropertyReadAccessor,
+ /* Nullable */BiConsumer> beanPropertyWriteAccessor,
+ Supplier>> collectionInstantiatorResolver) {
+ this.beanPropertyReadAccessor = beanPropertyReadAccessor;
+ this.beanPropertyWriteAccessor = beanPropertyWriteAccessor;
+ this.collectionInstantiatorResolver = collectionInstantiatorResolver;
+ }
+
+ @Override
+ public void accept(BEAN bean, TYPE value) {
+ Collection collection = beanPropertyReadAccessor.apply(bean);
+ if (collection == null) {
+ if (beanPropertyWriteAccessor == null)
+ throw new IncompletePathException(
+ new ReadOnlyPropertyException("Collection property is read-only"));
+ final Supplier> collectionInstantiator = collectionInstantiatorResolver.get();
+ if (collectionInstantiator == null)
+ throw new IncompletePathException("Cannot instantiate new collection due to unknown type");
+ collection = collectionInstantiator.get();
+ try {
+ beanPropertyWriteAccessor.accept(bean, collection);
+ } catch (Exception e) {
+ throw new IncompletePathException(e);
+ }
+ }
+ if (value == null)
+ collection.clear();
+ else
+ collection.add(value);
+ }
+ }
+
}
diff --git a/src/main/java/com/github/throwable/beanref/DynamicBeanPropertyResolver.java b/src/main/java/com/github/throwable/beanref/DynamicBeanPropertyResolver.java
index 5ef1e0b..bc4b3df 100644
--- a/src/main/java/com/github/throwable/beanref/DynamicBeanPropertyResolver.java
+++ b/src/main/java/com/github/throwable/beanref/DynamicBeanPropertyResolver.java
@@ -3,104 +3,113 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;
-public class DynamicBeanPropertyResolver
-{
- @SuppressWarnings("rawtypes")
- private static final ConcurrentHashMap> resolvedBeanPropertiesCache = new ConcurrentHashMap<>();
+import com.github.throwable.beanref.lfp.BeanRefCache;
+import com.github.throwable.beanref.lfp.BeanRefUtils;
- @SuppressWarnings("unchecked")
- static BeanProperty resolveBeanProperty(Class beanClass, String propertyName, Class type) {
- final BeanProperty beanProperty = resolveBeanProperty(beanClass, propertyName);
- if (!type.isAssignableFrom(beanProperty.getType()))
- throw new IllegalArgumentException("Wrong type specified for property '" + propertyName + "' does not exist in bean " + beanClass.getSimpleName());
- return (BeanProperty) beanProperty;
- }
+public class DynamicBeanPropertyResolver {
- static BeanProperty resolveBeanProperty(Class beanClass, String propertyName) {
- final BeanProperty beanProperty = resolveAllBeanProperties(beanClass).get(propertyName);
- if (beanProperty == null)
- throw new IllegalArgumentException("Property '" + propertyName + "' does not exist in bean " + beanClass.getSimpleName());
- return beanProperty;
- }
+ @SuppressWarnings("unchecked")
+ static BeanProperty resolveBeanProperty(Class beanClass, String propertyName,
+ Class type) {
+ final BeanProperty beanProperty = resolveBeanProperty(beanClass, propertyName);
+ if (!type.isAssignableFrom(beanProperty.getType()))
+ throw new IllegalArgumentException("Wrong type specified for property '" + propertyName
+ + "' does not exist in bean " + beanClass.getSimpleName());
+ return (BeanProperty) beanProperty;
+ }
+ static BeanProperty resolveBeanProperty(Class beanClass, String propertyName) {
+ final BeanProperty beanProperty = resolveAllBeanProperties(beanClass).get(propertyName);
+ if (beanProperty == null)
+ throw new IllegalArgumentException(
+ "Property '" + propertyName + "' does not exist in bean " + beanClass.getSimpleName());
+ return beanProperty;
+ }
- @SuppressWarnings({"unchecked", "rawtypes"})
- static Map> resolveAllBeanProperties(Class beanClass) {
- Map beanProperties = resolvedBeanPropertiesCache.get(beanClass);
- if (beanProperties == null) {
- beanProperties = resolveAllBeanPropertiesImpl(beanClass);
- beanProperties = Optional.ofNullable(
- resolvedBeanPropertiesCache.putIfAbsent(beanClass, beanProperties)
- ).orElse(beanProperties);
- }
- return beanProperties;
- }
+ static Map> resolveAllBeanProperties(Class beanClass) {
+ {
+ var namedBeanClass = BeanRefUtils.getNamedClassType(beanClass);
+ if (!Objects.equals(namedBeanClass, beanClass))
+ return resolveAllBeanProperties(namedBeanClass);
+ }
+ return BeanRefCache.instance().get(Arrays.asList(beanClass, "resolveAllBeanProperties"), nil -> {
+ Map> beanProperties = resolveAllBeanPropertiesImpl(beanClass);
+ return Collections.unmodifiableMap(beanProperties);
+ });
+ }
+ private static Map> resolveAllBeanPropertiesImpl(Class beanClass) {
+ final HashMap> map = new HashMap<>();
+ final Method[] methods = beanClass.getMethods();
- private static Map> resolveAllBeanPropertiesImpl(Class beanClass)
- {
- final HashMap> map = new HashMap<>();
- final Method[] methods = beanClass.getMethods();
+ for (Method getterMethod : methods) {
+ if ((getterMethod.getModifiers() & Modifier.STATIC) != 0x0 || getterMethod.getParameterCount() > 0
+ || Void.TYPE.equals(getterMethod.getReturnType()))
+ continue;
+ // skip Object.class methods
+ switch (getterMethod.getName()) {
+ case "hashCode":
+ case "getClass":
+ case "clone": // Cloneable objects may re-define it as public
+ case "toString":
+ case "notify":
+ case "notifyAll":
+ continue;
+ }
+ @SuppressWarnings("unchecked")
+ final Class