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