1 /*
   2  * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.sun.javafx.fxml.builder;
  26 
  27 import com.sun.javafx.fxml.BeanAdapter;
  28 import com.sun.javafx.fxml.ModuleHelper;
  29 import java.lang.annotation.Annotation;
  30 import java.lang.reflect.Array;
  31 import java.lang.reflect.Constructor;
  32 import java.lang.reflect.Method;
  33 import java.lang.reflect.Modifier;
  34 import java.util.AbstractMap;
  35 import java.util.ArrayList;
  36 import java.util.Collection;
  37 import java.util.Comparator;
  38 import java.util.HashMap;
  39 import java.util.HashSet;
  40 import java.util.LinkedHashMap;
  41 import java.util.LinkedList;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import java.util.TreeSet;
  46 import javafx.beans.NamedArg;
  47 import javafx.util.Builder;
  48 import com.sun.javafx.reflect.ConstructorUtil;
  49 import com.sun.javafx.reflect.ReflectUtil;
  50 
  51 /**
  52  * Using this builder assumes that some of the constructors of desired class
  53  * with arguments are annotated with NamedArg annotation.
  54  */
  55 public class ProxyBuilder<T> extends AbstractMap<String, Object> implements Builder<T> {
  56 
  57     private Class<?> type;
  58 
  59     private final Map<Constructor, Map<String, AnnotationValue>> constructorsMap;
  60     private final Map<String, Property> propertiesMap;
  61     private final Set<Constructor> constructors;
  62     private Set<String> propertyNames;
  63 
  64     private boolean hasDefaultConstructor = false;
  65     private Constructor defaultConstructor;
  66 
  67     private static final String SETTER_PREFIX = "set";
  68     private static final String GETTER_PREFIX = "get";
  69 
  70     public ProxyBuilder(Class<?> tp) {
  71         this.type = tp;
  72 
  73         constructorsMap = new HashMap<>();
  74         Constructor ctors[] = ConstructorUtil.getConstructors(type);
  75 
  76         for (Constructor c : ctors) {
  77             Map<String, AnnotationValue> args;
  78             Class<?> paramTypes[] = c.getParameterTypes();
  79             Annotation[][] paramAnnotations = c.getParameterAnnotations();
  80 
  81             // probably default constructor
  82             if (paramTypes.length == 0) {
  83                 hasDefaultConstructor = true;
  84                 defaultConstructor = c;
  85             } else { // constructor with parameters
  86                 int i = 0;
  87                 boolean properlyAnnotated = true;
  88                 args = new LinkedHashMap<>();
  89                 for (Class<?> clazz : paramTypes) {
  90                     NamedArg argAnnotation = null;
  91                     for (Annotation annotation : paramAnnotations[i]) {
  92                         if (annotation instanceof NamedArg) {
  93                             argAnnotation = (NamedArg) annotation;
  94                             break;
  95                         }
  96                     }
  97 
  98                     if (argAnnotation != null) {
  99                         AnnotationValue av = new AnnotationValue(
 100                                 argAnnotation.value(),
 101                                 argAnnotation.defaultValue(),
 102                                 clazz);
 103                         args.put(argAnnotation.value(), av);
 104                     } else {
 105                         properlyAnnotated = false;
 106                         break;
 107                     }
 108                     i++;
 109                 }
 110                 if (properlyAnnotated) {
 111                     constructorsMap.put(c, args);
 112                 }
 113             }
 114         }
 115 
 116         if (!hasDefaultConstructor && constructorsMap.isEmpty()) {
 117             throw new RuntimeException("Cannot create instance of "
 118                     + type.getCanonicalName()
 119                     + " the constructor is not properly annotated.");
 120         }
 121 
 122         constructors = new TreeSet<>(constructorComparator);
 123         constructors.addAll(constructorsMap.keySet());
 124         propertiesMap = scanForSetters();
 125     }
 126 
 127     //make sure int goes before float
 128     private final Comparator<Constructor> constructorComparator
 129             = (Constructor o1, Constructor o2) -> {
 130                 int len1 = o1.getParameterCount();
 131                 int len2 = o2.getParameterCount();
 132                 int lim = Math.min(len1, len2);
 133                 for (int i = 0; i < lim; i++) {
 134                     Class c1 = o1.getParameterTypes()[i];
 135                     Class c2 = o2.getParameterTypes()[i];
 136                     if (c1.equals(c2)) {
 137                         continue;
 138                     }
 139                     if (c1.equals(Integer.TYPE) && c2.equals(Double.TYPE)) {
 140                         return -1;
 141                     }
 142                     if (c1.equals(Double.TYPE) && c2.equals(Integer.TYPE)) {
 143                         return 1;
 144                     }
 145                     return c1.getCanonicalName().compareTo(c2.getCanonicalName());
 146                 }
 147                 return len1 - len2;
 148             };
 149     private final Map<String, Object> userValues = new HashMap<>();
 150 
 151     @Override
 152     public Object put(String key, Object value) {
 153         userValues.put(key, value);
 154         return null; // to behave the same way as ObjectBuilder does
 155     }
 156 
 157     private final Map<String, Object> containers = new HashMap<>();
 158 
 159     /**
 160      * This is used to support read-only collection property. This method must
 161      * return a Collection of the appropriate type if 1. the property is
 162      * read-only, and 2. the property is a collection. It must return null
 163      * otherwise.
 164      *
 165      */
 166     private Object getTemporaryContainer(String propName) {
 167         Object o = containers.get(propName);
 168         if (o == null) {
 169             o = getReadOnlyProperty(propName);
 170             if (o != null) {
 171                 containers.put(propName, o);
 172             }
 173         }
 174         return o;
 175     }
 176 
 177     // Wrapper for ArrayList which we use to store read-only collection
 178     // properties in
 179     private static class ArrayListWrapper<T> extends ArrayList<T> {
 180 
 181     }
 182 
 183     // This is used to support read-only collection property.
 184     private Object getReadOnlyProperty(String propName) {
 185         // return ArrayListWrapper now and convert it to proper type later
 186         // during the build - once we know which constructor we will use
 187         // and what types it accepts
 188         return new ArrayListWrapper<>();
 189     }
 190 
 191     @Override
 192     public int size() {
 193         throw new UnsupportedOperationException();
 194     }
 195 
 196     @Override
 197     public Set<Entry<String, Object>> entrySet() {
 198         throw new UnsupportedOperationException();
 199     }
 200 
 201     @Override
 202     public boolean isEmpty() {
 203         throw new UnsupportedOperationException();
 204     }
 205 
 206     @Override
 207     public boolean containsKey(Object key) {
 208         return (getTemporaryContainer(key.toString()) != null);
 209     }
 210 
 211     @Override
 212     public boolean containsValue(Object value) {
 213         throw new UnsupportedOperationException();
 214     }
 215 
 216     @Override
 217     public Object get(Object key) {
 218         return getTemporaryContainer(key.toString());
 219     }
 220 
 221     @Override
 222     public T build() {
 223         Object retObj = null;
 224         // adding collection properties to userValues
 225         for (Entry<String, Object> entry : containers.entrySet()) {
 226             put(entry.getKey(), entry.getValue());
 227         }
 228 
 229         propertyNames = userValues.keySet();
 230 
 231         for (Constructor c : constructors) {
 232             Set<String> argumentNames = getArgumentNames(c);
 233 
 234             // the object is created only if attributes from fxml exactly match constructor arguments
 235             if (propertyNames.equals(argumentNames)) {
 236                 retObj = createObjectWithExactArguments(c, argumentNames);
 237                 if (retObj != null) {
 238                     return (T) retObj;
 239                 }
 240             }
 241         }
 242 
 243         // constructor with exact match doesn't exist
 244         Set<String> settersArgs = propertiesMap.keySet();
 245 
 246         // check if all properties can be set by setters and class has default constructor
 247         if (settersArgs.containsAll(propertyNames) && hasDefaultConstructor) {
 248             retObj = createObjectFromDefaultConstructor();
 249             if (retObj != null) {
 250                 return (T) retObj;
 251             }
 252         }
 253 
 254         // set of mutable properties which are given by the user in fxml
 255         Set<String> propertiesToSet = new HashSet<>(propertyNames);
 256         propertiesToSet.retainAll(settersArgs);
 257 
 258         // will search for combination of constructor and setters
 259         Set<Constructor> chosenConstructors = chooseBestConstructors(settersArgs);
 260 
 261         // we have chosen the best constructors, let's try to find one we can use
 262         for (Constructor constructor : chosenConstructors) {
 263             retObj = createObjectFromConstructor(constructor, propertiesToSet);
 264             if (retObj != null) {
 265                 return (T) retObj;
 266             }
 267         }
 268 
 269         if (retObj == null) {
 270             throw new RuntimeException("Cannot create instance of "
 271                     + type.getCanonicalName() + " with given set of properties: "
 272                     + userValues.keySet().toString());
 273         }
 274 
 275         return (T) retObj;
 276     }
 277 
 278     private Set<Constructor> chooseBestConstructors(Set<String> settersArgs) {
 279         // set of immutable properties which are given by the user in fxml
 280         Set<String> immutablesToSet = new HashSet<>(propertyNames);
 281         immutablesToSet.removeAll(settersArgs);
 282 
 283         // set of mutable properties which are given by the user in fxml
 284         Set<String> propertiesToSet = new HashSet<>(propertyNames);
 285         propertiesToSet.retainAll(settersArgs);
 286 
 287         int propertiesToSetCount = Integer.MAX_VALUE;
 288         int mutablesToSetCount = Integer.MAX_VALUE;
 289 
 290         // there may be more constructor with the same argument names
 291         // (this often happens in case of List<T> and T... etc.
 292         Set<Constructor> chosenConstructors = new TreeSet<>(constructorComparator);
 293         Set<String> argsNotSet = null;
 294         for (Constructor c : constructors) {
 295             Set<String> argumentNames = getArgumentNames(c);
 296 
 297             // check whether this constructor takes all immutable properties
 298             // given by the user; if not, skip it
 299             if (!argumentNames.containsAll(immutablesToSet)) {
 300                 continue;
 301             }
 302 
 303             // all properties of this constructor which the user didn't
 304             // specify in FXML
 305             // we try to minimize this set
 306             Set<String> propertiesToSetInConstructor = new HashSet<>(argumentNames);
 307             propertiesToSetInConstructor.removeAll(propertyNames);
 308 
 309             // all mutable properties which the user did specify in FXML
 310             // but are not settable with this constructor
 311             // we try to minimize this too (but only if we have more constructors with
 312             // the same propertiesToSetCount)
 313             Set<String> mutablesNotSet = new HashSet<>(propertiesToSet);
 314             mutablesNotSet.removeAll(argumentNames);
 315 
 316             int currentPropSize = propertiesToSetInConstructor.size();
 317             if (propertiesToSetCount == currentPropSize
 318                     && mutablesToSetCount == mutablesNotSet.size()) {
 319                 // we found constructor which is as good as the ones we already have
 320                 chosenConstructors.add(c);
 321             }
 322 
 323             if (propertiesToSetCount > currentPropSize
 324                     || (propertiesToSetCount == currentPropSize && mutablesToSetCount > mutablesNotSet.size())) {
 325                 propertiesToSetCount = currentPropSize;
 326                 mutablesToSetCount = mutablesNotSet.size();
 327                 chosenConstructors.clear();
 328                 chosenConstructors.add(c);
 329             }
 330         }
 331 
 332         if (argsNotSet != null && !argsNotSet.isEmpty()) {
 333             throw new RuntimeException("Cannot create instance of "
 334                     + type.getCanonicalName()
 335                     + " no constructor contains all properties specified in FXML.");
 336         }
 337 
 338         return chosenConstructors;
 339     }
 340 
 341     // Returns argument names for given constructor
 342     private Set<String> getArgumentNames(Constructor c) {
 343         Map<String, AnnotationValue> constructorArgsMap = constructorsMap.get(c);
 344         Set<String> argumentNames = null;
 345         if (constructorArgsMap != null) {
 346             argumentNames = constructorArgsMap.keySet();
 347         }
 348         return argumentNames;
 349     }
 350 
 351     private Object createObjectFromDefaultConstructor() throws RuntimeException {
 352         Object retObj = null;
 353 
 354         // create class with default constructor and iterate over all required setters
 355         try {
 356             retObj = createInstance(defaultConstructor, new Object[]{});
 357         } catch (Exception ex) {
 358             throw new RuntimeException(ex);
 359         }
 360         for (String propName : propertyNames) {
 361             try {
 362                 Property property = propertiesMap.get(propName);
 363                 property.invoke(retObj, getUserValue(propName, property.getType()));
 364             } catch (Exception ex) {
 365                 throw new RuntimeException(ex);
 366             }
 367         }
 368 
 369         return retObj;
 370     }
 371 
 372     private Object createObjectFromConstructor(Constructor constructor, Set<String> propertiesToSet) {
 373         Object retObj = null;
 374         Map<String, AnnotationValue> constructorArgsMap = constructorsMap.get(constructor);
 375         Object argsForConstruction[] = new Object[constructorArgsMap.size()];
 376         int i = 0;
 377 
 378         // set of properties which need to be set by setters if we use current
 379         // constructor
 380         Set<String> currentPropertiesToSet = new HashSet<>(propertiesToSet);
 381         for (AnnotationValue value : constructorArgsMap.values()) {
 382             // first try to coerce user give value
 383             Object userValue = getUserValue(value.getName(), value.getType());
 384             if (userValue != null) {
 385                 try {
 386                     argsForConstruction[i] = BeanAdapter.coerce(userValue, value.getType());
 387                 } catch (Exception ex) {
 388                     return null;
 389                 }
 390             } else {
 391                 // trying to coerce default value
 392                 if (!value.getDefaultValue().isEmpty()) {
 393                     try {
 394                         argsForConstruction[i] = BeanAdapter.coerce(value.getDefaultValue(), value.getType());
 395                     } catch (Exception ex) {
 396                         return null;
 397                     }
 398                 } else {
 399                     argsForConstruction[i] = getDefaultValue(value.getType());
 400                 }
 401             }
 402             currentPropertiesToSet.remove(value.getName());
 403             i++;
 404         }
 405 
 406         try {
 407             retObj = createInstance(constructor, argsForConstruction);
 408         } catch (Exception ex) {
 409             // try next constructor
 410         }
 411 
 412         if (retObj != null) {
 413             for (String propName : currentPropertiesToSet) {
 414                 try {
 415                     Property property = propertiesMap.get(propName);
 416                     property.invoke(retObj, getUserValue(propName, property.getType()));
 417                 } catch (Exception ex) {
 418                     // try next constructor
 419                     return null;
 420                 }
 421             }
 422         }
 423 
 424         return retObj;
 425     }
 426 
 427     private Object getUserValue(String key, Class<?> type) {
 428         Object val = userValues.get(key);
 429         if (val == null) {
 430             return null;
 431         }
 432 
 433         if (type.isAssignableFrom(val.getClass())) {
 434             return val;
 435         }
 436 
 437         // we currently don't have proper support support for arrays
 438         // in FXML so we use lists instead
 439         // the user provides us with a list and here we convert it to
 440         // array to pass to the constructor
 441         if (type.isArray()) {
 442             try {
 443                 return convertListToArray(val, type);
 444             } catch (RuntimeException ex) {
 445                 // conversion failed, maybe the ArrayListWrapper is
 446                 // used for storing single value
 447             }
 448         }
 449 
 450         if (ArrayListWrapper.class.equals(val.getClass())) {
 451             // user given value is an ArrayList but the constructor doesn't
 452             // accept an ArrayList so the ArrayList comes from
 453             // the getTemporaryContainer method
 454             // we take the first argument
 455             List l = (List) val;
 456             return l.get(0);
 457         }
 458 
 459         return val;
 460     }
 461 
 462     private Object createObjectWithExactArguments(Constructor c, Set<String> argumentNames) {
 463         Object retObj = null;
 464         Object argsForConstruction[] = new Object[argumentNames.size()];
 465         Map<String, AnnotationValue> constructorArgsMap = constructorsMap.get(c);
 466 
 467         int i = 0;
 468 
 469         for (String arg : argumentNames) {
 470             Class<?> tp = constructorArgsMap.get(arg).getType();
 471             Object value = getUserValue(arg, tp);
 472             try {
 473                 argsForConstruction[i++] = BeanAdapter.coerce(value, tp);
 474             } catch (Exception ex) {
 475                 return null;
 476             }
 477         }
 478 
 479         try {
 480             retObj = createInstance(c, argsForConstruction);
 481         } catch (Exception ex) {
 482             // will try to fall back to different constructor
 483         }
 484 
 485         return retObj;
 486     }
 487 
 488     private Object createInstance(Constructor c, Object args[]) throws Exception {
 489         Object retObj = null;
 490 
 491         ReflectUtil.checkPackageAccess(type);
 492         retObj = c.newInstance(args);
 493 
 494         return retObj;
 495     }
 496 
 497     private Map<String, Property> scanForSetters() {
 498         Map<String, Property> strsMap = new HashMap<>();
 499         Map<String, LinkedList<Method>> methods = getClassMethodCache(type);
 500 
 501         for (String methodName : methods.keySet()) {
 502             if (methodName.startsWith(SETTER_PREFIX) && methodName.length() > SETTER_PREFIX.length()) {
 503                 String propName = methodName.substring(SETTER_PREFIX.length());
 504                 propName = Character.toLowerCase(propName.charAt(0)) + propName.substring(1);
 505                 List<Method> methodsList = methods.get(methodName);
 506                 for (Method m : methodsList) {
 507                     Class<?> retType = m.getReturnType();
 508                     Class<?> argType[] = m.getParameterTypes();
 509                     if (retType.equals(Void.TYPE) && argType.length == 1) {
 510                         strsMap.put(propName, new Setter(m, argType[0]));
 511                     }
 512                 }
 513             }
 514             if (methodName.startsWith(GETTER_PREFIX) && methodName.length() > GETTER_PREFIX.length()) {
 515                 String propName = methodName.substring(GETTER_PREFIX.length());
 516                 propName = Character.toLowerCase(propName.charAt(0)) + propName.substring(1);
 517                 List<Method> methodsList = methods.get(methodName);
 518                 for (Method m : methodsList) {
 519                     Class<?> retType = m.getReturnType();
 520                     Class<?> argType[] = m.getParameterTypes();
 521                     if (Collection.class.isAssignableFrom(retType) && argType.length == 0) {
 522                         strsMap.put(propName, new Getter(m, retType));
 523                     }
 524                 }
 525             }
 526         }
 527 
 528         return strsMap;
 529     }
 530 
 531     private static abstract class Property {
 532         protected final Method method;
 533         protected final Class<?> type;
 534 
 535         public Property(Method m, Class<?> t) {
 536             method = m;
 537             type = t;
 538         }
 539 
 540         public Class<?> getType() {
 541             return type;
 542         }
 543 
 544         public abstract void invoke(Object obj, Object argStr) throws Exception;
 545     }
 546 
 547     private static class Setter extends Property {
 548 
 549         public Setter(Method m, Class<?> t) {
 550             super(m, t);
 551         }
 552 
 553         public void invoke(Object obj, Object argStr) throws Exception {
 554             Object arg[] = new Object[]{BeanAdapter.coerce(argStr, type)};
 555             ModuleHelper.invoke(method, obj, arg);
 556         }
 557     }
 558 
 559     private static class Getter extends Property {
 560 
 561         public Getter(Method m, Class<?> t) {
 562             super(m, t);
 563         }
 564 
 565         @Override
 566         public void invoke(Object obj, Object argStr) throws Exception {
 567             // we know that this.method returns collection otherwise it wouldn't be here
 568             Collection to = (Collection) ModuleHelper.invoke(method, obj, new Object[]{});
 569             if (argStr instanceof Collection) {
 570                 Collection from = (Collection) argStr;
 571                 to.addAll(from);
 572             } else {
 573                 to.add(argStr);
 574             }
 575         }
 576     }
 577 
 578     // This class holds information for one argument of the constructor
 579     // which we got from the NamedArg annotation
 580     private static class AnnotationValue {
 581 
 582         private final String name;
 583         private final String defaultValue;
 584         private final Class<?> type;
 585 
 586         public AnnotationValue(String name, String defaultValue, Class<?> type) {
 587             this.name = name;
 588             this.defaultValue = defaultValue;
 589             this.type = type;
 590         }
 591 
 592         public String getName() {
 593             return name;
 594         }
 595 
 596         public String getDefaultValue() {
 597             return defaultValue;
 598         }
 599 
 600         public Class<?> getType() {
 601             return type;
 602         }
 603     }
 604 
 605     private static HashMap<String, LinkedList<Method>> getClassMethodCache(Class<?> type) {
 606         HashMap<String, LinkedList<Method>> classMethodCache = new HashMap<>();
 607 
 608         ReflectUtil.checkPackageAccess(type);
 609 
 610         Method[] declaredMethods = type.getMethods();
 611         for (Method method : declaredMethods) {
 612             int modifiers = method.getModifiers();
 613 
 614             if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) {
 615                 String name = method.getName();
 616                 LinkedList<Method> namedMethods = classMethodCache.get(name);
 617 
 618                 if (namedMethods == null) {
 619                     namedMethods = new LinkedList<>();
 620                     classMethodCache.put(name, namedMethods);
 621                 }
 622 
 623                 namedMethods.add(method);
 624             }
 625         }
 626 
 627         return classMethodCache;
 628     }
 629 
 630     // Utility method for converting list to array via reflection
 631     // it assumes that localType is array
 632     private static Object[] convertListToArray(Object userValue, Class<?> localType) {
 633         Class<?> arrayType = localType.getComponentType();
 634         List l = (List) BeanAdapter.coerce(userValue, List.class);
 635 
 636         return l.toArray((Object[]) Array.newInstance(arrayType, 0));
 637     }
 638 
 639     private static Object getDefaultValue(Class clazz) {
 640         return defaultsMap.get(clazz);
 641     }
 642 
 643     private static final Map<Class<?>, Object> defaultsMap;
 644 
 645     static {
 646         defaultsMap = new HashMap<>();
 647         defaultsMap.put(byte.class, (byte) 0);
 648         defaultsMap.put(short.class, (short) 0);
 649         defaultsMap.put(int.class, 0);
 650         defaultsMap.put(long.class, 0L);
 651         defaultsMap.put(int.class, 0);
 652         defaultsMap.put(float.class, 0.0f);
 653         defaultsMap.put(double.class, 0.0d);
 654         defaultsMap.put(char.class, '\u0000');
 655         defaultsMap.put(boolean.class, false);
 656         defaultsMap.put(Object.class, null);
 657     }
 658 }