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 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 com.sun.javafx.reflect.ConstructorUtil;
  67 import com.sun.javafx.reflect.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 }