1 /* 2 * Copyright (c) 2010, 2015, 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 java.lang.annotation.Annotation; 29 import java.lang.reflect.Array; 30 import java.lang.reflect.Constructor; 31 import java.lang.reflect.Method; 32 import java.lang.reflect.Modifier; 33 import java.util.AbstractMap; 34 import java.util.ArrayList; 35 import java.util.Collection; 36 import java.util.Comparator; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.LinkedHashMap; 40 import java.util.LinkedList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Set; 44 import java.util.TreeSet; 45 import javafx.beans.NamedArg; 46 import javafx.util.Builder; 47 import sun.reflect.misc.ConstructorUtil; 48 import sun.reflect.misc.MethodUtil; 49 import sun.reflect.misc.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 (Collection.class.isAssignableFrom(type)) { 451 return val; 452 } 453 454 if (ArrayListWrapper.class.equals(val.getClass())) { 455 // user given value is an ArrayList but the constructor doesn't 456 // accept an ArrayList so the ArrayList comes from 457 // the getTemporaryContainer method 458 // we take the first argument 459 List l = (List) val; 460 return l.get(0); 461 } 462 463 return val; 464 } 465 466 private Object createObjectWithExactArguments(Constructor c, Set<String> argumentNames) { 467 Object retObj = null; 468 Object argsForConstruction[] = new Object[argumentNames.size()]; 469 Map<String, AnnotationValue> constructorArgsMap = constructorsMap.get(c); 470 471 int i = 0; 472 473 for (String arg : argumentNames) { 474 Class<?> tp = constructorArgsMap.get(arg).getType(); 475 Object value = getUserValue(arg, tp); 476 try { 477 argsForConstruction[i++] = BeanAdapter.coerce(value, tp); 478 } catch (Exception ex) { 479 return null; 480 } 481 } 482 483 try { 484 retObj = createInstance(c, argsForConstruction); 485 } catch (Exception ex) { 486 // will try to fall back to different constructor 487 } 488 489 return retObj; 490 } 491 492 private Object createInstance(Constructor c, Object args[]) throws Exception { 493 Object retObj = null; 494 495 ReflectUtil.checkPackageAccess(type); 496 retObj = c.newInstance(args); 497 498 return retObj; 499 } 500 501 private Map<String, Property> scanForSetters() { 502 Map<String, Property> strsMap = new HashMap<>(); 503 Map<String, LinkedList<Method>> methods = getClassMethodCache(type); 504 505 for (String methodName : methods.keySet()) { 506 if (methodName.startsWith(SETTER_PREFIX)) { 507 String propName = methodName.substring(SETTER_PREFIX.length()); 508 propName = Character.toLowerCase(propName.charAt(0)) + propName.substring(1); 509 List<Method> methodsList = methods.get(methodName); 510 for (Method m : methodsList) { 511 Class<?> retType = m.getReturnType(); 512 Class<?> argType[] = m.getParameterTypes(); 513 if (retType.equals(Void.TYPE) && argType.length == 1) { 514 strsMap.put(propName, new Setter(m, argType[0])); 515 } 516 } 517 } 518 if (methodName.startsWith(GETTER_PREFIX)) { 519 String propName = methodName.substring(SETTER_PREFIX.length()); 520 propName = Character.toLowerCase(propName.charAt(0)) + propName.substring(1); 521 List<Method> methodsList = methods.get(methodName); 522 for (Method m : methodsList) { 523 Class<?> retType = m.getReturnType(); 524 Class<?> argType[] = m.getParameterTypes(); 525 if (Collection.class.isAssignableFrom(retType) && argType.length == 0) { 526 strsMap.put(propName, new Getter(m, retType)); 527 } 528 } 529 } 530 } 531 532 return strsMap; 533 } 534 535 private static abstract class Property { 536 protected final Method method; 537 protected final Class<?> type; 538 539 public Property(Method m, Class<?> t) { 540 method = m; 541 type = t; 542 } 543 544 public Class<?> getType() { 545 return type; 546 } 547 548 public abstract void invoke(Object obj, Object argStr) throws Exception; 549 } 550 551 private static class Setter extends Property { 552 553 public Setter(Method m, Class<?> t) { 554 super(m, t); 555 } 556 557 public void invoke(Object obj, Object argStr) throws Exception { 558 Object arg[] = new Object[]{BeanAdapter.coerce(argStr, type)}; 559 MethodUtil.invoke(method, obj, arg); 560 } 561 } 562 563 private static class Getter extends Property { 564 565 public Getter(Method m, Class<?> t) { 566 super(m, t); 567 } 568 569 @Override 570 public void invoke(Object obj, Object argStr) throws Exception { 571 // we know that this.method returns collection otherwise it wouldn't be here 572 Collection to = (Collection) MethodUtil.invoke(method, obj, new Object[]{}); 573 if (argStr instanceof Collection) { 574 Collection from = (Collection) argStr; 575 to.addAll(from); 576 } else { 577 to.add(argStr); 578 } 579 } 580 } 581 582 // This class holds information for one argument of the constructor 583 // which we got from the NamedArg annotation 584 private static class AnnotationValue { 585 586 private final String name; 587 private final String defaultValue; 588 private final Class<?> type; 589 590 public AnnotationValue(String name, String defaultValue, Class<?> type) { 591 this.name = name; 592 this.defaultValue = defaultValue; 593 this.type = type; 594 } 595 596 public String getName() { 597 return name; 598 } 599 600 public String getDefaultValue() { 601 return defaultValue; 602 } 603 604 public Class<?> getType() { 605 return type; 606 } 607 } 608 609 private static HashMap<String, LinkedList<Method>> getClassMethodCache(Class<?> type) { 610 HashMap<String, LinkedList<Method>> classMethodCache = new HashMap<>(); 611 612 ReflectUtil.checkPackageAccess(type); 613 614 Method[] declaredMethods = type.getMethods(); 615 for (Method method : declaredMethods) { 616 int modifiers = method.getModifiers(); 617 618 if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) { 619 String name = method.getName(); 620 LinkedList<Method> namedMethods = classMethodCache.get(name); 621 622 if (namedMethods == null) { 623 namedMethods = new LinkedList<>(); 624 classMethodCache.put(name, namedMethods); 625 } 626 627 namedMethods.add(method); 628 } 629 } 630 631 return classMethodCache; 632 } 633 634 // Utility method for converting list to array via reflection 635 // it assumes that localType is array 636 private static Object[] convertListToArray(Object userValue, Class<?> localType) { 637 Class<?> arrayType = localType.getComponentType(); 638 List l = (List) BeanAdapter.coerce(userValue, List.class); 639 640 return l.toArray((Object[]) Array.newInstance(arrayType, 0)); 641 } 642 643 private static Object getDefaultValue(Class clazz) { 644 return defaultsMap.get(clazz); 645 } 646 647 private static final Map<Class<?>, Object> defaultsMap; 648 649 static { 650 defaultsMap = new HashMap<>(); 651 defaultsMap.put(byte.class, (byte) 0); 652 defaultsMap.put(short.class, (short) 0); 653 defaultsMap.put(int.class, 0); 654 defaultsMap.put(long.class, 0L); 655 defaultsMap.put(int.class, 0); 656 defaultsMap.put(float.class, 0.0f); 657 defaultsMap.put(double.class, 0.0d); 658 defaultsMap.put(char.class, '\u0000'); 659 defaultsMap.put(boolean.class, false); 660 defaultsMap.put(Object.class, null); 661 } 662 }