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 }