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 }