/*
 * Copyright (c) 2010, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.javafx.fxml.builder;

import com.sun.javafx.fxml.BeanAdapter;
import com.sun.javafx.fxml.ModuleHelper;
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 com.sun.javafx.reflect.ConstructorUtil;
import com.sun.javafx.reflect.ReflectUtil;

Using this builder assumes that some of the constructors of desired class with arguments are annotated with NamedArg annotation.
/** * 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.
/** * 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) && methodName.length() > SETTER_PREFIX.length()) { 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) && methodName.length() > GETTER_PREFIX.length()) { String propName = methodName.substring(GETTER_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)}; ModuleHelper.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) ModuleHelper.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 DEFAULTS_MAP.get(clazz); } private static final Map<Class<?>, Object> DEFAULTS_MAP = new HashMap<>(9); static { DEFAULTS_MAP.put(byte.class, (byte) 0); DEFAULTS_MAP.put(short.class, (short) 0); DEFAULTS_MAP.put(int.class, 0); DEFAULTS_MAP.put(long.class, 0L); DEFAULTS_MAP.put(float.class, 0.0f); DEFAULTS_MAP.put(double.class, 0.0d); DEFAULTS_MAP.put(char.class, '\u0000'); DEFAULTS_MAP.put(boolean.class, false); DEFAULTS_MAP.put(Object.class, null); } }