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 javafx.fxml; 26 27 import com.sun.javafx.fxml.BeanAdapter; 28 import com.sun.javafx.fxml.ModuleHelper; 29 import com.sun.javafx.fxml.builder.JavaFXFontBuilder; 30 import com.sun.javafx.fxml.builder.JavaFXImageBuilder; 31 import com.sun.javafx.fxml.builder.JavaFXSceneBuilder; 32 import com.sun.javafx.fxml.builder.ProxyBuilder; 33 import com.sun.javafx.fxml.builder.TriangleMeshBuilder; 34 import com.sun.javafx.fxml.builder.URLBuilder; 35 import java.lang.annotation.Annotation; 36 import java.lang.reflect.Array; 37 import java.lang.reflect.Constructor; 38 import java.lang.reflect.InvocationTargetException; 39 import java.lang.reflect.Method; 40 import java.lang.reflect.Modifier; 41 import java.net.URL; 42 import java.util.AbstractMap; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.Collection; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.Iterator; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Set; 52 import java.util.logging.Level; 53 import java.util.logging.Logger; 54 import javafx.application.ConditionalFeature; 55 import javafx.application.Platform; 56 import javafx.beans.NamedArg; 57 import javafx.collections.FXCollections; 58 import javafx.collections.ObservableList; 59 import javafx.collections.ObservableMap; 60 import javafx.scene.Node; 61 import javafx.scene.Scene; 62 import javafx.scene.image.Image; 63 import javafx.scene.shape.TriangleMesh; 64 import javafx.scene.text.Font; 65 import javafx.util.Builder; 66 import javafx.util.BuilderFactory; 67 import sun.reflect.misc.ConstructorUtil; 68 import sun.reflect.misc.MethodUtil; 69 70 /** 71 * JavaFX builder factory. 72 * @since JavaFX 2.0 73 */ 74 public final class JavaFXBuilderFactory implements BuilderFactory { 75 private final ClassLoader classLoader; 76 private final boolean webSupported; 77 private static final String WEBVIEW_NAME = "javafx.scene.web.WebView"; 78 79 // WebViewBuilder class name loaded via reflection 80 // TODO: Uncomment the following when RT-40037 is fixed. 81 // private static final String WEBVIEW_BUILDER_NAME = 82 // "com.sun.javafx.fxml.builder.web.JavaFXWebViewBuilder"; 83 84 // TODO: Remove the following when RT-40037 is fixed. 85 private static final String WEBVIEW_BUILDER_NAME = 86 "com.sun.javafx.fxml.builder.web.WebViewBuilder"; 87 88 /** 89 * Default constructor. 90 */ 91 public JavaFXBuilderFactory() { 92 this(FXMLLoader.getDefaultClassLoader()); 93 } 94 95 /** 96 * Constructor that takes a class loader. 97 * 98 * @param classLoader 99 * @since JavaFX 2.1 100 */ 101 public JavaFXBuilderFactory(ClassLoader classLoader) { 102 if (classLoader == null) { 103 throw new NullPointerException(); 104 } 105 106 this.classLoader = classLoader; 107 this.webSupported = Platform.isSupported(ConditionalFeature.WEB); 108 } 109 110 /** 111 * Returns the builder for the specified type, or null if no builder is 112 * used. Most classes will note use a builder. 113 * 114 * @param type the class being looked up. 115 * 116 * @return the builder for the class, or null if no builder is use. 117 */ 118 @Override 119 public Builder<?> getBuilder(Class<?> type) { 120 if (type == null) { 121 throw new NullPointerException(); 122 } 123 124 Builder<?> builder; 125 126 // All classes without a default constructor need to appear here, as 127 // well as any other class that has special requirements that need 128 // a builder to handle them. 129 if (type == Scene.class) { 130 builder = new JavaFXSceneBuilder(); 131 } else if (type == Font.class) { 132 builder = new JavaFXFontBuilder(); 133 } else if (type == Image.class) { 134 builder = new JavaFXImageBuilder(); 135 } else if (type == URL.class) { 136 builder = new URLBuilder(classLoader); 137 } else if (type == TriangleMesh.class) { 138 builder = new TriangleMeshBuilder(); 139 } else if (webSupported && type.getName().equals(WEBVIEW_NAME)) { 140 141 // TODO: enable this code when RT-40037 is fixed. 142 // // Construct a WebViewBuilder via reflection 143 // try { 144 // Class<Builder<?>> builderClass = 145 // (Class<Builder<?>>)classLoader.loadClass(WEBVIEW_BUILDER_NAME); 146 // Constructor<Builder<?>> constructor = builderClass.getConstructor(new Class[0]); 147 // builder = constructor.newInstance(); 148 // } catch (Exception ex) { 149 // // This should never happen 150 // ex.printStackTrace(); 151 // builder = null; 152 // } 153 154 // TODO: Remove the following when RT-40037 is fixed. 155 try { 156 Class<?> builderClass = classLoader.loadClass(WEBVIEW_BUILDER_NAME); 157 ObjectBuilderWrapper wrapper = new ObjectBuilderWrapper(builderClass); 158 builder = wrapper.createBuilder(); 159 } catch (Exception ex) { 160 builder = null; 161 } 162 } else if (scanForConstructorAnnotations(type)) { 163 builder = new ProxyBuilder(type); 164 } else { 165 // No builder will be used to construct this class. The class must 166 // have a public default constructor, which is the case for all 167 // platform classes, except those handled above. 168 builder = null; 169 } 170 171 return builder; 172 } 173 174 private boolean scanForConstructorAnnotations(Class<?> type) { 175 Constructor constructors[] = ConstructorUtil.getConstructors(type); 176 for (Constructor constructor : constructors) { 177 Annotation[][] paramAnnotations = constructor.getParameterAnnotations(); 178 for (int i = 0; i < constructor.getParameterTypes().length; i++) { 179 for (Annotation annotation : paramAnnotations[i]) { 180 if (annotation instanceof NamedArg) { 181 return true; 182 } 183 } 184 } 185 } 186 return false; 187 } 188 189 190 /** 191 * Legacy ObjectBuilder wrapper. 192 * 193 * TODO: move this legacy functionality to JavaFXWebViewBuilder and modify 194 * it to work without requiring the legacy builders. See RT-40037. 195 */ 196 private static final class ObjectBuilderWrapper { 197 private static final Object[] NO_ARGS = {}; 198 private static final Class<?>[] NO_SIG = {}; 199 200 private final Class<?> builderClass; 201 private final Method createMethod; 202 private final Method buildMethod; 203 private final Map<String,Method> methods = new HashMap<String, Method>(); 204 private final Map<String,Method> getters = new HashMap<String,Method>(); 205 private final Map<String,Method> setters = new HashMap<String,Method>(); 206 207 final class ObjectBuilder extends AbstractMap<String, Object> implements Builder<Object> { 208 private final Map<String,Object> containers = new HashMap<String,Object>(); 209 private Object builder = null; 210 private Map<Object,Object> properties; 211 212 private ObjectBuilder() { 213 try { 214 builder = createMethod.invoke(null, NO_ARGS); 215 } catch (Exception e) { 216 //TODO 217 throw new RuntimeException("Creation of the builder " + builderClass.getName() + " failed.", e); 218 } 219 } 220 221 @Override 222 public Object build() { 223 for (Iterator<Entry<String,Object>> iter = containers.entrySet().iterator(); iter.hasNext(); ) { 224 Entry<String, Object> entry = iter.next(); 225 226 put(entry.getKey(), entry.getValue()); 227 } 228 229 Object res; 230 try { 231 res = buildMethod.invoke(builder, NO_ARGS); 232 // TODO: 233 // temporary special case for Node properties until 234 // platform builders are fixed 235 if (properties != null && res instanceof Node) { 236 ((Map<Object, Object>)((Node)res).getProperties()).putAll(properties); 237 } 238 } catch (InvocationTargetException exception) { 239 throw new RuntimeException(exception); 240 } catch (IllegalAccessException exception) { 241 throw new RuntimeException(exception); 242 } finally { 243 builder = null; 244 } 245 246 return res; 247 } 248 249 @Override 250 public int size() { 251 throw new UnsupportedOperationException(); 252 } 253 254 @Override 255 public boolean isEmpty() { 256 throw new UnsupportedOperationException(); 257 } 258 259 @Override 260 public boolean containsKey(Object key) { 261 return (getTemporaryContainer(key.toString()) != null); 262 } 263 264 @Override 265 public boolean containsValue(Object value) { 266 throw new UnsupportedOperationException(); 267 } 268 269 @Override 270 public Object get(Object key) { 271 return getTemporaryContainer(key.toString()); 272 } 273 274 @Override 275 @SuppressWarnings("unchecked") 276 public Object put(String key, Object value) { 277 // TODO: 278 // temporary hack: builders don't have a method for properties... 279 if (Node.class.isAssignableFrom(getTargetClass()) && "properties".equals(key)) { 280 properties = (Map<Object,Object>) value; 281 return null; 282 } 283 try { 284 Method m = methods.get(key); 285 if (m == null) { 286 m = findMethod(key); 287 methods.put(key, m); 288 } 289 try { 290 final Class<?> type = m.getParameterTypes()[0]; 291 292 // If the type is an Array, and our value is a list, 293 // we simply convert the list into an array. Otherwise, 294 // we treat the value as a string and split it into a 295 // list using the array component delimiter. 296 if (type.isArray()) { 297 final List<?> list; 298 if (value instanceof List) { 299 list = (List<?>)value; 300 } else { 301 list = Arrays.asList(value.toString().split(FXMLLoader.ARRAY_COMPONENT_DELIMITER)); 302 } 303 304 final Class<?> componentType = type.getComponentType(); 305 Object array = Array.newInstance(componentType, list.size()); 306 for (int i=0; i<list.size(); i++) { 307 Array.set(array, i, BeanAdapter.coerce(list.get(i), componentType)); 308 } 309 value = array; 310 } 311 312 m.invoke(builder, new Object[] { BeanAdapter.coerce(value, type) }); 313 } catch (Exception e) { 314 Logger.getLogger(ObjectBuilderWrapper.class.getName()).log(Level.WARNING, 315 "Method " + m.getName() + " failed", e); 316 } 317 //TODO Is it OK to return null here? 318 return null; 319 } catch (Exception e) { 320 //TODO Should be reported 321 Logger.getLogger(ObjectBuilderWrapper.class.getName()).log(Level.WARNING, 322 "Failed to set "+getTargetClass()+"."+key+" using "+builderClass, e); 323 return null; 324 } 325 } 326 327 // Should do this in BeanAdapter? 328 // This is used to support read-only collection property. 329 // This method must return a Collection of the appropriate type 330 // if 1. the property is read-only, and 2. the property is a collection. 331 // It must return null otherwise. 332 Object getReadOnlyProperty(String propName) { 333 if (setters.get(propName) != null) return null; 334 Method getter = getters.get(propName); 335 if (getter == null) { 336 Method setter = null; 337 Class<?> target = getTargetClass(); 338 String suffix = Character.toUpperCase(propName.charAt(0)) + propName.substring(1); 339 try { 340 getter = MethodUtil.getMethod(target, "get"+ suffix, NO_SIG); 341 setter = MethodUtil.getMethod(target, "set"+ suffix, new Class[] { getter.getReturnType() }); 342 } catch (Exception x) { 343 } 344 if (getter != null) { 345 getters.put(propName, getter); 346 setters.put(propName, setter); 347 } 348 if (setter != null) return null; 349 } 350 351 Class<?> type; 352 if (getter == null) { 353 // if we have found no getter it might be a constructor property 354 // try to get the type from the builder method. 355 final Method m = findMethod(propName); 356 if (m == null) { 357 return null; 358 } 359 type = m.getParameterTypes()[0]; 360 if (type.isArray()) type = List.class; 361 } else { 362 type = getter.getReturnType(); 363 } 364 365 if (ObservableMap.class.isAssignableFrom(type)) { 366 return FXCollections.observableMap(new HashMap<Object, Object>()); 367 } else if (Map.class.isAssignableFrom(type)) { 368 return new HashMap<Object, Object>(); 369 } else if (ObservableList.class.isAssignableFrom(type)) { 370 return FXCollections.observableArrayList(); 371 } else if (List.class.isAssignableFrom(type)) { 372 return new ArrayList<Object>(); 373 } else if (Set.class.isAssignableFrom(type)) { 374 return new HashSet<Object>(); 375 } 376 return null; 377 } 378 379 /** 380 * This is used to support read-only collection property. 381 * This method must return a Collection of the appropriate type 382 * if 1. the property is read-only, and 2. the property is a collection. 383 * It must return null otherwise. 384 **/ 385 public Object getTemporaryContainer(String propName) { 386 Object o = containers.get(propName); 387 if (o == null) { 388 o = getReadOnlyProperty(propName); 389 if (o != null) { 390 containers.put(propName, o); 391 } 392 } 393 394 return o; 395 } 396 397 @Override 398 public Object remove(Object key) { 399 throw new UnsupportedOperationException(); 400 } 401 402 @Override 403 public void putAll(Map<? extends String, ? extends Object> m) { 404 throw new UnsupportedOperationException(); 405 } 406 407 @Override 408 public void clear() { 409 throw new UnsupportedOperationException(); 410 } 411 412 @Override 413 public Set<String> keySet() { 414 throw new UnsupportedOperationException(); 415 } 416 417 @Override 418 public Collection<Object> values() { 419 throw new UnsupportedOperationException(); 420 } 421 422 @Override 423 public Set<Entry<String, Object>> entrySet() { 424 throw new UnsupportedOperationException(); 425 } 426 } 427 428 ObjectBuilderWrapper() { 429 builderClass = null; 430 createMethod = null; 431 buildMethod = null; 432 } 433 434 ObjectBuilderWrapper(Class<?> builderClass) throws NoSuchMethodException, InstantiationException, IllegalAccessException { 435 this.builderClass = builderClass; 436 Object thisModule = ModuleHelper.getModule(this.getClass()); 437 ModuleHelper.addReads(thisModule, ModuleHelper.getModule(builderClass)); 438 createMethod = MethodUtil.getMethod(builderClass, "create", NO_SIG); 439 buildMethod = MethodUtil.getMethod(builderClass, "build", NO_SIG); 440 assert Modifier.isStatic(createMethod.getModifiers()); 441 assert !Modifier.isStatic(buildMethod.getModifiers()); 442 } 443 444 Builder<Object> createBuilder() { 445 return new ObjectBuilder(); 446 } 447 448 private Method findMethod(String name) { 449 if (name.length() > 1 450 && Character.isUpperCase(name.charAt(1))) { 451 name = Character.toUpperCase(name.charAt(0)) + name.substring(1); 452 } 453 454 for (Method m : MethodUtil.getMethods(builderClass)) { 455 if (m.getName().equals(name)) { 456 return m; 457 } 458 } 459 throw new IllegalArgumentException("Method " + name + " could not be found at class " + builderClass.getName()); 460 } 461 462 /** 463 * The type constructed by this builder. 464 * @return The type constructed by this builder. 465 */ 466 public Class<?> getTargetClass() { 467 return buildMethod.getReturnType(); 468 } 469 } 470 471 }