Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.fxml;
027
028import com.sun.javafx.fxml.BeanAdapter;
029import com.sun.javafx.fxml.builder.JavaFXFontBuilder;
030import com.sun.javafx.fxml.builder.JavaFXImageBuilder;
031import com.sun.javafx.fxml.builder.JavaFXSceneBuilder;
032import com.sun.javafx.fxml.builder.TriangleMeshBuilder;
033import com.sun.javafx.fxml.builder.URLBuilder;
034import java.lang.reflect.Array;
035import java.lang.reflect.InvocationTargetException;
036import java.lang.reflect.Method;
037import java.lang.reflect.Modifier;
038import java.net.URL;
039import java.util.AbstractMap;
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collection;
043import java.util.HashMap;
044import java.util.HashSet;
045import java.util.Iterator;
046import java.util.List;
047import java.util.Map;
048import java.util.Set;
049import java.util.logging.Level;
050import java.util.logging.Logger;
051import javafx.application.ConditionalFeature;
052import javafx.application.Platform;
053import javafx.collections.FXCollections;
054import javafx.collections.ObservableList;
055import javafx.collections.ObservableMap;
056import javafx.scene.Node;
057import javafx.scene.Scene;
058import javafx.scene.image.Image;
059import javafx.scene.shape.TriangleMesh;
060import javafx.scene.text.Font;
061import javafx.util.Builder;
062import javafx.util.BuilderFactory;
063import sun.reflect.misc.ConstructorUtil;
064import sun.reflect.misc.MethodUtil;
065
066/**
067 * JavaFX builder factory.
068 */
069public final class JavaFXBuilderFactory implements BuilderFactory {
070    private final JavaFXBuilder NO_BUILDER = new JavaFXBuilder();
071
072    private final Map<Class<?>, JavaFXBuilder> builders = new HashMap<Class<?>, JavaFXBuilder>();
073
074    private final ClassLoader classLoader;
075    private final boolean alwaysUseBuilders;
076    
077    private final boolean webSupported;
078
079    /**
080     * Default constructor.
081     */
082    public JavaFXBuilderFactory() {
083        this(FXMLLoader.getDefaultClassLoader(), false);
084    }
085
086    /**
087     * @treatAsPrivate
088     * This constructor is for internal use only.
089     *
090     * @deprecated
091     */
092    public JavaFXBuilderFactory(boolean alwaysUseBuilders) {
093        // SB-dependency: RT-21230 has been filed to track this
094        this(FXMLLoader.getDefaultClassLoader(), alwaysUseBuilders);
095    }
096
097    /**
098     * Constructor that takes a class loader.
099     *
100     * @param classLoader
101     */
102    public JavaFXBuilderFactory(ClassLoader classLoader) {
103        this(classLoader, false);
104    }
105
106    /**
107     * @treatAsPrivate
108     * This constructor is for internal use only.
109     *
110     * @deprecated
111     */
112    public JavaFXBuilderFactory(ClassLoader classLoader, boolean alwaysUseBuilders) {
113        // SB-dependency: RT-21230 has been filed to track this
114        if (classLoader == null) {
115            throw new NullPointerException();
116        }
117
118        this.classLoader = classLoader;
119        this.alwaysUseBuilders = alwaysUseBuilders;
120        this.webSupported = Platform.isSupported(ConditionalFeature.WEB);
121    }
122
123    @Override
124    public Builder<?> getBuilder(Class<?> type) {
125        Builder<?> builder;
126
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 {
138            Builder<Object> objectBuilder = null;
139            JavaFXBuilder typeBuilder = builders.get(type);
140
141            if (typeBuilder != NO_BUILDER) {
142                if (typeBuilder == null) {
143                    // We want to retun a builder here
144                    // only for those classes that reqire it.
145                    // For now we assume that an object that has a default
146                    // constructor does not require a builder. This is the case
147                    // for most platform classes, except those handled above.
148                    // We may need to add other exceptions to the rule if the need
149                    // arises...
150                    //
151                    boolean hasDefaultConstructor;
152                    try {
153                        ConstructorUtil.getConstructor(type, new Class[] {});
154                        // found!
155                        // forces the factory  to return a builder if there is one.
156                        // TODO: delete the line below when we are sure that both
157                        //       builders and default constructors are working!
158                        if (alwaysUseBuilders) throw new Exception();
159
160                        hasDefaultConstructor = true;
161                    } catch (Exception x) {
162                        hasDefaultConstructor = false;
163                    }
164
165                    // Force the loader to use a builder for WebView even though
166                    // it defines a default constructor
167                    if (!hasDefaultConstructor || (webSupported && type.getName().equals("javafx.scene.web.WebView"))) {
168                        try {
169                            typeBuilder = createTypeBuilder(type);
170                        } catch (ClassNotFoundException ex) {
171                            // no builder... will fail later when the FXMLLoader
172                            // will try to instantiate the bean...
173                        }
174                    }
175
176                    builders.put(type, typeBuilder == null ? NO_BUILDER : typeBuilder);
177                    
178                }
179                if (typeBuilder != null) {
180                    objectBuilder = typeBuilder.createBuilder();
181                }
182            }
183
184            builder = objectBuilder;
185        }
186
187        return builder;
188    }
189
190    JavaFXBuilder createTypeBuilder(Class<?> type) throws ClassNotFoundException {
191        JavaFXBuilder typeBuilder = null;
192        Class<?> builderClass = classLoader.loadClass(type.getName() + "Builder");
193        try {
194            typeBuilder = new JavaFXBuilder(builderClass);
195        } catch (Exception ex) {
196            //TODO should be reported
197            Logger.getLogger(JavaFXBuilderFactory.class.getName()).
198                    log(Level.WARNING, "Failed to instantiate JavaFXBuilder for " + builderClass, ex);
199        }
200        if (!alwaysUseBuilders) {
201            Logger.getLogger(JavaFXBuilderFactory.class.getName()).
202                    log(Level.FINER, "class {0} requires a builder.", type);
203        }
204        return typeBuilder;
205    }
206}
207
208/**
209 * JavaFX builder.
210 */
211final class JavaFXBuilder {
212    private static final Object[]   NO_ARGS = {};
213    private static final Class<?>[] NO_SIG = {};
214
215    private final Class<?>           builderClass;
216    private final Method             createMethod;
217    private final Method             buildMethod;
218    private final Map<String,Method> methods = new HashMap<String, Method>();
219    private final Map<String,Method> getters = new HashMap<String,Method>();
220    private final Map<String,Method> setters = new HashMap<String,Method>();
221
222    final class ObjectBuilder extends AbstractMap<String, Object> implements Builder<Object> {
223        private final Map<String,Object> containers = new HashMap<String,Object>();
224        private Object                   builder = null;
225        private Map<Object,Object>       properties;
226
227        private ObjectBuilder() {
228            try {
229                builder = MethodUtil.invoke(createMethod, null, NO_ARGS);
230            } catch (Exception e) {
231                //TODO
232                throw new RuntimeException("Creation of the builder " + builderClass.getName() + " failed.", e);
233            }
234        }
235
236        @Override
237        public Object build() {
238            for (Iterator<Entry<String,Object>> iter = containers.entrySet().iterator(); iter.hasNext(); ) {
239                Entry<String, Object> entry = iter.next();
240
241                put(entry.getKey(), entry.getValue());
242            }
243
244            Object res;
245            try {
246                res = MethodUtil.invoke(buildMethod, builder, NO_ARGS);
247                // TODO:
248                // temporary special case for Node properties until
249                // platform builders are fixed
250                if (properties != null && res instanceof Node) {
251                    ((Map<Object, Object>)((Node)res).getProperties()).putAll(properties);
252                }
253            } catch (InvocationTargetException exception) {
254                throw new RuntimeException(exception);
255            } catch (IllegalAccessException exception) {
256                throw new RuntimeException(exception);
257            } finally {
258                builder = null;
259            }
260
261            return res;
262        }
263
264        @Override
265        public int size() {
266            throw new UnsupportedOperationException();
267        }
268
269        @Override
270        public boolean isEmpty() {
271            throw new UnsupportedOperationException();
272        }
273
274        @Override
275        public boolean containsKey(Object key) {
276            return (getTemporaryContainer(key.toString()) != null);
277        }
278
279        @Override
280        public boolean containsValue(Object value) {
281            throw new UnsupportedOperationException();
282        }
283
284        @Override
285        public Object get(Object key) {
286            return getTemporaryContainer(key.toString());
287        }
288
289        @Override
290        @SuppressWarnings("unchecked")
291        public Object put(String key, Object value) {
292            // TODO:
293            // temporary hack: builders don't have a method for properties...
294            if (Node.class.isAssignableFrom(getTargetClass()) && "properties".equals(key)) {
295                properties = (Map<Object,Object>) value;
296                return null;
297            }
298            try {
299                Method m = methods.get(key);
300                if (m == null) {
301                    m = findMethod(key);
302                    methods.put(key, m);
303                }
304                try {
305                    final Class<?> type = m.getParameterTypes()[0];
306
307                    // If the type is an Array, and our value is a list,
308                    // we simply convert the list into an array. Otherwise,
309                    // we treat the value as a string and split it into a
310                    // list using the array component delimiter.
311                    if (type.isArray()) {
312                        final List<?> list;
313                        if (value instanceof List) {
314                            list = (List<?>)value;
315                        } else {
316                            list = Arrays.asList(value.toString().split(FXMLLoader.ARRAY_COMPONENT_DELIMITER));
317                        }
318
319                        final Class<?> componentType = type.getComponentType();
320                        Object array = Array.newInstance(componentType, list.size());
321                        for (int i=0; i<list.size(); i++) {
322                            Array.set(array, i, BeanAdapter.coerce(list.get(i), componentType));
323                        }
324                        value = array;
325                    }
326
327                    MethodUtil.invoke(m, builder, new Object[] { BeanAdapter.coerce(value, type) });
328                } catch (Exception e) {
329                    Logger.getLogger(JavaFXBuilder.class.getName()).log(Level.WARNING,
330                        "Method " + m.getName() + " failed", e);
331                }
332                //TODO Is it OK to return null here?
333                return null;
334            } catch (Exception e) {
335                //TODO Should be reported
336                Logger.getLogger(JavaFXBuilder.class.getName()).log(Level.WARNING,
337                        "Failed to set "+getTargetClass()+"."+key+" using "+builderClass, e);
338                return null;
339            }
340        }
341
342        // Should do this in BeanAdapter?
343        // This is used to support read-only collection property.
344        // This method must return a Collection of the appropriate type
345        // if 1. the property is read-only, and 2. the property is a collection.
346        // It must return null otherwise.
347        Object getReadOnlyProperty(String propName) {
348            if (setters.get(propName) != null) return null;
349            Method getter = getters.get(propName);
350            if (getter == null) {
351                Method setter = null;
352                Class<?> target = getTargetClass();
353                String suffix = Character.toUpperCase(propName.charAt(0)) + propName.substring(1);
354                try {
355                    getter = MethodUtil.getMethod(target, "get"+ suffix, NO_SIG);
356                    setter = MethodUtil.getMethod(target, "set"+ suffix, new Class[] { getter.getReturnType() });
357                } catch (Exception x) {
358                }
359                if (getter != null) {
360                    getters.put(propName, getter);
361                    setters.put(propName, setter);
362                }
363                if (setter != null) return null;
364            }
365
366            Class<?> type;
367            if (getter == null) {
368                // if we have found no getter it might be a constructor property
369                // try to get the type from the builder method.
370                final Method m = findMethod(propName);
371                if (m == null) {
372                    return null;
373                }
374                type = m.getParameterTypes()[0];
375                if (type.isArray()) type = List.class;
376            } else {
377                type = getter.getReturnType();
378            }
379
380            if (ObservableMap.class.isAssignableFrom(type)) {
381                return FXCollections.observableMap(new HashMap<Object, Object>());
382            } else if (Map.class.isAssignableFrom(type)) {
383                return new HashMap<Object, Object>();
384            } else if (ObservableList.class.isAssignableFrom(type)) {
385                return FXCollections.observableArrayList();
386            } else if (List.class.isAssignableFrom(type)) {
387                return new ArrayList<Object>();
388            } else if (Set.class.isAssignableFrom(type)) {
389                return new HashSet<Object>();
390            }
391            return null;
392        }
393
394        /**
395         * This is used to support read-only collection property.
396         * This method must return a Collection of the appropriate type
397         * if 1. the property is read-only, and 2. the property is a collection.
398         * It must return null otherwise.
399         **/
400        public Object getTemporaryContainer(String propName) {
401            Object o = containers.get(propName);
402            if (o == null) {
403                o = getReadOnlyProperty(propName);
404                if (o != null) {
405                    containers.put(propName, o);
406                }
407            }
408
409            return o;
410        }
411
412        @Override
413        public Object remove(Object key) {
414            throw new UnsupportedOperationException();
415        }
416
417        @Override
418        public void putAll(Map<? extends String, ? extends Object> m) {
419            throw new UnsupportedOperationException();
420        }
421
422        @Override
423        public void clear() {
424            throw new UnsupportedOperationException();
425        }
426
427        @Override
428        public Set<String> keySet() {
429            throw new UnsupportedOperationException();
430        }
431
432        @Override
433        public Collection<Object> values() {
434            throw new UnsupportedOperationException();
435        }
436
437        @Override
438        public Set<Entry<String, Object>> entrySet() {
439            throw new UnsupportedOperationException();
440        }
441    }
442
443    JavaFXBuilder() {
444        builderClass = null;
445        createMethod = null;
446        buildMethod = null;
447    }
448
449    JavaFXBuilder(Class<?> builderClass) throws NoSuchMethodException, InstantiationException, IllegalAccessException {
450        this.builderClass = builderClass;
451        createMethod = MethodUtil.getMethod(builderClass, "create", NO_SIG);
452        buildMethod = MethodUtil.getMethod(builderClass, "build", NO_SIG);
453        assert Modifier.isStatic(createMethod.getModifiers());
454        assert !Modifier.isStatic(buildMethod.getModifiers());
455    }
456
457    Builder<Object> createBuilder() {
458        return new ObjectBuilder();
459    }
460
461    private Method findMethod(String name) {
462        if (name.length() > 1
463            && Character.isUpperCase(name.charAt(1))) {
464            name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
465        }
466
467        for (Method m : MethodUtil.getMethods(builderClass)) {
468            if (m.getName().equals(name)) {
469                return m;
470            }
471        }
472        throw new IllegalArgumentException("Method " + name + " could not be found at class " + builderClass.getName());
473    }
474
475    /**
476     * The type constructed by this builder.
477     * @return The type constructed by this builder.
478     */
479    public Class<?> getTargetClass() {
480        return buildMethod.getReturnType();
481    }
482}