/* * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * * * * * * * * * * * * * * * * * * * * */ package javafx.fxml; import com.sun.javafx.fxml.BeanAdapter; import com.sun.javafx.fxml.builder.JavaFXFontBuilder; import com.sun.javafx.fxml.builder.JavaFXImageBuilder; import com.sun.javafx.fxml.builder.JavaFXSceneBuilder; import com.sun.javafx.fxml.builder.ProxyBuilder; import com.sun.javafx.fxml.builder.TriangleMeshBuilder; import com.sun.javafx.fxml.builder.URLBuilder; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.ConditionalFeature; import javafx.application.Platform; import javafx.beans.NamedArg; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.shape.TriangleMesh; import javafx.scene.text.Font; import javafx.util.Builder; import javafx.util.BuilderFactory; import sun.reflect.misc.ConstructorUtil; import sun.reflect.misc.MethodUtil; /** * JavaFX builder factory. * @since JavaFX 2.0 */ public final class JavaFXBuilderFactory implements BuilderFactory { private final JavaFXBuilder NO_BUILDER = new JavaFXBuilder(); private final Map, JavaFXBuilder> builders = new HashMap, JavaFXBuilder>(); private final ClassLoader classLoader; private final boolean alwaysUseBuilders; private final boolean webSupported; /** * Default constructor. */ public JavaFXBuilderFactory() { this(FXMLLoader.getDefaultClassLoader(), false); } /** * @treatAsPrivate * This constructor is for internal use only. * * @deprecated */ public JavaFXBuilderFactory(boolean alwaysUseBuilders) { // SB-dependency: RT-21230 has been filed to track this this(FXMLLoader.getDefaultClassLoader(), alwaysUseBuilders); } /** * Constructor that takes a class loader. * * @param classLoader * @since JavaFX 2.1 */ public JavaFXBuilderFactory(ClassLoader classLoader) { this(classLoader, false); } /** * @treatAsPrivate * This constructor is for internal use only. * * @deprecated * @since JavaFX 2.1 */ public JavaFXBuilderFactory(ClassLoader classLoader, boolean alwaysUseBuilders) { // SB-dependency: RT-21230 has been filed to track this if (classLoader == null) { throw new NullPointerException(); } this.classLoader = classLoader; this.alwaysUseBuilders = alwaysUseBuilders; this.webSupported = Platform.isSupported(ConditionalFeature.WEB); } @Override public Builder getBuilder(Class type) { Builder builder; if (type == Scene.class) { builder = new JavaFXSceneBuilder(); } else if (type == Font.class) { builder = new JavaFXFontBuilder(); } else if (type == Image.class) { builder = new JavaFXImageBuilder(); } else if (type == URL.class) { builder = new URLBuilder(classLoader); } else if (type == TriangleMesh.class) { builder = new TriangleMeshBuilder(); } else if (scanForConstructorAnnotations(type)) { builder = new ProxyBuilder(type); } else { Builder objectBuilder = null; JavaFXBuilder typeBuilder = builders.get(type); if (typeBuilder != NO_BUILDER) { if (typeBuilder == null) { // We want to retun a builder here // only for those classes that reqire it. // For now we assume that an object that has a default // constructor does not require a builder. This is the case // for most platform classes, except those handled above. // We may need to add other exceptions to the rule if the need // arises... // boolean hasDefaultConstructor; try { ConstructorUtil.getConstructor(type, new Class[] {}); // found! // forces the factory to return a builder if there is one. // TODO: delete the line below when we are sure that both // builders and default constructors are working! if (alwaysUseBuilders) throw new Exception(); hasDefaultConstructor = true; } catch (Exception x) { hasDefaultConstructor = false; } // Force the loader to use a builder for WebView even though // it defines a default constructor if (!hasDefaultConstructor || (webSupported && type.getName().equals("javafx.scene.web.WebView"))) { try { typeBuilder = createTypeBuilder(type); } catch (ClassNotFoundException ex) { // no builder... will fail later when the FXMLLoader // will try to instantiate the bean... } } builders.put(type, typeBuilder == null ? NO_BUILDER : typeBuilder); } if (typeBuilder != null) { objectBuilder = typeBuilder.createBuilder(); } } builder = objectBuilder; } return builder; } JavaFXBuilder createTypeBuilder(Class type) throws ClassNotFoundException { JavaFXBuilder typeBuilder = null; Class builderClass = classLoader.loadClass(type.getName() + "Builder"); try { typeBuilder = new JavaFXBuilder(builderClass); } catch (Exception ex) { //TODO should be reported Logger.getLogger(JavaFXBuilderFactory.class.getName()). log(Level.WARNING, "Failed to instantiate JavaFXBuilder for " + builderClass, ex); } if (!alwaysUseBuilders) { Logger.getLogger(JavaFXBuilderFactory.class.getName()). log(Level.FINER, "class {0} requires a builder.", type); } return typeBuilder; } private boolean scanForConstructorAnnotations(Class type) { Constructor constructors[] = ConstructorUtil.getConstructors(type); for (Constructor constructor : constructors) { Annotation[][] paramAnnotations = constructor.getParameterAnnotations(); for (int i = 0; i < constructor.getParameterTypes().length; i++) { for (Annotation annotation : paramAnnotations[i]) { if (annotation instanceof NamedArg) { return true; } } } } return false; } } /** * JavaFX builder. */ final class JavaFXBuilder { private static final Object[] NO_ARGS = {}; private static final Class[] NO_SIG = {}; private final Class builderClass; private final Method createMethod; private final Method buildMethod; private final Map methods = new HashMap(); private final Map getters = new HashMap(); private final Map setters = new HashMap(); final class ObjectBuilder extends AbstractMap implements Builder { private final Map containers = new HashMap(); private Object builder = null; private Map properties; private ObjectBuilder() { try { builder = MethodUtil.invoke(createMethod, null, NO_ARGS); } catch (Exception e) { //TODO throw new RuntimeException("Creation of the builder " + builderClass.getName() + " failed.", e); } } @Override public Object build() { for (Iterator> iter = containers.entrySet().iterator(); iter.hasNext(); ) { Entry entry = iter.next(); put(entry.getKey(), entry.getValue()); } Object res; try { res = MethodUtil.invoke(buildMethod, builder, NO_ARGS); // TODO: // temporary special case for Node properties until // platform builders are fixed if (properties != null && res instanceof Node) { ((Map)((Node)res).getProperties()).putAll(properties); } } catch (InvocationTargetException exception) { throw new RuntimeException(exception); } catch (IllegalAccessException exception) { throw new RuntimeException(exception); } finally { builder = null; } return res; } @Override public int size() { throw new UnsupportedOperationException(); } @Override public boolean isEmpty() { throw new UnsupportedOperationException(); } @Override public boolean containsKey(Object key) { return (getTemporaryContainer(key.toString()) != null); } @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } @Override public Object get(Object key) { return getTemporaryContainer(key.toString()); } @Override @SuppressWarnings("unchecked") public Object put(String key, Object value) { // TODO: // temporary hack: builders don't have a method for properties... if (Node.class.isAssignableFrom(getTargetClass()) && "properties".equals(key)) { properties = (Map) value; return null; } try { Method m = methods.get(key); if (m == null) { m = findMethod(key); methods.put(key, m); } try { final Class type = m.getParameterTypes()[0]; // If the type is an Array, and our value is a list, // we simply convert the list into an array. Otherwise, // we treat the value as a string and split it into a // list using the array component delimiter. if (type.isArray()) { final List list; if (value instanceof List) { list = (List)value; } else { list = Arrays.asList(value.toString().split(FXMLLoader.ARRAY_COMPONENT_DELIMITER)); } final Class componentType = type.getComponentType(); Object array = Array.newInstance(componentType, list.size()); for (int i=0; i target = getTargetClass(); String suffix = Character.toUpperCase(propName.charAt(0)) + propName.substring(1); try { getter = MethodUtil.getMethod(target, "get"+ suffix, NO_SIG); setter = MethodUtil.getMethod(target, "set"+ suffix, new Class[] { getter.getReturnType() }); } catch (Exception x) { } if (getter != null) { getters.put(propName, getter); setters.put(propName, setter); } if (setter != null) return null; } Class type; if (getter == null) { // if we have found no getter it might be a constructor property // try to get the type from the builder method. final Method m = findMethod(propName); if (m == null) { return null; } type = m.getParameterTypes()[0]; if (type.isArray()) type = List.class; } else { type = getter.getReturnType(); } if (ObservableMap.class.isAssignableFrom(type)) { return FXCollections.observableMap(new HashMap()); } else if (Map.class.isAssignableFrom(type)) { return new HashMap(); } else if (ObservableList.class.isAssignableFrom(type)) { return FXCollections.observableArrayList(); } else if (List.class.isAssignableFrom(type)) { return new ArrayList(); } else if (Set.class.isAssignableFrom(type)) { return new HashSet(); } return null; } /** * This is used to support read-only collection property. * This method must return a Collection of the appropriate type * if 1. the property is read-only, and 2. the property is a collection. * It must return null otherwise. **/ public Object getTemporaryContainer(String propName) { Object o = containers.get(propName); if (o == null) { o = getReadOnlyProperty(propName); if (o != null) { containers.put(propName, o); } } return o; } @Override public Object remove(Object key) { throw new UnsupportedOperationException(); } @Override public void putAll(Map m) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } @Override public Set keySet() { throw new UnsupportedOperationException(); } @Override public Collection values() { throw new UnsupportedOperationException(); } @Override public Set> entrySet() { throw new UnsupportedOperationException(); } } JavaFXBuilder() { builderClass = null; createMethod = null; buildMethod = null; } JavaFXBuilder(Class builderClass) throws NoSuchMethodException, InstantiationException, IllegalAccessException { this.builderClass = builderClass; createMethod = MethodUtil.getMethod(builderClass, "create", NO_SIG); buildMethod = MethodUtil.getMethod(builderClass, "build", NO_SIG); assert Modifier.isStatic(createMethod.getModifiers()); assert !Modifier.isStatic(buildMethod.getModifiers()); } Builder createBuilder() { return new ObjectBuilder(); } private Method findMethod(String name) { if (name.length() > 1 && Character.isUpperCase(name.charAt(1))) { name = Character.toUpperCase(name.charAt(0)) + name.substring(1); } for (Method m : MethodUtil.getMethods(builderClass)) { if (m.getName().equals(name)) { return m; } } throw new IllegalArgumentException("Method " + name + " could not be found at class " + builderClass.getName()); } /** * The type constructed by this builder. * @return The type constructed by this builder. */ public Class getTargetClass() { return buildMethod.getReturnType(); } }