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