/*
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package com.sun.javafx.fxml.builder;
import com.sun.javafx.fxml.
BeanAdapter;
import java.lang.annotation.
Annotation;
import java.lang.reflect.
Array;
import java.lang.reflect.
Constructor;
import java.lang.reflect.
Method;
import java.lang.reflect.
Modifier;
import java.util.
AbstractMap;
import java.util.
ArrayList;
import java.util.
Collection;
import java.util.
Comparator;
import java.util.
HashMap;
import java.util.
HashSet;
import java.util.
LinkedHashMap;
import java.util.
LinkedList;
import java.util.
List;
import java.util.
Map;
import java.util.
Set;
import java.util.
TreeSet;
import javafx.beans.
NamedArg;
import javafx.util.
Builder;
import sun.reflect.misc.
ConstructorUtil;
import sun.reflect.misc.
MethodUtil;
import sun.reflect.misc.
ReflectUtil;
/**
* Using this builder assumes that some of the constructors of desired class
* with arguments are annotated with NamedArg annotation.
*/
public class
ProxyBuilder<T> extends
AbstractMap<
String,
Object> implements
Builder<T> {
private
Class<?>
type;
private final
Map<
Constructor,
Map<
String,
AnnotationValue>>
constructorsMap;
private final
Map<
String,
Property>
propertiesMap;
private final
Set<
Constructor>
constructors;
private
Set<
String>
propertyNames;
private boolean
hasDefaultConstructor = false;
private
Constructor defaultConstructor;
private static final
String SETTER_PREFIX = "set";
private static final
String GETTER_PREFIX = "get";
public
ProxyBuilder(
Class<?>
tp) {
this.
type =
tp;
constructorsMap = new
HashMap<>();
Constructor ctors[] =
ConstructorUtil.
getConstructors(
type);
for (
Constructor c :
ctors) {
Map<
String,
AnnotationValue>
args;
Class<?>
paramTypes[] =
c.
getParameterTypes();
Annotation[][]
paramAnnotations =
c.
getParameterAnnotations();
// probably default constructor
if (
paramTypes.length == 0) {
hasDefaultConstructor = true;
defaultConstructor =
c;
} else { // constructor with parameters
int
i = 0;
boolean
properlyAnnotated = true;
args = new
LinkedHashMap<>();
for (
Class<?>
clazz :
paramTypes) {
NamedArg argAnnotation = null;
for (
Annotation annotation :
paramAnnotations[
i]) {
if (
annotation instanceof
NamedArg) {
argAnnotation = (
NamedArg)
annotation;
break;
}
}
if (
argAnnotation != null) {
AnnotationValue av = new
AnnotationValue(
argAnnotation.
value(),
argAnnotation.
defaultValue(),
clazz);
args.
put(
argAnnotation.
value(),
av);
} else {
properlyAnnotated = false;
break;
}
i++;
}
if (
properlyAnnotated) {
constructorsMap.
put(
c,
args);
}
}
}
if (!
hasDefaultConstructor &&
constructorsMap.
isEmpty()) {
throw new
RuntimeException("Cannot create instance of "
+
type.
getCanonicalName()
+ " the constructor is not properly annotated.");
}
constructors = new
TreeSet<>(
constructorComparator);
constructors.
addAll(
constructorsMap.
keySet());
propertiesMap =
scanForSetters();
}
//make sure int goes before float
private final
Comparator<
Constructor>
constructorComparator
= (
Constructor o1,
Constructor o2) -> {
int
len1 =
o1.
getParameterCount();
int
len2 =
o2.
getParameterCount();
int
lim =
Math.
min(
len1,
len2);
for (int
i = 0;
i <
lim;
i++) {
Class c1 =
o1.
getParameterTypes()[
i];
Class c2 =
o2.
getParameterTypes()[
i];
if (
c1.
equals(
c2)) {
continue;
}
if (
c1.
equals(
Integer.
TYPE) &&
c2.
equals(
Double.
TYPE)) {
return -1;
}
if (
c1.
equals(
Double.
TYPE) &&
c2.
equals(
Integer.
TYPE)) {
return 1;
}
return
c1.
getCanonicalName().
compareTo(
c2.
getCanonicalName());
}
return
len1 -
len2;
};
private final
Map<
String,
Object>
userValues = new
HashMap<>();
@
Override
public
Object put(
String key,
Object value) {
userValues.
put(
key,
value);
return null; // to behave the same way as ObjectBuilder does
}
private final
Map<
String,
Object>
containers = new
HashMap<>();
/**
* This is used to support read-only collection property. This method must
* return a Collection of the appropriate type if 1. the property is
* read-only, and 2. the property is a collection. It must return null
* otherwise.
*
*/
private
Object getTemporaryContainer(
String propName) {
Object o =
containers.
get(
propName);
if (
o == null) {
o =
getReadOnlyProperty(
propName);
if (
o != null) {
containers.
put(
propName,
o);
}
}
return
o;
}
// Wrapper for ArrayList which we use to store read-only collection
// properties in
private static class
ArrayListWrapper<T> extends
ArrayList<T> {
}
// This is used to support read-only collection property.
private
Object getReadOnlyProperty(
String propName) {
// return ArrayListWrapper now and convert it to proper type later
// during the build - once we know which constructor we will use
// and what types it accepts
return new
ArrayListWrapper<>();
}
@
Override
public int
size() {
throw new
UnsupportedOperationException();
}
@
Override
public
Set<
Entry<
String,
Object>>
entrySet() {
throw new
UnsupportedOperationException();
}
@
Override
public boolean
isEmpty() {
throw new
UnsupportedOperationException();
}
@
Override
public boolean
containsKey(
Object key) {
return (
getTemporaryContainer(
key.
toString()) != null);
}
@
Override
public boolean
containsValue(
Object value) {
throw new
UnsupportedOperationException();
}
@
Override
public
Object get(
Object key) {
return
getTemporaryContainer(
key.
toString());
}
@
Override
public T
build() {
Object retObj = null;
// adding collection properties to userValues
for (
Entry<
String,
Object>
entry :
containers.
entrySet()) {
put(
entry.
getKey(),
entry.
getValue());
}
propertyNames =
userValues.
keySet();
for (
Constructor c :
constructors) {
Set<
String>
argumentNames =
getArgumentNames(
c);
// the object is created only if attributes from fxml exactly match constructor arguments
if (
propertyNames.
equals(
argumentNames)) {
retObj =
createObjectWithExactArguments(
c,
argumentNames);
if (
retObj != null) {
return (T)
retObj;
}
}
}
// constructor with exact match doesn't exist
Set<
String>
settersArgs =
propertiesMap.
keySet();
// check if all properties can be set by setters and class has default constructor
if (
settersArgs.
containsAll(
propertyNames) &&
hasDefaultConstructor) {
retObj =
createObjectFromDefaultConstructor();
if (
retObj != null) {
return (T)
retObj;
}
}
// set of mutable properties which are given by the user in fxml
Set<
String>
propertiesToSet = new
HashSet<>(
propertyNames);
propertiesToSet.
retainAll(
settersArgs);
// will search for combination of constructor and setters
Set<
Constructor>
chosenConstructors =
chooseBestConstructors(
settersArgs);
// we have chosen the best constructors, let's try to find one we can use
for (
Constructor constructor :
chosenConstructors) {
retObj =
createObjectFromConstructor(
constructor,
propertiesToSet);
if (
retObj != null) {
return (T)
retObj;
}
}
if (
retObj == null) {
throw new
RuntimeException("Cannot create instance of "
+
type.
getCanonicalName() + " with given set of properties: "
+
userValues.
keySet().
toString());
}
return (T)
retObj;
}
private
Set<
Constructor>
chooseBestConstructors(
Set<
String>
settersArgs) {
// set of immutable properties which are given by the user in fxml
Set<
String>
immutablesToSet = new
HashSet<>(
propertyNames);
immutablesToSet.
removeAll(
settersArgs);
// set of mutable properties which are given by the user in fxml
Set<
String>
propertiesToSet = new
HashSet<>(
propertyNames);
propertiesToSet.
retainAll(
settersArgs);
int
propertiesToSetCount =
Integer.
MAX_VALUE;
int
mutablesToSetCount =
Integer.
MAX_VALUE;
// there may be more constructor with the same argument names
// (this often happens in case of List<T> and T... etc.
Set<
Constructor>
chosenConstructors = new
TreeSet<>(
constructorComparator);
Set<
String>
argsNotSet = null;
for (
Constructor c :
constructors) {
Set<
String>
argumentNames =
getArgumentNames(
c);
// check whether this constructor takes all immutable properties
// given by the user; if not, skip it
if (!
argumentNames.
containsAll(
immutablesToSet)) {
continue;
}
// all properties of this constructor which the user didn't
// specify in FXML
// we try to minimize this set
Set<
String>
propertiesToSetInConstructor = new
HashSet<>(
argumentNames);
propertiesToSetInConstructor.
removeAll(
propertyNames);
// all mutable properties which the user did specify in FXML
// but are not settable with this constructor
// we try to minimize this too (but only if we have more constructors with
// the same propertiesToSetCount)
Set<
String>
mutablesNotSet = new
HashSet<>(
propertiesToSet);
mutablesNotSet.
removeAll(
argumentNames);
int
currentPropSize =
propertiesToSetInConstructor.
size();
if (
propertiesToSetCount ==
currentPropSize
&&
mutablesToSetCount ==
mutablesNotSet.
size()) {
// we found constructor which is as good as the ones we already have
chosenConstructors.
add(
c);
}
if (
propertiesToSetCount >
currentPropSize
|| (
propertiesToSetCount ==
currentPropSize &&
mutablesToSetCount >
mutablesNotSet.
size())) {
propertiesToSetCount =
currentPropSize;
mutablesToSetCount =
mutablesNotSet.
size();
chosenConstructors.
clear();
chosenConstructors.
add(
c);
}
}
if (
argsNotSet != null && !
argsNotSet.
isEmpty()) {
throw new
RuntimeException("Cannot create instance of "
+
type.
getCanonicalName()
+ " no constructor contains all properties specified in FXML.");
}
return
chosenConstructors;
}
// Returns argument names for given constructor
private
Set<
String>
getArgumentNames(
Constructor c) {
Map<
String,
AnnotationValue>
constructorArgsMap =
constructorsMap.
get(
c);
Set<
String>
argumentNames = null;
if (
constructorArgsMap != null) {
argumentNames =
constructorArgsMap.
keySet();
}
return
argumentNames;
}
private
Object createObjectFromDefaultConstructor() throws
RuntimeException {
Object retObj = null;
// create class with default constructor and iterate over all required setters
try {
retObj =
createInstance(
defaultConstructor, new
Object[]{});
} catch (
Exception ex) {
throw new
RuntimeException(
ex);
}
for (
String propName :
propertyNames) {
try {
Property property =
propertiesMap.
get(
propName);
property.
invoke(
retObj,
getUserValue(
propName,
property.
getType()));
} catch (
Exception ex) {
throw new
RuntimeException(
ex);
}
}
return
retObj;
}
private
Object createObjectFromConstructor(
Constructor constructor,
Set<
String>
propertiesToSet) {
Object retObj = null;
Map<
String,
AnnotationValue>
constructorArgsMap =
constructorsMap.
get(
constructor);
Object argsForConstruction[] = new
Object[
constructorArgsMap.
size()];
int
i = 0;
// set of properties which need to be set by setters if we use current
// constructor
Set<
String>
currentPropertiesToSet = new
HashSet<>(
propertiesToSet);
for (
AnnotationValue value :
constructorArgsMap.
values()) {
// first try to coerce user give value
Object userValue =
getUserValue(
value.
getName(),
value.
getType());
if (
userValue != null) {
try {
argsForConstruction[
i] =
BeanAdapter.
coerce(
userValue,
value.
getType());
} catch (
Exception ex) {
return null;
}
} else {
// trying to coerce default value
if (!
value.
getDefaultValue().
isEmpty()) {
try {
argsForConstruction[
i] =
BeanAdapter.
coerce(
value.
getDefaultValue(),
value.
getType());
} catch (
Exception ex) {
return null;
}
} else {
argsForConstruction[
i] =
getDefaultValue(
value.
getType());
}
}
currentPropertiesToSet.
remove(
value.
getName());
i++;
}
try {
retObj =
createInstance(
constructor,
argsForConstruction);
} catch (
Exception ex) {
// try next constructor
}
if (
retObj != null) {
for (
String propName :
currentPropertiesToSet) {
try {
Property property =
propertiesMap.
get(
propName);
property.
invoke(
retObj,
getUserValue(
propName,
property.
getType()));
} catch (
Exception ex) {
// try next constructor
return null;
}
}
}
return
retObj;
}
private
Object getUserValue(
String key,
Class<?>
type) {
Object val =
userValues.
get(
key);
if (
val == null) {
return null;
}
if (
type.
isAssignableFrom(
val.
getClass())) {
return
val;
}
// we currently don't have proper support support for arrays
// in FXML so we use lists instead
// the user provides us with a list and here we convert it to
// array to pass to the constructor
if (
type.
isArray()) {
try {
return
convertListToArray(
val,
type);
} catch (
RuntimeException ex) {
// conversion failed, maybe the ArrayListWrapper is
// used for storing single value
}
}
if (
ArrayListWrapper.class.
equals(
val.
getClass())) {
// user given value is an ArrayList but the constructor doesn't
// accept an ArrayList so the ArrayList comes from
// the getTemporaryContainer method
// we take the first argument
List l = (
List)
val;
return
l.
get(0);
}
return
val;
}
private
Object createObjectWithExactArguments(
Constructor c,
Set<
String>
argumentNames) {
Object retObj = null;
Object argsForConstruction[] = new
Object[
argumentNames.
size()];
Map<
String,
AnnotationValue>
constructorArgsMap =
constructorsMap.
get(
c);
int
i = 0;
for (
String arg :
argumentNames) {
Class<?>
tp =
constructorArgsMap.
get(
arg).
getType();
Object value =
getUserValue(
arg,
tp);
try {
argsForConstruction[
i++] =
BeanAdapter.
coerce(
value,
tp);
} catch (
Exception ex) {
return null;
}
}
try {
retObj =
createInstance(
c,
argsForConstruction);
} catch (
Exception ex) {
// will try to fall back to different constructor
}
return
retObj;
}
private
Object createInstance(
Constructor c,
Object args[]) throws
Exception {
Object retObj = null;
ReflectUtil.
checkPackageAccess(
type);
retObj =
c.
newInstance(
args);
return
retObj;
}
private
Map<
String,
Property>
scanForSetters() {
Map<
String,
Property>
strsMap = new
HashMap<>();
Map<
String,
LinkedList<
Method>>
methods =
getClassMethodCache(
type);
for (
String methodName :
methods.
keySet()) {
if (
methodName.
startsWith(
SETTER_PREFIX)) {
String propName =
methodName.
substring(
SETTER_PREFIX.
length());
propName =
Character.
toLowerCase(
propName.
charAt(0)) +
propName.
substring(1);
List<
Method>
methodsList =
methods.
get(
methodName);
for (
Method m :
methodsList) {
Class<?>
retType =
m.
getReturnType();
Class<?>
argType[] =
m.
getParameterTypes();
if (
retType.
equals(
Void.
TYPE) &&
argType.length == 1) {
strsMap.
put(
propName, new
Setter(
m,
argType[0]));
}
}
}
if (
methodName.
startsWith(
GETTER_PREFIX)) {
String propName =
methodName.
substring(
SETTER_PREFIX.
length());
propName =
Character.
toLowerCase(
propName.
charAt(0)) +
propName.
substring(1);
List<
Method>
methodsList =
methods.
get(
methodName);
for (
Method m :
methodsList) {
Class<?>
retType =
m.
getReturnType();
Class<?>
argType[] =
m.
getParameterTypes();
if (
Collection.class.
isAssignableFrom(
retType) &&
argType.length == 0) {
strsMap.
put(
propName, new
Getter(
m,
retType));
}
}
}
}
return
strsMap;
}
private static abstract class
Property {
protected final
Method method;
protected final
Class<?>
type;
public
Property(
Method m,
Class<?>
t) {
method =
m;
type =
t;
}
public
Class<?>
getType() {
return
type;
}
public abstract void
invoke(
Object obj,
Object argStr) throws
Exception;
}
private static class
Setter extends
Property {
public
Setter(
Method m,
Class<?>
t) {
super(
m,
t);
}
public void
invoke(
Object obj,
Object argStr) throws
Exception {
Object arg[] = new
Object[]{
BeanAdapter.
coerce(
argStr,
type)};
MethodUtil.
invoke(
method,
obj,
arg);
}
}
private static class
Getter extends
Property {
public
Getter(
Method m,
Class<?>
t) {
super(
m,
t);
}
@
Override
public void
invoke(
Object obj,
Object argStr) throws
Exception {
// we know that this.method returns collection otherwise it wouldn't be here
Collection to = (
Collection)
MethodUtil.
invoke(
method,
obj, new
Object[]{});
if (
argStr instanceof
Collection) {
Collection from = (
Collection)
argStr;
to.
addAll(
from);
} else {
to.
add(
argStr);
}
}
}
// This class holds information for one argument of the constructor
// which we got from the NamedArg annotation
private static class
AnnotationValue {
private final
String name;
private final
String defaultValue;
private final
Class<?>
type;
public
AnnotationValue(
String name,
String defaultValue,
Class<?>
type) {
this.
name =
name;
this.
defaultValue =
defaultValue;
this.
type =
type;
}
public
String getName() {
return
name;
}
public
String getDefaultValue() {
return
defaultValue;
}
public
Class<?>
getType() {
return
type;
}
}
private static
HashMap<
String,
LinkedList<
Method>>
getClassMethodCache(
Class<?>
type) {
HashMap<
String,
LinkedList<
Method>>
classMethodCache = new
HashMap<>();
ReflectUtil.
checkPackageAccess(
type);
Method[]
declaredMethods =
type.
getMethods();
for (
Method method :
declaredMethods) {
int
modifiers =
method.
getModifiers();
if (
Modifier.
isPublic(
modifiers) && !
Modifier.
isStatic(
modifiers)) {
String name =
method.
getName();
LinkedList<
Method>
namedMethods =
classMethodCache.
get(
name);
if (
namedMethods == null) {
namedMethods = new
LinkedList<>();
classMethodCache.
put(
name,
namedMethods);
}
namedMethods.
add(
method);
}
}
return
classMethodCache;
}
// Utility method for converting list to array via reflection
// it assumes that localType is array
private static
Object[]
convertListToArray(
Object userValue,
Class<?>
localType) {
Class<?>
arrayType =
localType.
getComponentType();
List l = (
List)
BeanAdapter.
coerce(
userValue,
List.class);
return
l.
toArray((
Object[])
Array.
newInstance(
arrayType, 0));
}
private static
Object getDefaultValue(
Class clazz) {
return
defaultsMap.
get(
clazz);
}
private static final
Map<
Class<?>,
Object>
defaultsMap;
static {
defaultsMap = new
HashMap<>();
defaultsMap.
put(byte.class, (byte) 0);
defaultsMap.
put(short.class, (short) 0);
defaultsMap.
put(int.class, 0);
defaultsMap.
put(long.class, 0L);
defaultsMap.
put(int.class, 0);
defaultsMap.
put(float.class, 0.0f);
defaultsMap.
put(double.class, 0.0d);
defaultsMap.
put(char.class, '\u0000');
defaultsMap.
put(boolean.class, false);
defaultsMap.
put(
Object.class, null);
}
}