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.Logging;
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.InputStreamReader;
032import java.lang.reflect.Array;
033import java.lang.reflect.Constructor;
034import java.lang.reflect.Field;
035import java.lang.reflect.InvocationTargetException;
036import java.lang.reflect.Method;
037import java.lang.reflect.Modifier;
038import java.lang.reflect.ParameterizedType;
039import java.lang.reflect.Type;
040import java.net.URL;
041import java.nio.charset.Charset;
042import java.util.AbstractMap;
043import java.util.ArrayList;
044import java.util.Collections;
045import java.util.HashMap;
046import java.util.LinkedList;
047import java.util.List;
048import java.util.Map;
049import java.util.ResourceBundle;
050import java.util.Set;
051import java.util.regex.Pattern;
052
053import javafx.beans.DefaultProperty;
054import javafx.beans.property.Property;
055import javafx.beans.value.ChangeListener;
056import javafx.beans.value.ObservableValue;
057import javafx.collections.FXCollections;
058import javafx.collections.ListChangeListener;
059import javafx.collections.MapChangeListener;
060import javafx.collections.ObservableList;
061import javafx.collections.ObservableMap;
062import javafx.event.Event;
063import javafx.event.EventHandler;
064import javafx.event.EventType;
065import javafx.util.Builder;
066import javafx.util.BuilderFactory;
067import javafx.util.Callback;
068
069import javax.script.Bindings;
070import javax.script.ScriptContext;
071import javax.script.ScriptEngine;
072import javax.script.ScriptEngineManager;
073import javax.script.ScriptException;
074import javax.script.SimpleBindings;
075import javax.xml.stream.XMLInputFactory;
076import javax.xml.stream.XMLStreamConstants;
077import javax.xml.stream.XMLStreamException;
078import javax.xml.stream.XMLStreamReader;
079import javax.xml.stream.util.StreamReaderDelegate;
080
081import com.sun.javafx.beans.IDProperty;
082import com.sun.javafx.fxml.BeanAdapter;
083import com.sun.javafx.fxml.LoadListener;
084import com.sun.javafx.fxml.ObservableListChangeEvent;
085import com.sun.javafx.fxml.ObservableMapChangeEvent;
086import com.sun.javafx.fxml.PropertyChangeEvent;
087import com.sun.javafx.fxml.PropertyNotFoundException;
088import com.sun.javafx.fxml.expression.Expression;
089import com.sun.javafx.fxml.expression.ExpressionValue;
090import com.sun.javafx.fxml.expression.KeyPath;
091import java.net.MalformedURLException;
092import java.security.AccessController;
093import java.security.PrivilegedAction;
094import java.util.Locale;
095import java.util.StringTokenizer;
096import sun.reflect.misc.ConstructorUtil;
097import sun.reflect.misc.FieldUtil;
098import sun.reflect.misc.MethodUtil;
099import sun.reflect.misc.ReflectUtil;
100
101/**
102 * Loads an object hierarchy from an XML document.
103 */
104public class FXMLLoader {
105    // Abstract base class for elements
106    private abstract class Element {
107        public final Element parent;
108        public final int lineNumber;
109
110        public Object value = null;
111        private BeanAdapter valueAdapter = null;
112
113        public final LinkedList<Attribute> eventHandlerAttributes = new LinkedList<Attribute>();
114        public final LinkedList<Attribute> instancePropertyAttributes = new LinkedList<Attribute>();
115        public final LinkedList<Attribute> staticPropertyAttributes = new LinkedList<Attribute>();
116        public final LinkedList<PropertyElement> staticPropertyElements = new LinkedList<PropertyElement>();
117
118        public Element() {
119            parent = current;
120            lineNumber = getLineNumber();
121        }
122
123        public boolean isCollection() {
124            // Return true if value is a list, or if the value's type defines
125            // a default property that is a list
126            boolean collection;
127            if (value instanceof List<?>) {
128                collection = true;
129            } else {
130                Class<?> type = value.getClass();
131                DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
132
133                if (defaultProperty != null) {
134                    collection = getProperties().get(defaultProperty.value()) instanceof List<?>;
135                } else {
136                    collection = false;
137                }
138            }
139
140            return collection;
141        }
142
143        @SuppressWarnings("unchecked")
144        public void add(Object element) throws LoadException {
145            // If value is a list, add element to it; otherwise, get the value
146            // of the default property, which is assumed to be a list and add
147            // to that (coerce to the appropriate type)
148            List<Object> list;
149            if (value instanceof List<?>) {
150                list = (List<Object>)value;
151            } else {
152                Class<?> type = value.getClass();
153                DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
154                String defaultPropertyName = defaultProperty.value();
155
156                // Get the list value
157                list = (List<Object>)getProperties().get(defaultPropertyName);
158
159                // Coerce the element to the list item type
160                if (!Map.class.isAssignableFrom(type)) {
161                    Type listType = getValueAdapter().getGenericType(defaultPropertyName);
162                    element = BeanAdapter.coerce(element, BeanAdapter.getListItemType(listType));
163                }
164            }
165
166            list.add(element);
167        }
168
169        public void set(Object value) throws LoadException {
170            if (this.value == null) {
171                throw new LoadException("Cannot set value on this element.");
172            }
173
174            // Apply value to this element's properties
175            Class<?> type = this.value.getClass();
176            DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
177            if (defaultProperty == null) {
178                throw new LoadException("Element does not define a default property.");
179            }
180
181            getProperties().put(defaultProperty.value(), value);
182        }
183
184        public void updateValue(Object value) {
185            this.value = value;
186            valueAdapter = null;
187        }
188
189        public boolean isTyped() {
190            return !(value instanceof Map<?, ?>);
191        }
192
193        public BeanAdapter getValueAdapter() {
194            if (valueAdapter == null) {
195                valueAdapter = new BeanAdapter(value);
196            }
197
198            return valueAdapter;
199        }
200
201        @SuppressWarnings("unchecked")
202        public Map<String, Object> getProperties() {
203            return (isTyped()) ? getValueAdapter() : (Map<String, Object>)value;
204        }
205
206        public void processStartElement() throws IOException {
207            for (int i = 0, n = xmlStreamReader.getAttributeCount(); i < n; i++) {
208                String prefix = xmlStreamReader.getAttributePrefix(i);
209                String localName = xmlStreamReader.getAttributeLocalName(i);
210                String value = xmlStreamReader.getAttributeValue(i);
211
212                if (loadListener != null
213                    && prefix != null
214                    && prefix.equals(FX_NAMESPACE_PREFIX)) {
215                    loadListener.readInternalAttribute(prefix + ":" + localName, value);
216                }
217
218                processAttribute(prefix, localName, value);
219            }
220        }
221
222        public void processEndElement() throws IOException {
223            // No-op
224        }
225
226        public void processCharacters() throws IOException {
227            throw new LoadException("Unexpected characters in input stream.");
228        }
229
230        public void processInstancePropertyAttributes() throws IOException {
231            if (instancePropertyAttributes.size() > 0) {
232                for (Attribute attribute : instancePropertyAttributes) {
233                    processPropertyAttribute(attribute);
234                }
235            }
236        }
237
238        public void processAttribute(String prefix, String localName, String value)
239            throws IOException{
240            if (prefix == null) {
241                // Add the attribute to the appropriate list
242                if (localName.startsWith(EVENT_HANDLER_PREFIX)) {
243                    if (loadListener != null) {
244                        loadListener.readEventHandlerAttribute(localName, value);
245                    }
246
247                    eventHandlerAttributes.add(new Attribute(localName, null, value));
248                } else {
249                    int i = localName.lastIndexOf('.');
250
251                    if (i == -1) {
252                        // The attribute represents an instance property
253                        if (loadListener != null) {
254                            loadListener.readPropertyAttribute(localName, null, value);
255                        }
256
257                        instancePropertyAttributes.add(new Attribute(localName, null, value));
258                    } else {
259                        // The attribute represents a static property
260                        String name = localName.substring(i + 1);
261                        Class<?> sourceType = getType(localName.substring(0, i));
262
263                        if (sourceType != null) {
264                            if (loadListener != null) {
265                                loadListener.readPropertyAttribute(name, sourceType, value);
266                            }
267
268                            staticPropertyAttributes.add(new Attribute(name, sourceType, value));
269                        } else if (staticLoad) {
270                            if (loadListener != null) {
271                                loadListener.readUnknownStaticPropertyAttribute(localName, value);
272                            }
273                        } else {
274                            throw new LoadException(localName + " is not a valid attribute.");
275                        }
276                    }
277
278                }
279            } else {
280                throw new LoadException(prefix + ":" + localName
281                    + " is not a valid attribute.");
282            }
283        }
284
285        @SuppressWarnings("unchecked")
286        public void processPropertyAttribute(Attribute attribute) throws IOException {
287            String value = attribute.value;
288            if (isBindingExpression(value)) {
289                // Resolve the expression
290                Expression expression;
291
292                if (attribute.sourceType != null) {
293                    throw new LoadException("Cannot bind to static property.");
294                }
295
296                if (!isTyped()) {
297                    throw new LoadException("Cannot bind to untyped object.");
298                }
299
300                // TODO We may want to identify binding properties in processAttribute()
301                // and apply them after build() has been called
302                if (this.value instanceof Builder) {
303                    throw new LoadException("Cannot bind to builder property.");
304                }
305
306                value = value.substring(BINDING_EXPRESSION_PREFIX.length(),
307                        value.length() - 1);
308                expression = Expression.valueOf(value);
309
310                // Create the binding
311                BeanAdapter targetAdapter = new BeanAdapter(this.value);
312                ObservableValue<Object> propertyModel = targetAdapter.getPropertyModel(attribute.name);
313                Class<?> type = targetAdapter.getType(attribute.name);
314
315                if (propertyModel instanceof Property<?>) {
316                    ((Property<Object>)propertyModel).bind(new ExpressionValue(namespace, expression, type));
317                }
318            } else if (isBidirectionalBindingExpression(value)) {
319                throw new UnsupportedOperationException("This feature is not currently enabled.");
320            } else {
321                processValue(attribute.sourceType, attribute.name, value);
322            }
323        }
324
325        private boolean isBindingExpression(String aValue) {
326            return aValue.startsWith(BINDING_EXPRESSION_PREFIX)
327                   && aValue.endsWith(BINDING_EXPRESSION_SUFFIX);
328        }
329
330        private boolean isBidirectionalBindingExpression(String aValue) {
331            return aValue.startsWith(BI_DIRECTIONAL_BINDING_PREFIX);
332        }
333
334        private boolean processValue(Class sourceType, String propertyName, String aValue)
335            throws LoadException {
336
337            boolean processed = false;
338                //process list or array first
339                if (sourceType == null && isTyped()) {
340                    BeanAdapter valueAdapter = getValueAdapter();
341                    Class<?> type = valueAdapter.getType(propertyName);
342
343                    if (type == null) {
344                        throw new PropertyNotFoundException("Property \"" + propertyName
345                            + "\" does not exist" + " or is read-only.");
346                    }
347
348                    if (List.class.isAssignableFrom(type)
349                        && valueAdapter.isReadOnly(propertyName)) {
350                        populateListFromString(valueAdapter, propertyName, aValue);
351                        processed = true;
352                    } else if (type.isArray()) {
353                        applyProperty(propertyName, sourceType,
354                                populateArrayFromString(type, aValue));
355                        processed = true;
356                    }
357                }
358                if (!processed) {
359                    applyProperty(propertyName, sourceType, resolvePrefixedValue(aValue));
360                    processed = true;
361                }
362                return processed;
363        }
364
365        /**
366         * Resolves value prefixed with RELATIVE_PATH_PREFIX and RESOURCE_KEY_PREFIX.
367         */
368        private Object resolvePrefixedValue(String aValue) throws LoadException {
369            if (aValue.startsWith(ESCAPE_PREFIX)) {
370                aValue = aValue.substring(ESCAPE_PREFIX.length());
371
372                if (aValue.length() == 0
373                    || !(aValue.startsWith(ESCAPE_PREFIX)
374                        || aValue.startsWith(RELATIVE_PATH_PREFIX)
375                        || aValue.startsWith(RESOURCE_KEY_PREFIX)
376                        || aValue.startsWith(EXPRESSION_PREFIX)
377                        || aValue.startsWith(BI_DIRECTIONAL_BINDING_PREFIX))) {
378                    throw new LoadException("Invalid escape sequence.");
379                }
380                return aValue;
381            } else if (aValue.startsWith(RELATIVE_PATH_PREFIX)) {
382                aValue = aValue.substring(RELATIVE_PATH_PREFIX.length());
383                if (aValue.length() == 0) {
384                    throw new LoadException("Missing relative path.");
385                }
386                if (aValue.startsWith(RELATIVE_PATH_PREFIX)) {
387                    // The prefix was escaped
388                    warnDeprecatedEscapeSequence(RELATIVE_PATH_PREFIX);
389                    return aValue;
390                } else {
391                    try {
392                        return (aValue.charAt(0) == '/') ?
393                                classLoader.getResource(aValue.substring(1)).toString() :
394                                new URL(FXMLLoader.this.location, aValue).toString();
395                    } catch (MalformedURLException e) {
396                        System.err.println(FXMLLoader.this.location + "/" + aValue);
397                    }
398                }
399            } else if (aValue.startsWith(RESOURCE_KEY_PREFIX)) {
400                aValue = aValue.substring(RESOURCE_KEY_PREFIX.length());
401                if (aValue.length() == 0) {
402                    throw new LoadException("Missing resource key.");
403                }
404                if (aValue.startsWith(RESOURCE_KEY_PREFIX)) {
405                    // The prefix was escaped
406                    warnDeprecatedEscapeSequence(RESOURCE_KEY_PREFIX);
407                    return aValue;
408                } else {
409                    // Resolve the resource value
410                    if (resources == null) {
411                        throw new LoadException("No resources specified.");
412                    }
413                    if (!resources.containsKey(aValue)) {
414                        throw new LoadException("Resource \"" + aValue + "\" not found.");
415                    }
416
417                    return resources.getString(aValue);
418                }
419            } else if (aValue.startsWith(EXPRESSION_PREFIX)) {
420                aValue = aValue.substring(EXPRESSION_PREFIX.length());
421                if (aValue.length() == 0) {
422                    throw new LoadException("Missing expression.");
423                }
424                if (aValue.startsWith(EXPRESSION_PREFIX)) {
425                    // The prefix was escaped
426                    warnDeprecatedEscapeSequence(EXPRESSION_PREFIX);
427                    return aValue;
428                } else if (aValue.equals(NULL_KEYWORD)) {
429                    // The attribute value is null
430                    return null;
431                }
432                return Expression.get(namespace, KeyPath.parse(aValue));
433            }
434            return aValue;
435        }
436
437        /**
438         * Creates an array of given type and populates it with values from
439         * a string where tokens are separated by ARRAY_COMPONENT_DELIMITER.
440         * If token is prefixed with RELATIVE_PATH_PREFIX a value added to
441         * the array becomes relative to document location.
442         */
443        private Object populateArrayFromString(
444                Class<?>type,
445                String stringValue) throws LoadException {
446
447            Object propertyValue = null;
448            // Split the string and set the values as an array
449            Class<?> componentType = type.getComponentType();
450
451            if (stringValue.length() > 0) {
452                String[] values = stringValue.split(ARRAY_COMPONENT_DELIMITER);
453                propertyValue = Array.newInstance(componentType, values.length);
454                for (int i = 0; i < values.length; i++) {
455                    Array.set(propertyValue, i,
456                            BeanAdapter.coerce(resolvePrefixedValue(values[i].trim()),
457                            type.getComponentType()));
458                }
459            } else {
460                propertyValue = Array.newInstance(componentType, 0);
461            }
462            return propertyValue;
463        }
464
465        /**
466         * Populates list with values from a string where tokens are separated
467         * by ARRAY_COMPONENT_DELIMITER. If token is prefixed with RELATIVE_PATH_PREFIX
468         * a value added to the list becomes relative to document location.
469         */
470        private void populateListFromString(
471                BeanAdapter valueAdapter,
472                String listPropertyName,
473                String stringValue) throws LoadException {
474            // Split the string and add the values to the list
475            List<Object> list = (List<Object>)valueAdapter.get(listPropertyName);
476            Type listType = valueAdapter.getGenericType(listPropertyName);
477            Type itemType = (Class<?>)BeanAdapter.getGenericListItemType(listType);
478
479            if (itemType instanceof ParameterizedType) {
480                itemType = ((ParameterizedType)itemType).getRawType();
481            }
482
483            if (stringValue.length() > 0) {
484                String[] values = stringValue.split(ARRAY_COMPONENT_DELIMITER);
485
486                for (String aValue: values) {
487                    aValue = aValue.trim();
488                    list.add(
489                            BeanAdapter.coerce(resolvePrefixedValue(aValue),
490                                               (Class<?>)itemType));
491                }
492            }
493        }
494
495        public void warnDeprecatedEscapeSequence(String prefix) {
496            System.err.println(prefix + prefix + " is a deprecated escape sequence. "
497                + "Please use \\" + prefix + " instead.");
498        }
499
500        public void applyProperty(String name, Class<?> sourceType, Object value) {
501            if (sourceType == null) {
502                getProperties().put(name, value);
503            } else {
504                BeanAdapter.put(this.value, sourceType, name, value);
505            }
506        }
507
508        public void processEventHandlerAttributes() throws LoadException {
509            if (eventHandlerAttributes.size() > 0 && !staticLoad) {
510                for (Attribute attribute : eventHandlerAttributes) {
511                    EventHandler<? extends Event> eventHandler = null;
512
513                    String attrValue = attribute.value;
514
515                    if (attrValue.startsWith(CONTROLLER_METHOD_PREFIX)) {
516                        attrValue = attrValue.substring(CONTROLLER_METHOD_PREFIX.length());
517
518                        if (!attrValue.startsWith(CONTROLLER_METHOD_PREFIX)) {
519                            if (attrValue.length() == 0) {
520                                throw new LoadException("Missing controller method.");
521                            }
522
523                            if (controller == null) {
524                                throw new LoadException("No controller specified.");
525                            }
526
527                            Method method = getControllerMethods().get(attrValue);
528
529                            if (method == null) {
530                                throw new LoadException("Controller method \"" + attrValue + "\" not found.");
531                            }
532
533                            eventHandler = new ControllerMethodEventHandler(controller, method);
534                        }
535
536                    } else if (attrValue.startsWith(EXPRESSION_PREFIX)) {
537                        attrValue = attrValue.substring(EXPRESSION_PREFIX.length());
538
539                        if (attrValue.length() == 0) {
540                            throw new LoadException("Missing expression reference.");
541                        }
542
543                        Object expression = Expression.get(namespace, KeyPath.parse(attrValue));
544                        if (expression instanceof EventHandler) {
545                            eventHandler = (EventHandler<? extends Event>) expression;
546                        }
547
548                    }
549
550                    if (eventHandler == null) {
551                        if (attrValue.length() == 0 || scriptEngine == null) {
552                            throw new LoadException("Error resolving "  + attribute.name + "='" + attribute.value
553                                + "', either the event handler is not in the Namespace or there is an error in the script.");
554                        }
555
556                        eventHandler = new ScriptEventHandler(attrValue, scriptEngine);
557                    }
558
559                    // Add the handler
560                    if (eventHandler != null){
561                        addEventHandler(attribute, eventHandler);
562                    }
563                }
564            }
565        }
566
567        @SuppressWarnings("unchecked")
568        private void addEventHandler(Attribute attribute, EventHandler<? extends Event> eventHandler)
569            throws LoadException {
570            if (attribute.name.endsWith(CHANGE_EVENT_HANDLER_SUFFIX)) {
571                int i = EVENT_HANDLER_PREFIX.length();
572                int j = attribute.name.length() - CHANGE_EVENT_HANDLER_SUFFIX.length();
573
574                if (i == j) {
575                    if (value instanceof ObservableList<?>) {
576                        ObservableList<Object> list = (ObservableList<Object>)value;
577                        list.addListener(new ObservableListChangeAdapter(list,
578                            (EventHandler<ObservableListChangeEvent<?>>)eventHandler));
579                    } else if (value instanceof ObservableMap<?, ?>) {
580                        ObservableMap<Object, Object> map = (ObservableMap<Object, Object>)value;
581                        map.addListener(new ObservableMapChangeAdapter(map,
582                            (EventHandler<ObservableMapChangeEvent<?, ?>>)eventHandler));
583                    } else {
584                        throw new LoadException("Invalid event source.");
585                    }
586                } else {
587                    String key = Character.toLowerCase(attribute.name.charAt(i))
588                        + attribute.name.substring(i + 1, j);
589
590                    ObservableValue<Object> propertyModel = getValueAdapter().getPropertyModel(key);
591                    if (propertyModel == null) {
592                        throw new LoadException(value.getClass().getName() + " does not define"
593                                + " a property model for \"" + key + "\".");
594                    }
595
596                    propertyModel.addListener(new PropertyChangeAdapter(value,
597                        (EventHandler<PropertyChangeEvent<?>>)eventHandler));
598                }
599            } else {
600                getValueAdapter().put(attribute.name, eventHandler);
601            }
602        }
603    }
604
605    // Element representing a value
606    private abstract class ValueElement extends Element {
607        public String fx_id = null;
608
609        @Override
610        public void processStartElement() throws IOException {
611            super.processStartElement();
612
613            updateValue(constructValue());
614
615            if (value instanceof Builder<?>) {
616                processInstancePropertyAttributes();
617            } else {
618                processValue();
619            }
620        }
621
622        @Override
623        @SuppressWarnings("unchecked")
624        public void processEndElement() throws IOException {
625            super.processEndElement();
626
627            // Build the value, if necessary
628            if (value instanceof Builder<?>) {
629                Builder<Object> builder = (Builder<Object>)value;
630                updateValue(builder.build());
631
632                processValue();
633            } else {
634                processInstancePropertyAttributes();
635            }
636
637            processEventHandlerAttributes();
638
639            // Process static property attributes
640            if (staticPropertyAttributes.size() > 0) {
641                for (Attribute attribute : staticPropertyAttributes) {
642                    processPropertyAttribute(attribute);
643                }
644            }
645
646            // Process static property elements
647            if (staticPropertyElements.size() > 0) {
648                for (PropertyElement element : staticPropertyElements) {
649                    BeanAdapter.put(value, element.sourceType, element.name, element.value);
650                }
651            }
652
653            if (parent != null) {
654                if (parent.isCollection()) {
655                    parent.add(value);
656                } else {
657                    parent.set(value);
658                }
659            }
660        }
661
662        private Object getListValue(Element parent, String listPropertyName, Object value) {
663            // If possible, coerce the value to the list item type
664            if (parent.isTyped()) {
665                Type listType = parent.getValueAdapter().getGenericType(listPropertyName);
666
667                if (listType != null) {
668                    Type itemType = BeanAdapter.getGenericListItemType(listType);
669
670                    if (itemType instanceof ParameterizedType) {
671                        itemType = ((ParameterizedType)itemType).getRawType();
672                    }
673
674                    value = BeanAdapter.coerce(value, (Class<?>)itemType);
675                }
676            }
677
678            return value;
679        }
680
681        private void processValue() throws LoadException {
682            // If this is the root element, update the value
683            if (parent == null) {
684                root = value;
685
686                // checking version of fx namespace - throw exception if not supported
687                String fxNSURI = xmlStreamReader.getNamespaceContext().getNamespaceURI("fx");
688                if (fxNSURI != null) {
689                    String fxVersion = fxNSURI.substring(fxNSURI.lastIndexOf("/") + 1);
690                    if (compareJFXVersions(FX_NAMESPACE_VERSION, fxVersion) < 0) {
691                        throw new LoadException("Loading FXML document of version " +
692                                fxVersion + " by JavaFX runtime supporting version " + FX_NAMESPACE_VERSION);
693                    }
694                }
695
696                // checking the version JavaFX API - print warning if not supported
697                String defaultNSURI = xmlStreamReader.getNamespaceContext().getNamespaceURI("");
698                if (defaultNSURI != null) {
699                    String nsVersion = defaultNSURI.substring(defaultNSURI.lastIndexOf("/") + 1);
700                    if (compareJFXVersions(JAVAFX_VERSION, nsVersion) < 0) {
701                        Logging.getJavaFXLogger().warning("Loading FXML document with JavaFX API of version " +
702                                nsVersion + " by JavaFX runtime of version " + JAVAFX_VERSION);
703                    }
704                }
705            }
706
707            // Add the value to the namespace
708            if (fx_id != null) {
709                namespace.put(fx_id, value);
710
711                // If the value defines an ID property, set it
712                IDProperty idProperty = value.getClass().getAnnotation(IDProperty.class);
713
714                if (idProperty != null) {
715                    Map<String, Object> properties = getProperties();
716                    // set fx:id property value to Node.id only if Node.id was not
717                    // already set when processing start element attributes
718                    if (properties.get(idProperty.value()) == null) {
719                        properties.put(idProperty.value(), fx_id);
720                    }
721                }
722
723                // Set the controller field value
724                if (controller != null) {
725                    Field field = getControllerFields().get(fx_id);
726
727                    if (field != null) {
728                        try {
729                            field.set(controller, value);
730                        } catch (IllegalAccessException exception) {
731                            throw new RuntimeException(exception);
732                        }
733                    }
734                }
735            }
736        }
737
738        @Override
739        @SuppressWarnings("unchecked")
740        public void processCharacters() throws LoadException {
741            Class<?> type = value.getClass();
742            DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
743
744            // If the default property is a read-only list, add the value to it;
745            // otherwise, set the value as the default property
746            if (defaultProperty != null) {
747                String text = xmlStreamReader.getText();
748                text = extraneousWhitespacePattern.matcher(text).replaceAll(" ");
749
750                String defaultPropertyName = defaultProperty.value();
751                BeanAdapter valueAdapter = getValueAdapter();
752
753                if (valueAdapter.isReadOnly(defaultPropertyName)
754                    && List.class.isAssignableFrom(valueAdapter.getType(defaultPropertyName))) {
755                    List<Object> list = (List<Object>)valueAdapter.get(defaultPropertyName);
756                    list.add(getListValue(this, defaultPropertyName, text));
757                } else {
758                    valueAdapter.put(defaultPropertyName, text.trim());
759                }
760            } else {
761                throw new LoadException(type.getName() + " does not have a default property.");
762            }
763        }
764
765        @Override
766        public void processAttribute(String prefix, String localName, String value)
767            throws IOException{
768            if (prefix != null
769                && prefix.equals(FX_NAMESPACE_PREFIX)) {
770                if (localName.equals(FX_ID_ATTRIBUTE)) {
771                    // Verify that ID is a valid identifier
772                    if (value.equals(NULL_KEYWORD)) {
773                        throw new LoadException("Invalid identifier.");
774                    }
775
776                    for (int i = 0, n = value.length(); i < n; i++) {
777                        if (!Character.isJavaIdentifierPart(value.charAt(i))) {
778                            throw new LoadException("Invalid identifier.");
779                        }
780                    }
781
782                    fx_id = value;
783
784                } else if (localName.equals(FX_CONTROLLER_ATTRIBUTE)) {
785                    if (current.parent != null) {
786                        throw new LoadException(FX_NAMESPACE_PREFIX + ":" + FX_CONTROLLER_ATTRIBUTE
787                            + " can only be applied to root element.");
788                    }
789
790                    if (controller != null) {
791                        throw new LoadException("Controller value already specified.");
792                    }
793
794                    if (!staticLoad) {
795                        Class<?> type;
796                        try {
797                            type = classLoader.loadClass(value);
798                        } catch (ClassNotFoundException exception) {
799                            throw new LoadException(exception);
800                        }
801
802                        try {
803                            if (controllerFactory == null) {
804                                setController(ReflectUtil.newInstance(type));
805                            } else {
806                                setController(controllerFactory.call(type));
807                            }
808                        } catch (InstantiationException exception) {
809                            throw new LoadException(exception);
810                        } catch (IllegalAccessException exception) {
811                            throw new LoadException(exception);
812                        }
813                    }
814                } else {
815                    throw new LoadException("Invalid attribute.");
816                }
817            } else {
818                super.processAttribute(prefix, localName, value);
819            }
820        }
821
822        public abstract Object constructValue() throws IOException;
823    }
824
825    // Element representing a class instance
826    private class InstanceDeclarationElement extends ValueElement {
827        public Class<?> type;
828
829        public String constant = null;
830        public String factory = null;
831
832        public InstanceDeclarationElement(Class<?> type) throws LoadException {
833            this.type = type;
834        }
835
836        @Override
837        public void processAttribute(String prefix, String localName, String value)
838            throws IOException {
839            if (prefix != null
840                && prefix.equals(FX_NAMESPACE_PREFIX)) {
841                if (localName.equals(FX_VALUE_ATTRIBUTE)) {
842                    this.value = value;
843                } else if (localName.equals(FX_CONSTANT_ATTRIBUTE)) {
844                    constant = value;
845                } else if (localName.equals(FX_FACTORY_ATTRIBUTE)) {
846                    factory = value;
847                } else {
848                    super.processAttribute(prefix, localName, value);
849                }
850            } else {
851                super.processAttribute(prefix, localName, value);
852            }
853        }
854
855        @Override
856        public Object constructValue() throws IOException {
857            Object value;
858            if (this.value != null) {
859                value = BeanAdapter.coerce(this.value, type);
860            } else if (constant != null) {
861                value = BeanAdapter.getConstantValue(type, constant);
862            } else if (factory != null) {
863                Method factoryMethod;
864                try {
865                    factoryMethod = MethodUtil.getMethod(type, factory, new Class[] {});
866                } catch (NoSuchMethodException exception) {
867                    throw new LoadException(exception);
868                }
869
870                try {
871                    value = MethodUtil.invoke(factoryMethod, null, new Object [] {});
872                } catch (IllegalAccessException exception) {
873                    throw new LoadException(exception);
874                } catch (InvocationTargetException exception) {
875                    throw new LoadException(exception);
876                }
877            } else {
878                value = (builderFactory == null) ? null : builderFactory.getBuilder(type);
879
880                if (value == null) {
881                    try {
882                        value = ReflectUtil.newInstance(type);
883                    } catch (InstantiationException exception) {
884                        throw new LoadException(exception);
885                    } catch (IllegalAccessException exception) {
886                        throw new LoadException(exception);
887                    }
888                }
889            }
890
891            return value;
892        }
893    }
894
895    // Element representing an unknown type
896    private class UnknownTypeElement extends ValueElement {
897        // Map type representing an unknown value
898        @DefaultProperty("items")
899        public class UnknownValueMap extends AbstractMap<String, Object> {
900            private ArrayList<?> items = new ArrayList<Object>();
901            private HashMap<String, Object> values = new HashMap<String, Object>();
902
903            @Override
904            public Object get(Object key) {
905                if (key == null) {
906                    throw new NullPointerException();
907                }
908
909                return (key.equals(getClass().getAnnotation(DefaultProperty.class).value())) ?
910                    items : values.get(key);
911            }
912
913            @Override
914            public Object put(String key, Object value) {
915                if (key == null) {
916                    throw new NullPointerException();
917                }
918
919                if (key.equals(getClass().getAnnotation(DefaultProperty.class).value())) {
920                    throw new IllegalArgumentException();
921                }
922
923                return values.put(key, value);
924            }
925
926            @Override
927            public Set<Entry<String, Object>> entrySet() {
928                return Collections.emptySet();
929            }
930        }
931
932        @Override
933        public void processEndElement() throws IOException {
934            // No-op
935        }
936
937        @Override
938        public Object constructValue() throws LoadException {
939            return new UnknownValueMap();
940        }
941    }
942
943    // Element representing an include
944    private class IncludeElement extends ValueElement {
945        public String source = null;
946        public ResourceBundle resources = FXMLLoader.this.resources;
947        public Charset charset = FXMLLoader.this.charset;
948
949        @Override
950        public void processAttribute(String prefix, String localName, String value)
951            throws IOException {
952            if (prefix == null) {
953                if (localName.equals(INCLUDE_SOURCE_ATTRIBUTE)) {
954                    if (loadListener != null) {
955                        loadListener.readInternalAttribute(localName, value);
956                    }
957
958                    source = value;
959                } else if (localName.equals(INCLUDE_RESOURCES_ATTRIBUTE)) {
960                    if (loadListener != null) {
961                        loadListener.readInternalAttribute(localName, value);
962                    }
963
964                    resources = ResourceBundle.getBundle(value, Locale.getDefault(), 
965                            FXMLLoader.this.resources.getClass().getClassLoader());
966                } else if (localName.equals(INCLUDE_CHARSET_ATTRIBUTE)) {
967                    if (loadListener != null) {
968                        loadListener.readInternalAttribute(localName, value);
969                    }
970
971                    charset = Charset.forName(value);
972                } else {
973                    super.processAttribute(prefix, localName, value);
974                }
975            } else {
976                super.processAttribute(prefix, localName, value);
977            }
978        }
979
980        @Override
981        public Object constructValue() throws IOException {
982            if (source == null) {
983                throw new LoadException(INCLUDE_SOURCE_ATTRIBUTE + " is required.");
984            }
985
986            URL location;
987            if (source.charAt(0) == '/') {
988                location = classLoader.getResource(source.substring(1));
989            } else {
990                if (FXMLLoader.this.location == null) {
991                    throw new LoadException("Base location is undefined.");
992                }
993
994                location = new URL(FXMLLoader.this.location, source);
995            }
996
997            FXMLLoader fxmlLoader = new FXMLLoader(location, resources,
998                builderFactory, controllerFactory, charset,
999                loaders);
1000            fxmlLoader.parentLoader = FXMLLoader.this;
1001
1002            if (isCyclic(FXMLLoader.this, fxmlLoader)) {
1003                throw new IOException(
1004                        String.format(
1005                        "Including \"%s\" in \"%s\" created cyclic reference.",
1006                        fxmlLoader.location.toExternalForm(),
1007                        FXMLLoader.this.location.toExternalForm()));
1008            }
1009            fxmlLoader.setClassLoader(classLoader);
1010            fxmlLoader.setStaticLoad(staticLoad);
1011
1012            Object value = fxmlLoader.load();
1013
1014            if (fx_id != null) {
1015                String id = this.fx_id + CONTROLLER_SUFFIX;
1016                Object controller = fxmlLoader.getController();
1017
1018                namespace.put(id, controller);
1019
1020                if (FXMLLoader.this.controller != null) {
1021                    Field field = getControllerFields().get(id);
1022
1023                    if (field != null) {
1024                        try {
1025                            field.set(FXMLLoader.this.controller, controller);
1026                        } catch (IllegalAccessException exception) {
1027                            throw new LoadException(exception);
1028                        }
1029                    }
1030                }
1031            }
1032
1033            return value;
1034        }
1035    }
1036
1037    // Element representing a reference
1038    private class ReferenceElement extends ValueElement {
1039        public String source = null;
1040
1041        @Override
1042        public void processAttribute(String prefix, String localName, String value)
1043            throws IOException {
1044            if (prefix == null) {
1045                if (localName.equals(REFERENCE_SOURCE_ATTRIBUTE)) {
1046                    if (loadListener != null) {
1047                        loadListener.readInternalAttribute(localName, value);
1048                    }
1049
1050                    source = value;
1051                } else {
1052                    super.processAttribute(prefix, localName, value);
1053                }
1054            } else {
1055                super.processAttribute(prefix, localName, value);
1056            }
1057        }
1058
1059        @Override
1060        public Object constructValue() throws LoadException {
1061            if (source == null) {
1062                throw new LoadException(REFERENCE_SOURCE_ATTRIBUTE + " is required.");
1063            }
1064
1065            KeyPath path = KeyPath.parse(source);
1066            if (!Expression.isDefined(namespace, path)) {
1067                throw new LoadException("Value \"" + source + "\" does not exist.");
1068            }
1069
1070            return Expression.get(namespace, path);
1071        }
1072    }
1073
1074    // Element representing a copy
1075    private class CopyElement extends ValueElement {
1076        public String source = null;
1077
1078        @Override
1079        public void processAttribute(String prefix, String localName, String value)
1080            throws IOException {
1081            if (prefix == null) {
1082                if (localName.equals(COPY_SOURCE_ATTRIBUTE)) {
1083                    if (loadListener != null) {
1084                        loadListener.readInternalAttribute(localName, value);
1085                    }
1086
1087                    source = value;
1088                } else {
1089                    super.processAttribute(prefix, localName, value);
1090                }
1091            } else {
1092                super.processAttribute(prefix, localName, value);
1093            }
1094        }
1095
1096        @Override
1097        public Object constructValue() throws LoadException {
1098            if (source == null) {
1099                throw new LoadException(COPY_SOURCE_ATTRIBUTE + " is required.");
1100            }
1101
1102            KeyPath path = KeyPath.parse(source);
1103            if (!Expression.isDefined(namespace, path)) {
1104                throw new LoadException("Value \"" + source + "\" does not exist.");
1105            }
1106
1107            Object sourceValue = Expression.get(namespace, path);
1108            Class<?> sourceValueType = sourceValue.getClass();
1109
1110            Constructor<?> constructor = null;
1111            try {
1112                constructor = ConstructorUtil.getConstructor(sourceValueType, new Class[] { sourceValueType });
1113            } catch (NoSuchMethodException exception) {
1114                // No-op
1115            }
1116
1117            Object value;
1118            if (constructor != null) {
1119                try {
1120                    ReflectUtil.checkPackageAccess(sourceValueType);
1121                    value = constructor.newInstance(sourceValue);
1122                } catch (InstantiationException exception) {
1123                    throw new LoadException(exception);
1124                } catch (IllegalAccessException exception) {
1125                    throw new LoadException(exception);
1126                } catch (InvocationTargetException exception) {
1127                    throw new LoadException(exception);
1128                }
1129            } else {
1130                throw new LoadException("Can't copy value " + sourceValue + ".");
1131            }
1132
1133            return value;
1134        }
1135    }
1136
1137    // Element representing a predefined root value
1138    private class RootElement extends ValueElement {
1139        public String type = null;
1140
1141        @Override
1142        public void processAttribute(String prefix, String localName, String value)
1143            throws IOException {
1144            if (prefix == null) {
1145                if (localName.equals(ROOT_TYPE_ATTRIBUTE)) {
1146                    if (loadListener != null) {
1147                        loadListener.readInternalAttribute(localName, value);
1148                    }
1149
1150                    type = value;
1151                } else {
1152                    super.processAttribute(prefix, localName, value);
1153                }
1154            } else {
1155                super.processAttribute(prefix, localName, value);
1156            }
1157        }
1158
1159        @Override
1160        public Object constructValue() throws LoadException {
1161            if (type == null) {
1162                throw new LoadException(ROOT_TYPE_ATTRIBUTE + " is required.");
1163            }
1164
1165            Class<?> type = getType(this.type);
1166
1167            if (type == null) {
1168                throw new LoadException(this.type + " is not a valid type.");
1169            }
1170
1171            Object value;
1172            if (root == null) {
1173                throw new LoadException("Root hasn't been set. Use method setRoot() before load.");
1174            } else {
1175                if (!type.isAssignableFrom(root.getClass())) {
1176                    throw new LoadException("Root is not an instance of "
1177                        + type.getName() + ".");
1178                }
1179
1180                value = root;
1181            }
1182
1183            return value;
1184        }
1185    }
1186
1187    // Element representing a property
1188    private class PropertyElement extends Element {
1189        public final String name;
1190        public final Class<?> sourceType;
1191        public final boolean readOnly;
1192
1193        public PropertyElement(String name, Class<?> sourceType) throws LoadException {
1194            if (parent == null) {
1195                throw new LoadException("Invalid root element.");
1196            }
1197
1198            if (parent.value == null) {
1199                throw new LoadException("Parent element does not support property elements.");
1200            }
1201
1202            this.name = name;
1203            this.sourceType = sourceType;
1204
1205            if (sourceType == null) {
1206                // The element represents an instance property
1207                if (name.startsWith(EVENT_HANDLER_PREFIX)) {
1208                    throw new LoadException("\"" + name + "\" is not a valid element name.");
1209                }
1210
1211            Map<String, Object> parentProperties = parent.getProperties();
1212
1213            if (parent.isTyped()) {
1214                readOnly = parent.getValueAdapter().isReadOnly(name);
1215            } else {
1216                // If the map already defines a value for the property, assume
1217                // that it is read-only
1218                readOnly = parentProperties.containsKey(name);
1219            }
1220
1221            if (readOnly) {
1222                Object value = parentProperties.get(name);
1223                if (value == null) {
1224                    throw new LoadException("Invalid property.");
1225                }
1226
1227                updateValue(value);
1228            }
1229            } else {
1230                // The element represents a static property
1231                readOnly = false;
1232            }
1233        }
1234
1235        @Override
1236        public boolean isCollection() {
1237            return (readOnly) ? super.isCollection() : false;
1238        }
1239
1240        @Override
1241        public void add(Object element) throws LoadException {
1242            // Coerce the element to the list item type
1243            if (parent.isTyped()) {
1244                Type listType = parent.getValueAdapter().getGenericType(name);
1245                element = BeanAdapter.coerce(element, BeanAdapter.getListItemType(listType));
1246            }
1247
1248            // Add the item to the list
1249            super.add(element);
1250        }
1251
1252        @Override
1253        public void set(Object value) throws LoadException {
1254            // Update the value
1255            updateValue(value);
1256
1257            if (sourceType == null) {
1258                // Apply value to parent element's properties
1259                parent.getProperties().put(name, value);
1260            } else {
1261                if (parent.value instanceof Builder) {
1262                    // Defer evaluation of the property
1263                    parent.staticPropertyElements.add(this);
1264                } else {
1265                    // Apply the static property value
1266                    BeanAdapter.put(parent.value, sourceType, name, value);
1267                }
1268            }
1269        }
1270
1271        @Override
1272        public void processAttribute(String prefix, String localName, String value)
1273            throws IOException {
1274            if (!readOnly) {
1275                throw new LoadException("Attributes are not supported for writable property elements.");
1276            }
1277
1278            super.processAttribute(prefix, localName, value);
1279        }
1280
1281        @Override
1282        public void processEndElement() throws IOException {
1283            super.processEndElement();
1284
1285            if (readOnly) {
1286                processInstancePropertyAttributes();
1287                processEventHandlerAttributes();
1288            }
1289        }
1290
1291        @Override
1292        public void processCharacters() throws IOException {
1293            if (!readOnly) {
1294                String text = xmlStreamReader.getText();
1295                text = extraneousWhitespacePattern.matcher(text).replaceAll(" ");
1296
1297                set(text.trim());
1298            } else {
1299                super.processCharacters();
1300            }
1301        }
1302    }
1303
1304    // Element representing an unknown static property
1305    private class UnknownStaticPropertyElement extends Element {
1306        public UnknownStaticPropertyElement() throws LoadException {
1307            if (parent == null) {
1308                throw new LoadException("Invalid root element.");
1309            }
1310
1311            if (parent.value == null) {
1312                throw new LoadException("Parent element does not support property elements.");
1313            }
1314        }
1315
1316        @Override
1317        public boolean isCollection() {
1318            return false;
1319        }
1320
1321        @Override
1322        public void set(Object value) {
1323            updateValue(value);
1324        }
1325
1326        @Override
1327        public void processCharacters() throws IOException {
1328            String text = xmlStreamReader.getText();
1329            text = extraneousWhitespacePattern.matcher(text).replaceAll(" ");
1330
1331            updateValue(text.trim());
1332        }
1333    }
1334
1335    // Element representing a script block
1336    private class ScriptElement extends Element {
1337        public String source = null;
1338        public Charset charset = FXMLLoader.this.charset;
1339
1340        @Override
1341        public boolean isCollection() {
1342            return false;
1343        }
1344
1345        @Override
1346        public void processStartElement() throws IOException {
1347            super.processStartElement();
1348
1349            if (source != null && !staticLoad) {
1350                int i = source.lastIndexOf(".");
1351                if (i == -1) {
1352                    throw new LoadException("Cannot determine type of script \""
1353                        + source + "\".");
1354                }
1355
1356                String extension = source.substring(i + 1);
1357                ScriptEngine scriptEngine;
1358                ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
1359                try {
1360                    Thread.currentThread().setContextClassLoader(classLoader);
1361                    ScriptEngineManager scriptEngineManager = getScriptEngineManager();
1362                    scriptEngine = scriptEngineManager.getEngineByExtension(extension);
1363                } finally {
1364                    Thread.currentThread().setContextClassLoader(oldLoader);
1365                }
1366
1367                if (scriptEngine == null) {
1368                    throw new LoadException("Unable to locate scripting engine for"
1369                        + " extension " + extension + ".");
1370                }
1371
1372                scriptEngine.setBindings(scriptEngineManager.getBindings(), ScriptContext.ENGINE_SCOPE);
1373
1374                try {
1375                    URL location;
1376                    if (source.charAt(0) == '/') {
1377                        location = classLoader.getResource(source.substring(1));
1378                    } else {
1379                        if (FXMLLoader.this.location == null) {
1380                            throw new LoadException("Base location is undefined.");
1381                        }
1382
1383                        location = new URL(FXMLLoader.this.location, source);
1384                    }
1385
1386                    InputStreamReader scriptReader = null;
1387                    try {
1388                        scriptReader = new InputStreamReader(location.openStream(), charset);
1389                        scriptEngine.eval(scriptReader);
1390                    } catch(ScriptException exception) {
1391                        exception.printStackTrace();
1392                    } finally {
1393                        if (scriptReader != null) {
1394                            scriptReader.close();
1395                        }
1396                    }
1397                } catch (IOException exception) {
1398                    throw new LoadException(exception);
1399                }
1400            }
1401        }
1402
1403        @Override
1404        public void processEndElement() throws IOException {
1405            super.processEndElement();
1406
1407            if (value != null && !staticLoad) {
1408                // Evaluate the script
1409                try {
1410                    scriptEngine.eval((String)value);
1411                } catch (ScriptException exception) {
1412                    System.err.println(exception.getMessage());
1413                }
1414            }
1415        }
1416
1417        @Override
1418        public void processCharacters() throws LoadException {
1419            if (source != null) {
1420                throw new LoadException("Script source already specified.");
1421            }
1422
1423            if (scriptEngine == null && !staticLoad) {
1424                throw new LoadException("Page language not specified.");
1425            }
1426
1427            updateValue(xmlStreamReader.getText());
1428        }
1429
1430        @Override
1431        public void processAttribute(String prefix, String localName, String value)
1432            throws IOException {
1433            if (prefix == null
1434                && localName.equals(SCRIPT_SOURCE_ATTRIBUTE)) {
1435                if (loadListener != null) {
1436                    loadListener.readInternalAttribute(localName, value);
1437                }
1438
1439                source = value;
1440            } else if (localName.equals(SCRIPT_CHARSET_ATTRIBUTE)) {
1441                if (loadListener != null) {
1442                    loadListener.readInternalAttribute(localName, value);
1443                }
1444
1445                charset = Charset.forName(value);
1446            } else {
1447                throw new LoadException(prefix == null ? localName : prefix + ":" + localName
1448                    + " is not a valid attribute.");
1449            }
1450        }
1451    }
1452
1453    // Element representing a define block
1454    private class DefineElement extends Element {
1455        @Override
1456        public boolean isCollection() {
1457            return true;
1458        }
1459
1460        @Override
1461        public void add(Object element) {
1462            // No-op
1463        }
1464
1465        @Override
1466        public void processAttribute(String prefix, String localName, String value)
1467            throws LoadException{
1468            throw new LoadException("Element does not support attributes.");
1469        }
1470    }
1471
1472    // Class representing an attribute of an element
1473    private static class Attribute {
1474        public final String name;
1475        public final Class<?> sourceType;
1476        public final String value;
1477
1478        public Attribute(String name, Class<?> sourceType, String value) {
1479            this.name = name;
1480            this.sourceType = sourceType;
1481            this.value = value;
1482        }
1483    }
1484
1485    // Event handler that delegates to a method defined by the controller object
1486    private static class ControllerMethodEventHandler implements EventHandler<Event> {
1487        public final Object controller;
1488        public final Method method;
1489        public final boolean typed;
1490
1491        public ControllerMethodEventHandler(Object controller, Method method) {
1492            this.controller = controller;
1493            this.method = method;
1494            this.typed = (method.getParameterTypes().length == 1);
1495        }
1496
1497        @Override
1498        public void handle(Event event) {
1499            try {
1500                if (typed) {
1501                    MethodUtil.invoke(method, controller, new Object[] { event });
1502                } else {
1503                    MethodUtil.invoke(method, controller, new Object[] {});
1504                }
1505            } catch (InvocationTargetException exception) {
1506                throw new RuntimeException(exception);
1507            } catch (IllegalAccessException exception) {
1508                throw new RuntimeException(exception);
1509            }
1510        }
1511    }
1512
1513    // Event handler implemented in script code
1514    private static class ScriptEventHandler implements EventHandler<Event> {
1515        public final String script;
1516        public final ScriptEngine scriptEngine;
1517
1518        public ScriptEventHandler(String script, ScriptEngine scriptEngine) {
1519            this.script = script;
1520            this.scriptEngine = scriptEngine;
1521        }
1522
1523        @Override
1524        public void handle(Event event) {
1525            // Don't pollute the page namespace with values defined in the script
1526            Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
1527            Bindings localBindings = scriptEngine.createBindings();
1528            localBindings.put(EVENT_KEY, event);
1529            scriptEngine.setBindings(localBindings, ScriptContext.ENGINE_SCOPE);
1530
1531            // Execute the script
1532            try {
1533                scriptEngine.eval(script);
1534            } catch (ScriptException exception){
1535                throw new RuntimeException(exception);
1536            }
1537
1538            // Restore the original bindings
1539            scriptEngine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);
1540        }
1541    }
1542
1543    // Observable list change listener
1544    private static class ObservableListChangeAdapter implements ListChangeListener<Object> {
1545        public final ObservableList<Object> source;
1546        public final EventHandler<ObservableListChangeEvent<?>> handler;
1547
1548        public ObservableListChangeAdapter(ObservableList<Object> source,
1549            EventHandler<ObservableListChangeEvent<?>> handler) {
1550            this.source = source;
1551            this.handler = handler;
1552        }
1553
1554        @Override
1555        @SuppressWarnings("unchecked")
1556        public void onChanged(Change<? extends Object> change) {
1557            while (change.next()) {
1558                EventType<ObservableListChangeEvent<?>> eventType;
1559                List<Object> removed = (List<Object>)change.getRemoved();
1560
1561                if (change.wasPermutated()) {
1562                    eventType = ObservableListChangeEvent.UPDATE;
1563                    removed = null;
1564                } else if (change.wasAdded() && change.wasRemoved()) {
1565                    eventType = ObservableListChangeEvent.UPDATE;
1566                } else if (change.wasAdded()) {
1567                    eventType = ObservableListChangeEvent.ADD;
1568                } else if (change.wasRemoved()) {
1569                    eventType = ObservableListChangeEvent.REMOVE;
1570                } else {
1571                    throw new UnsupportedOperationException();
1572                }
1573
1574                handler.handle(new ObservableListChangeEvent<Object>(source,
1575                    eventType, change.getFrom(), change.getTo(),
1576                    removed));
1577            }
1578        }
1579    }
1580
1581    // Observable map change listener
1582    private static class ObservableMapChangeAdapter implements MapChangeListener<Object, Object> {
1583        public final ObservableMap<Object, Object> source;
1584        public final EventHandler<ObservableMapChangeEvent<?, ?>> handler;
1585
1586        public ObservableMapChangeAdapter(ObservableMap<Object, Object> source,
1587            EventHandler<ObservableMapChangeEvent<?, ?>> handler) {
1588            this.source = source;
1589            this.handler = handler;
1590        }
1591
1592        @Override
1593        public void onChanged(Change<? extends Object, ? extends Object> change) {
1594            EventType<ObservableMapChangeEvent<?, ?>> eventType;
1595            if (change.wasAdded() && change.wasRemoved()) {
1596                eventType = ObservableMapChangeEvent.UPDATE;
1597            } else if (change.wasAdded()) {
1598                eventType = ObservableMapChangeEvent.ADD;
1599            } else if (change.wasRemoved()) {
1600                eventType = ObservableMapChangeEvent.REMOVE;
1601            } else {
1602                throw new UnsupportedOperationException();
1603            }
1604
1605            handler.handle(new ObservableMapChangeEvent<Object, Object>(source,
1606                eventType, change.getKey(), change.getValueRemoved()));
1607        }
1608    }
1609
1610    // Property model change listener
1611    private static class PropertyChangeAdapter implements ChangeListener<Object> {
1612        public final Object source;
1613        public final EventHandler<PropertyChangeEvent<?>> handler;
1614
1615        public PropertyChangeAdapter(Object source, EventHandler<PropertyChangeEvent<?>> handler) {
1616            if (source == null) {
1617                throw new NullPointerException();
1618            }
1619
1620            if (handler == null) {
1621                throw new NullPointerException();
1622            }
1623
1624            this.source = source;
1625            this.handler = handler;
1626        }
1627
1628        @Override
1629        public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
1630            handler.handle(new PropertyChangeEvent<Object>(source, oldValue));
1631        }
1632    }
1633
1634    protected URL location;
1635    protected ResourceBundle resources;
1636
1637    private ObservableMap<String, Object> namespace = FXCollections.observableHashMap();
1638
1639    protected Object root = null;
1640    protected Object controller = null;
1641
1642    private BuilderFactory builderFactory;
1643    private Callback<Class<?>, Object> controllerFactory;
1644    private Charset charset;
1645
1646    private LinkedList<FXMLLoader> loaders;
1647
1648    private ClassLoader classLoader = defaultClassLoader;
1649    private boolean staticLoad = false;
1650    private LoadListener loadListener = null;
1651
1652    private FXMLLoader parentLoader;
1653
1654    private XMLStreamReader xmlStreamReader = null;
1655    private Element current = null;
1656
1657    private ScriptEngine scriptEngine = null;
1658
1659    private boolean template = false;
1660
1661    private LinkedList<String> packages = new LinkedList<String>();
1662    private HashMap<String, Class<?>> classes = new HashMap<String, Class<?>>();
1663
1664    private HashMap<String, Field> controllerFields = null;
1665    private HashMap<String, Method> controllerMethods = null;
1666
1667    private ScriptEngineManager scriptEngineManager = null;
1668
1669    private static ClassLoader defaultClassLoader;
1670
1671    private static final Pattern extraneousWhitespacePattern = Pattern.compile("\\s+");
1672
1673    public static final String DEFAULT_CHARSET_NAME = "UTF-8";
1674
1675    public static final String LANGUAGE_PROCESSING_INSTRUCTION = "language";
1676    public static final String IMPORT_PROCESSING_INSTRUCTION = "import";
1677
1678    public static final String FX_NAMESPACE_PREFIX = "fx";
1679    public static final String FX_CONTROLLER_ATTRIBUTE = "controller";
1680    public static final String FX_ID_ATTRIBUTE = "id";
1681    public static final String FX_VALUE_ATTRIBUTE = "value";
1682    public static final String FX_CONSTANT_ATTRIBUTE = "constant";
1683    public static final String FX_FACTORY_ATTRIBUTE = "factory";
1684
1685    public static final String INCLUDE_TAG = "include";
1686    public static final String INCLUDE_SOURCE_ATTRIBUTE = "source";
1687    public static final String INCLUDE_RESOURCES_ATTRIBUTE = "resources";
1688    public static final String INCLUDE_CHARSET_ATTRIBUTE = "charset";
1689
1690    public static final String SCRIPT_TAG = "script";
1691    public static final String SCRIPT_SOURCE_ATTRIBUTE = "source";
1692    public static final String SCRIPT_CHARSET_ATTRIBUTE = "charset";
1693
1694    public static final String DEFINE_TAG = "define";
1695
1696    public static final String REFERENCE_TAG = "reference";
1697    public static final String REFERENCE_SOURCE_ATTRIBUTE = "source";
1698
1699    public static final String ROOT_TAG = "root";
1700    public static final String ROOT_TYPE_ATTRIBUTE = "type";
1701
1702    public static final String COPY_TAG = "copy";
1703    public static final String COPY_SOURCE_ATTRIBUTE = "source";
1704
1705    public static final String EVENT_HANDLER_PREFIX = "on";
1706    public static final String EVENT_KEY = "event";
1707    public static final String CHANGE_EVENT_HANDLER_SUFFIX = "Change";
1708
1709    public static final String NULL_KEYWORD = "null";
1710
1711    public static final String ESCAPE_PREFIX = "\\";
1712    public static final String RELATIVE_PATH_PREFIX = "@";
1713    public static final String RESOURCE_KEY_PREFIX = "%";
1714    public static final String EXPRESSION_PREFIX = "$";
1715    public static final String BINDING_EXPRESSION_PREFIX = "${";
1716    public static final String BINDING_EXPRESSION_SUFFIX = "}";
1717
1718    public static final String BI_DIRECTIONAL_BINDING_PREFIX = "#{";
1719    public static final String BI_DIRECTIONAL_BINDING_SUFFIX = "}";
1720
1721    public static final String ARRAY_COMPONENT_DELIMITER = ",";
1722
1723    public static final String LOCATION_KEY = "location";
1724    public static final String RESOURCES_KEY = "resources";
1725
1726    public static final String CONTROLLER_METHOD_PREFIX = "#";
1727    public static final String CONTROLLER_KEYWORD = "controller";
1728    public static final String CONTROLLER_SUFFIX = "Controller";
1729
1730    public static final String INITIALIZE_METHOD_NAME = "initialize";
1731
1732    public static final String JAVAFX_VERSION;
1733
1734    public static final String FX_NAMESPACE_VERSION = "1";
1735
1736    static {
1737        defaultClassLoader = Thread.currentThread().getContextClassLoader();
1738
1739        if (defaultClassLoader == null) {
1740            throw new NullPointerException();
1741        }
1742
1743        JAVAFX_VERSION = AccessController.doPrivileged(new PrivilegedAction<String>() {
1744            @Override
1745            public String run() {
1746                return System.getProperty("javafx.version");
1747            }
1748        });
1749    }
1750
1751    /**
1752     * Creates a new FXMLLoader instance.
1753     */
1754    public FXMLLoader() {
1755        this((URL)null);
1756    }
1757
1758    /**
1759     * Creates a new FXMLLoader instance.
1760     *
1761     * @param location
1762     */
1763    public FXMLLoader(URL location) {
1764        this(location, null);
1765    }
1766
1767    /**
1768     * Creates a new FXMLLoader instance.
1769     *
1770     * @param location
1771     * @param resources
1772     */
1773    public FXMLLoader(URL location, ResourceBundle resources) {
1774        this(location, resources, new JavaFXBuilderFactory());
1775    }
1776
1777    /**
1778     * Creates a new FXMLLoader instance.
1779     *
1780     * @param location
1781     * @param resources
1782     * @param builderFactory
1783     */
1784    public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory) {
1785        this(location, resources, builderFactory, null);
1786    }
1787
1788    /**
1789     * Creates a new FXMLLoader instance.
1790     *
1791     * @param location
1792     * @param resources
1793     * @param builderFactory
1794     * @param controllerFactory
1795     */
1796    public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
1797        Callback<Class<?>, Object> controllerFactory) {
1798        this(location, resources, builderFactory, controllerFactory, Charset.forName(DEFAULT_CHARSET_NAME));
1799    }
1800
1801    /**
1802     * Creates a new FXMLLoader instance.
1803     *
1804     * @param charset
1805     */
1806    public FXMLLoader(Charset charset) {
1807        this(null, null, null, null, charset);
1808    }
1809
1810    /**
1811     * Creates a new FXMLLoader instance.
1812     *
1813     * @param location
1814     * @param resources
1815     * @param builderFactory
1816     * @param controllerFactory
1817     * @param charset
1818     */
1819    public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
1820        Callback<Class<?>, Object> controllerFactory, Charset charset) {
1821        this(location, resources, builderFactory, controllerFactory, charset,
1822            new LinkedList<FXMLLoader>());
1823    }
1824
1825    /**
1826     * Creates a new FXMLLoader instance.
1827     *
1828     * @param location
1829     * @param resources
1830     * @param builderFactory
1831     * @param controllerFactory
1832     * @param charset
1833     * @param loaders
1834     */
1835    public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
1836        Callback<Class<?>, Object> controllerFactory, Charset charset,
1837        LinkedList<FXMLLoader> loaders) {
1838        setLocation(location);
1839        setResources(resources);
1840        setBuilderFactory(builderFactory);
1841        setControllerFactory(controllerFactory);
1842        setCharset(charset);
1843
1844        this.loaders = loaders;
1845    }
1846
1847    /**
1848     * Returns the location used to resolve relative path attribute values.
1849     */
1850    public URL getLocation() {
1851        return location;
1852    }
1853
1854    /**
1855     * Sets the location used to resolve relative path attribute values.
1856     *
1857     * @param location
1858     */
1859    public void setLocation(URL location) {
1860        this.location = location;
1861    }
1862
1863    /**
1864     * Returns the resources used to resolve resource key attribute values.
1865     */
1866    public ResourceBundle getResources() {
1867        return resources;
1868    }
1869
1870    /**
1871     * Sets the resources used to resolve resource key attribute values.
1872     *
1873     * @param resources
1874     */
1875    public void setResources(ResourceBundle resources) {
1876        this.resources = resources;
1877    }
1878
1879    /**
1880     * Returns the namespace used by this loader.
1881     */
1882    public ObservableMap<String, Object> getNamespace() {
1883        return namespace;
1884    }
1885
1886    /**
1887     * Returns the root of the object hierarchy.
1888     */
1889    @SuppressWarnings("unchecked")
1890    public <T> T getRoot() {
1891        return (T)root;
1892    }
1893
1894    /**
1895     * Sets the root of the object hierarchy. The value passed to this method
1896     * is used as the value of the <tt>&lt;fx:root&gt;</tt> tag. This method
1897     * must be called prior to loading the document when using
1898     * <tt>&lt;fx:root&gt;</tt>.
1899     *
1900     * @param root
1901     * The root of the object hierarchy.
1902     */
1903    public void setRoot(Object root) {
1904        this.root = root;
1905    }
1906
1907    @Override
1908    public boolean equals(Object obj) {
1909        if (obj instanceof FXMLLoader) {
1910            FXMLLoader loader = (FXMLLoader)obj;
1911            return loader.location.toExternalForm().equals(
1912                    location.toExternalForm());
1913        }
1914        return false;
1915    }
1916
1917    private boolean isCyclic(
1918                            FXMLLoader currentLoader,
1919                            FXMLLoader node) {
1920        if (currentLoader == null) {
1921            return false;
1922        }
1923        if (currentLoader.equals(node)) {
1924            return true;
1925        }
1926        return isCyclic(currentLoader.parentLoader, node);
1927    }
1928
1929    /**
1930     * Returns the controller associated with the root object.
1931     */
1932    @SuppressWarnings("unchecked")
1933    public <T> T getController() {
1934        return (T)controller;
1935    }
1936
1937    /**
1938     * Sets the controller associated with the root object. The value passed to
1939     * this method is used as the value of the <tt>fx:controller</tt> attribute.
1940     * This method must be called prior to loading the document when using
1941     * controller event handlers when an <tt>fx:controller</tt> attribute is not
1942     * specified in the document.
1943     *
1944     * @param controller
1945     * The controller to associate with the root object.
1946     */
1947    public void setController(Object controller) {
1948        this.controller = controller;
1949
1950        if (controller == null) {
1951            namespace.remove(CONTROLLER_KEYWORD);
1952        } else {
1953            namespace.put(CONTROLLER_KEYWORD, controller);
1954        }
1955
1956        controllerFields = null;
1957        controllerMethods = null;
1958    }
1959
1960    /**
1961     * Returns the template flag.
1962     */
1963    public boolean isTemplate() {
1964        return template;
1965    }
1966
1967    /**
1968     * Sets the template flag. Setting this value to <tt>true</tt> can improve
1969     * performance when using a single loader instance to reload the same FXML
1970     * document multiple times. See the documentation for the {@link #load()}
1971     * method for more information.
1972     *
1973     * @param template
1974     * The template flag.
1975     */
1976    public void setTemplate(boolean template) {
1977        this.template = template;
1978    }
1979
1980    /**
1981     * Returns the builder factory used by this loader.
1982     */
1983    public BuilderFactory getBuilderFactory() {
1984        return builderFactory;
1985    }
1986
1987    /**
1988     * Sets the builder factory used by this loader.
1989     *
1990     * @param builderFactory
1991     */
1992    public void setBuilderFactory(BuilderFactory builderFactory) {
1993        this.builderFactory = builderFactory;
1994    }
1995
1996    /**
1997     * Returns the controller factory used by this serializer.
1998     */
1999    public Callback<Class<?>, Object> getControllerFactory() {
2000        return controllerFactory;
2001    }
2002
2003    /**
2004     * Sets the controller factory used by this serializer.
2005     *
2006     * @param controllerFactory
2007     */
2008    public void setControllerFactory(Callback<Class<?>, Object> controllerFactory) {
2009        this.controllerFactory = controllerFactory;
2010    }
2011
2012    /**
2013     * Returns the character set used by this loader.
2014     */
2015    public Charset getCharset() {
2016        return charset;
2017    }
2018
2019    /**
2020     * Sets the charset used by this loader.
2021     *
2022     * @param charset
2023     */
2024    public void setCharset(Charset charset) {
2025        if (charset == null) {
2026            throw new NullPointerException("charset is null.");
2027        }
2028
2029        this.charset = charset;
2030    }
2031
2032    /**
2033     * Returns the classloader used by this serializer.
2034     */
2035    public ClassLoader getClassLoader() {
2036        return classLoader;
2037    }
2038
2039    /**
2040     * Sets the classloader used by this serializer and clears any existing
2041     * imports (see {@link #setTemplate(boolean)}).
2042     *
2043     * @param classLoader
2044     */
2045    public void setClassLoader(ClassLoader classLoader) {
2046        if (classLoader == null) {
2047            throw new IllegalArgumentException();
2048        }
2049
2050        this.classLoader = classLoader;
2051
2052        clearImports();
2053    }
2054
2055    /**
2056     * Returns the static load flag.
2057     *
2058     * @treatAsPrivate
2059     * @deprecated
2060     */
2061    public boolean isStaticLoad() {
2062        // SB-dependency: RT-21226 has been filed to track this
2063        return staticLoad;
2064    }
2065
2066    /**
2067     * Sets the static load flag.
2068     *
2069     * @param staticLoad
2070     *
2071     * @treatAsPrivate
2072     * @deprecated
2073     */
2074    public void setStaticLoad(boolean staticLoad) {
2075        // SB-dependency: RT-21226 has been filed to track this
2076        this.staticLoad = staticLoad;
2077    }
2078
2079    /**
2080     * Returns this loader's load listener.
2081     *
2082     * @treatAsPrivate
2083     * @deprecated
2084     */
2085    public LoadListener getLoadListener() {
2086        // SB-dependency: RT-21228 has been filed to track this
2087        return loadListener;
2088    }
2089
2090    /**
2091     * Sets this loader's load listener.
2092     *
2093     * @param loadListener
2094     *
2095     * @treatAsPrivate
2096     * @deprecated
2097     */
2098    public void setLoadListener(LoadListener loadListener) {
2099        // SB-dependency: RT-21228 has been filed to track this
2100        this.loadListener = loadListener;
2101    }
2102
2103    /**
2104     * Loads an object hierarchy from a FXML document. The location from which
2105     * the document will be loaded must have been set by a prior call to
2106     * {@link #setLocation(URL)}.
2107     * <p>
2108     * When the "template" flag is set to <tt>false</tt> (the default), this
2109     * method will clear the imports before loading the document's content.
2110     * When "template" is <tt>true</tt>, the imports will not be cleared, and
2111     * the root value will be set to <tt>null</tt> before the content is
2112     * loaded. This helps improve performance on subsequent loads by
2113     * eliminating the overhead of loading the classes referred to by the
2114     * document.
2115     *
2116     * @return
2117     * The loaded object hierarchy.
2118     */
2119    public Object load() throws IOException {
2120        if (location == null) {
2121            throw new IllegalStateException("Location is not set.");
2122        }
2123
2124        InputStream inputStream = null;
2125        Object value;
2126        try {
2127            inputStream = location.openStream();
2128            value = load(inputStream);
2129        } catch (IOException exception) {
2130            logException(exception);
2131            throw exception;
2132        } catch (RuntimeException exception) {
2133            logException(exception);
2134            throw exception;
2135        } finally {
2136            if (inputStream != null) {
2137                inputStream.close();
2138            }
2139        }
2140
2141        return value;
2142    }
2143
2144    /**
2145     * Loads an object hierarchy from a FXML document.
2146     *
2147     * @param inputStream
2148     * An input stream containing the FXML data to load.
2149     *
2150     * @return
2151     * The loaded object hierarchy.
2152     */
2153    @SuppressWarnings("dep-ann")
2154    public Object load(InputStream inputStream) throws IOException {
2155        if (inputStream == null) {
2156            throw new NullPointerException("inputStream is null.");
2157        }
2158
2159        if (template) {
2160            setRoot(null);
2161        } else {
2162            clearImports();
2163        }
2164
2165        // Initialize the namespace
2166        namespace.put(LOCATION_KEY, location);
2167        namespace.put(RESOURCES_KEY, resources);
2168
2169        // Clear the script engine
2170        scriptEngine = null;
2171
2172        // Create the parser
2173        try {
2174            XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
2175            xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true);
2176
2177            // Some stream readers incorrectly report an empty string as the prefix
2178            // for the default namespace; correct this as needed
2179            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset);
2180            xmlStreamReader = new StreamReaderDelegate(xmlInputFactory.createXMLStreamReader(inputStreamReader)) {
2181                @Override
2182                public String getPrefix() {
2183                    String prefix = super.getPrefix();
2184
2185                    if (prefix != null
2186                        && prefix.length() == 0) {
2187                        prefix = null;
2188                    }
2189
2190                    return prefix;
2191                }
2192
2193                @Override
2194                public String getAttributePrefix(int index) {
2195                    String attributePrefix = super.getAttributePrefix(index);
2196
2197                    if (attributePrefix != null
2198                        && attributePrefix.length() == 0) {
2199                        attributePrefix = null;
2200                    }
2201
2202                    return attributePrefix;
2203                }
2204            };
2205        } catch (XMLStreamException exception) {
2206            throw new LoadException(exception);
2207        }
2208
2209        // Push this loader onto the stack
2210        loaders.push(this);
2211
2212        // Parse the XML stream
2213        try {
2214            while (xmlStreamReader.hasNext()) {
2215                int event = xmlStreamReader.next();
2216
2217                switch (event) {
2218                    case XMLStreamConstants.PROCESSING_INSTRUCTION: {
2219                        processProcessingInstruction();
2220                        break;
2221                    }
2222
2223                    case XMLStreamConstants.COMMENT: {
2224                        processComment();
2225                        break;
2226                    }
2227
2228                    case XMLStreamConstants.START_ELEMENT: {
2229                        processStartElement();
2230                        break;
2231                    }
2232
2233                    case XMLStreamConstants.END_ELEMENT: {
2234                        processEndElement();
2235                        break;
2236                    }
2237
2238                    case XMLStreamConstants.CHARACTERS: {
2239                        processCharacters();
2240                        break;
2241                    }
2242                }
2243            }
2244        } catch (XMLStreamException exception) {
2245            throw new LoadException(exception);
2246        }
2247
2248        if (controller != null) {
2249            if (controller instanceof Initializable) {
2250                ((Initializable)controller).initialize(location, resources);
2251            } else {
2252                // Inject controller fields
2253                HashMap<String, Field> controllerFields = getControllerFields();
2254
2255                Field locationField = controllerFields.get(LOCATION_KEY);
2256                if (locationField != null) {
2257                    try {
2258                        locationField.set(controller, location);
2259                    } catch (IllegalAccessException exception) {
2260                        // TODO Throw when Initializable is deprecated/removed
2261                        // throw new LoadException(exception);
2262                    }
2263                }
2264
2265                Field resourcesField = controllerFields.get(RESOURCES_KEY);
2266                if (resourcesField != null) {
2267                    try {
2268                        resourcesField.set(controller, resources);
2269                    } catch (IllegalAccessException exception) {
2270                        // TODO Throw when Initializable is deprecated/removed
2271                        // throw new LoadException(exception);
2272                    }
2273                }
2274
2275                // Initialize the controller
2276                Method initializeMethod = getControllerMethods().get(INITIALIZE_METHOD_NAME);
2277
2278                if (initializeMethod != null) {
2279                    try {
2280                        MethodUtil.invoke(initializeMethod, controller, new Object [] {});
2281                    } catch (IllegalAccessException exception) {
2282                        // TODO Throw when Initializable is deprecated/removed
2283                        // throw new LoadException(exception);
2284                    } catch (InvocationTargetException exception) {
2285                        throw new LoadException(exception);
2286                    }
2287                }
2288            }
2289        }
2290
2291        // Pop this loader off of the stack
2292        loaders.pop();
2293
2294        // Clear the parser
2295        xmlStreamReader = null;
2296
2297        return root;
2298    }
2299
2300    private void clearImports() {
2301        packages.clear();
2302        classes.clear();
2303    }
2304
2305    private void logException(Exception exception) {
2306        String message = exception.getMessage();
2307        if (message == null) {
2308            message = exception.getClass().getName();
2309        }
2310
2311        StringBuilder messageBuilder = new StringBuilder(message);
2312        messageBuilder.append("\n");
2313
2314        for (FXMLLoader loader : loaders) {
2315            messageBuilder.append(loader.location.getPath());
2316
2317            if (loader.current != null) {
2318                messageBuilder.append(":");
2319                messageBuilder.append(loader.current.lineNumber);
2320            }
2321
2322            messageBuilder.append("\n");
2323        }
2324
2325        StackTraceElement[] stackTrace = exception.getStackTrace();
2326        if (stackTrace != null) {
2327            for (int i = 0; i < stackTrace.length; i++) {
2328                messageBuilder.append("  at ");
2329                messageBuilder.append(stackTrace[i].toString());
2330                messageBuilder.append("\n");
2331            }
2332        }
2333
2334        System.err.println(messageBuilder.toString());
2335    }
2336
2337    /**
2338     * Returns the current line number.
2339     *
2340     * @treatAsPrivate
2341     * @deprecated
2342     */
2343    public int getLineNumber() {
2344        return xmlStreamReader.getLocation().getLineNumber();
2345    }
2346
2347    /**
2348     * Returns the current parse trace.
2349     *
2350     * @treatAsPrivate
2351     * @deprecated
2352     */
2353    public ParseTraceElement[] getParseTrace() {
2354        ParseTraceElement[] parseTrace = new ParseTraceElement[loaders.size()];
2355
2356        int i = 0;
2357        for (FXMLLoader loader : loaders) {
2358            parseTrace[i++] = new ParseTraceElement(loader.location, (loader.current != null) ?
2359                loader.current.lineNumber : -1);
2360        }
2361
2362        return parseTrace;
2363    }
2364
2365    private void processProcessingInstruction() throws LoadException {
2366        String piTarget = xmlStreamReader.getPITarget().trim();
2367
2368        if (piTarget.equals(LANGUAGE_PROCESSING_INSTRUCTION)) {
2369            processLanguage();
2370        } else if (piTarget.equals(IMPORT_PROCESSING_INSTRUCTION)) {
2371            processImport();
2372        }
2373    }
2374
2375    private void processLanguage() throws LoadException {
2376        if (scriptEngine != null) {
2377            throw new LoadException("Page language already set.");
2378        }
2379
2380        String language = xmlStreamReader.getPIData();
2381
2382        if (loadListener != null) {
2383            loadListener.readLanguageProcessingInstruction(language);
2384        }
2385
2386        if (!staticLoad) {
2387            ScriptEngineManager scriptEngineManager = getScriptEngineManager();
2388            scriptEngine = scriptEngineManager.getEngineByName(language);
2389            scriptEngine.setBindings(scriptEngineManager.getBindings(), ScriptContext.ENGINE_SCOPE);
2390        }
2391    }
2392
2393    private void processImport() throws LoadException {
2394        String target = xmlStreamReader.getPIData().trim();
2395
2396        if (loadListener != null) {
2397            loadListener.readImportProcessingInstruction(target);
2398        }
2399
2400        if (target.endsWith(".*")) {
2401            importPackage(target.substring(0, target.length() - 2));
2402        } else {
2403            importClass(target);
2404        }
2405    }
2406
2407    private void processComment() throws LoadException {
2408        if (loadListener != null) {
2409            loadListener.readComment(xmlStreamReader.getText());
2410        }
2411    }
2412
2413    private void processStartElement() throws IOException {
2414        // Create the element
2415        createElement();
2416
2417        // Process the start tag
2418        current.processStartElement();
2419
2420        // Set the root value
2421        if (root == null) {
2422            root = current.value;
2423        }
2424    }
2425
2426    private void createElement() throws IOException {
2427        String prefix = xmlStreamReader.getPrefix();
2428        String localName = xmlStreamReader.getLocalName();
2429
2430        if (prefix == null) {
2431            int i = localName.lastIndexOf('.');
2432
2433            if (Character.isLowerCase(localName.charAt(i + 1))) {
2434                String name = localName.substring(i + 1);
2435
2436                if (i == -1) {
2437                    // This is an instance property
2438                    if (loadListener != null) {
2439                        loadListener.beginPropertyElement(name, null);
2440                    }
2441
2442                    current = new PropertyElement(name, null);
2443                } else {
2444                    // This is a static property
2445                    Class<?> sourceType = getType(localName.substring(0, i));
2446
2447                    if (sourceType != null) {
2448                        if (loadListener != null) {
2449                            loadListener.beginPropertyElement(name, sourceType);
2450                        }
2451
2452                        current = new PropertyElement(name, sourceType);
2453                    } else if (staticLoad) {
2454                        // The source type was not recognized
2455                        if (loadListener != null) {
2456                            loadListener.beginUnknownStaticPropertyElement(localName);
2457                        }
2458
2459                        current = new UnknownStaticPropertyElement();
2460                    } else {
2461                        throw new LoadException(localName + " is not a valid property.");
2462                    }
2463                }
2464            } else {
2465                if (current == null && root != null) {
2466                    throw new LoadException("Root value already specified.");
2467                }
2468
2469                Class<?> type = getType(localName);
2470
2471                if (type != null) {
2472                    if (loadListener != null) {
2473                        loadListener.beginInstanceDeclarationElement(type);
2474                    }
2475
2476                    current = new InstanceDeclarationElement(type);
2477                } else if (staticLoad) {
2478                    // The type was not recognized
2479                    if (loadListener != null) {
2480                        loadListener.beginUnknownTypeElement(localName);
2481                    }
2482
2483                    current = new UnknownTypeElement();
2484                } else {
2485                    throw new LoadException(localName + " is not a valid type.");
2486                }
2487            }
2488        } else if (prefix.equals(FX_NAMESPACE_PREFIX)) {
2489            if (localName.equals(INCLUDE_TAG)) {
2490                if (loadListener != null) {
2491                    loadListener.beginIncludeElement();
2492                }
2493
2494                current = new IncludeElement();
2495            } else if (localName.equals(REFERENCE_TAG)) {
2496                if (loadListener != null) {
2497                    loadListener.beginReferenceElement();
2498                }
2499
2500                current = new ReferenceElement();
2501            } else if (localName.equals(COPY_TAG)) {
2502                if (loadListener != null) {
2503                    loadListener.beginCopyElement();
2504                }
2505
2506                current = new CopyElement();
2507            } else if (localName.equals(ROOT_TAG)) {
2508                if (loadListener != null) {
2509                    loadListener.beginRootElement();
2510                }
2511
2512                current = new RootElement();
2513            } else if (localName.equals(SCRIPT_TAG)) {
2514                if (loadListener != null) {
2515                    loadListener.beginScriptElement();
2516                }
2517
2518                current = new ScriptElement();
2519            } else if (localName.equals(DEFINE_TAG)) {
2520                if (loadListener != null) {
2521                    loadListener.beginDefineElement();
2522                }
2523
2524                current = new DefineElement();
2525            } else {
2526                throw new LoadException(prefix + ":" + localName + " is not a valid element.");
2527            }
2528        } else {
2529            throw new LoadException("Unexpected namespace prefix: " + prefix + ".");
2530        }
2531    }
2532
2533    private void processEndElement() throws IOException {
2534        current.processEndElement();
2535
2536        if (loadListener != null) {
2537            loadListener.endElement(current.value);
2538        }
2539
2540        // Move up the stack
2541        current = current.parent;
2542    }
2543
2544    private void processCharacters() throws IOException {
2545        // Process the characters
2546        if (!xmlStreamReader.isWhiteSpace()) {
2547            current.processCharacters();
2548        }
2549    }
2550
2551    private void importPackage(String name) throws LoadException {
2552        packages.add(name);
2553    }
2554
2555    private void importClass(String name) throws LoadException {
2556        try {
2557            loadType(name, true);
2558        } catch (ClassNotFoundException exception) {
2559            throw new LoadException(exception);
2560        }
2561    }
2562
2563    private Class<?> getType(String name) throws LoadException {
2564        Class<?> type = null;
2565
2566        if (Character.isLowerCase(name.charAt(0))) {
2567            // This is a fully-qualified class name
2568            try {
2569                type = loadType(name, false);
2570            } catch (ClassNotFoundException exception) {
2571                // No-op
2572            }
2573        } else {
2574            // This is an unqualified class name
2575            type = classes.get(name);
2576
2577            if (type == null) {
2578                // The class has not been loaded yet; look it up
2579                for (String packageName : packages) {
2580                    try {
2581                        type = loadTypeForPackage(packageName, name);
2582                    } catch (ClassNotFoundException exception) {
2583                        // No-op
2584                    }
2585
2586                    if (type != null) {
2587                        break;
2588                    }
2589                }
2590
2591                if (type != null) {
2592                    classes.put(name, type);
2593                }
2594            }
2595        }
2596
2597        return type;
2598    }
2599
2600    private Class<?> loadType(String name, boolean cache) throws ClassNotFoundException {
2601        int i = name.indexOf('.');
2602        int n = name.length();
2603        while (i != -1
2604            && i < n
2605            && Character.isLowerCase(name.charAt(i + 1))) {
2606            i = name.indexOf('.', i + 1);
2607        }
2608
2609        if (i == -1 || i == n) {
2610            throw new ClassNotFoundException();
2611        }
2612
2613        String packageName = name.substring(0, i);
2614        String className = name.substring(i + 1);
2615
2616        Class<?> type = loadTypeForPackage(packageName, className);
2617
2618        if (cache) {
2619            classes.put(className, type);
2620        }
2621
2622        return type;
2623    }
2624
2625    // TODO Rename to loadType() when deprecated static version is removed
2626    private Class<?> loadTypeForPackage(String packageName, String className) throws ClassNotFoundException {
2627        return classLoader.loadClass(packageName + "." + className.replace('.', '$'));
2628    }
2629
2630    private HashMap<String, Field> getControllerFields() throws LoadException {
2631        if (controllerFields == null) {
2632            controllerFields = new HashMap<String, Field>();
2633
2634            Class<?> controllerType = controller.getClass();
2635            Class<?> type = controllerType;
2636
2637            while (type != Object.class) {
2638                Field[] fields = FieldUtil.getDeclaredFields(type);
2639
2640                for (int i = 0; i < fields.length; i++) {
2641                    Field field = fields[i];
2642                    int modifiers = field.getModifiers();
2643
2644                    // Only add fields that are visible to this controller type
2645                    if (type == controllerType
2646                        || (modifiers & Modifier.PRIVATE) == 0) {
2647                        // Ensure that the field is accessible
2648                        if ((modifiers & Modifier.PUBLIC) == 0
2649                            && field.getAnnotation(FXML.class) != null) {
2650                            try {
2651                                field.setAccessible(true);
2652                            } catch (SecurityException exception) {
2653                                throw new LoadException(exception);
2654                            }
2655                        }
2656
2657                        controllerFields.put(field.getName(), field);
2658                    }
2659                }
2660
2661                type = type.getSuperclass();
2662            }
2663        }
2664
2665        return controllerFields;
2666    }
2667
2668    private HashMap<String, Method> getControllerMethods() throws LoadException {
2669        if (controllerMethods == null) {
2670            controllerMethods = new HashMap<String, Method>();
2671
2672            Class<?> controllerType = controller.getClass();
2673            Class<?> type = controllerType;
2674
2675            while (type != Object.class) {
2676                ReflectUtil.checkPackageAccess(type);
2677                Method[] methods = type.getDeclaredMethods();
2678
2679                for (int i = 0; i < methods.length; i++) {
2680                    Method method = methods[i];
2681                    int modifiers = method.getModifiers();
2682
2683                    // Only add methods that are visible to this controller type
2684                    if (type == controllerType
2685                        || (modifiers & Modifier.PRIVATE) == 0) {
2686                        // Ensure that the method is accessible
2687                        if ((modifiers & Modifier.PUBLIC) == 0
2688                            && method.getAnnotation(FXML.class) != null) {
2689                            try {
2690                                method.setAccessible(true);
2691                            } catch (SecurityException exception) {
2692                                throw new LoadException(exception);
2693                            }
2694                        }
2695
2696                        // Add this method to the map if:
2697                        // a) it is the initialize() method, or
2698                        // b) it takes a single event argument, or
2699                        // c) it takes no arguments and a handler with this
2700                        //    name has not already been defined
2701                        String methodName = method.getName();
2702                        Class<?>[] parameterTypes = method.getParameterTypes();
2703
2704                        if (methodName.equals(INITIALIZE_METHOD_NAME)) {
2705                            if (parameterTypes.length == 0) {
2706                                controllerMethods.put(method.getName(), method);
2707                            }
2708                        } else if ((parameterTypes.length == 1 && Event.class.isAssignableFrom(parameterTypes[0]))
2709                            || (parameterTypes.length == 0 && !controllerMethods.containsKey(methodName))) {
2710                            controllerMethods.put(method.getName(), method);
2711                        }
2712                    }
2713                }
2714
2715                type = type.getSuperclass();
2716            }
2717        }
2718
2719        return controllerMethods;
2720    }
2721
2722    private ScriptEngineManager getScriptEngineManager() {
2723        if (scriptEngineManager == null) {
2724            scriptEngineManager = new javax.script.ScriptEngineManager();
2725            scriptEngineManager.setBindings(new SimpleBindings(namespace));
2726        }
2727
2728        return scriptEngineManager;
2729    }
2730
2731    /**
2732     * Loads a type using the default class loader.
2733     *
2734     * @param packageName
2735     * @param className
2736     *
2737     * @deprecated
2738     * This method now delegates to {@link #getDefaultClassLoader()}.
2739     */
2740    public static Class<?> loadType(String packageName, String className) throws ClassNotFoundException {
2741        return loadType(packageName + "." + className.replace('.', '$'));
2742    }
2743
2744    /**
2745     * Loads a type using the default class loader.
2746     *
2747     * @param className
2748     *
2749     * @deprecated
2750     * This method now delegates to {@link #getDefaultClassLoader()}.
2751     */
2752    public static Class<?> loadType(String className) throws ClassNotFoundException {
2753        ReflectUtil.checkPackageAccess(className);
2754        return Class.forName(className, true, defaultClassLoader);
2755    }
2756
2757    /**
2758     * Returns the default class loader.
2759     */
2760    public static ClassLoader getDefaultClassLoader() {
2761        return defaultClassLoader;
2762    }
2763
2764    /**
2765     * Sets the default class loader.
2766     *
2767     * @param defaultClassLoader
2768     * The default class loader to use when loading classes.
2769     */
2770    public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {
2771        if (defaultClassLoader == null) {
2772            throw new NullPointerException();
2773        }
2774
2775        FXMLLoader.defaultClassLoader = defaultClassLoader;
2776    }
2777
2778    /**
2779     * Loads an object hierarchy from a FXML document.
2780     *
2781     * @param location
2782     */
2783    @SuppressWarnings("unchecked")
2784    public static <T> T load(URL location) throws IOException {
2785        return (T)load(location, null);
2786    }
2787
2788    /**
2789     * Loads an object hierarchy from a FXML document.
2790     *
2791     * @param location
2792     * @param resources
2793     */
2794    @SuppressWarnings("unchecked")
2795    public static <T> T load(URL location, ResourceBundle resources) throws IOException {
2796        return (T)load(location, resources, new JavaFXBuilderFactory());
2797    }
2798
2799    /**
2800     * Loads an object hierarchy from a FXML document.
2801     *
2802     * @param location
2803     * @param resources
2804     * @param builderFactory
2805     */
2806    @SuppressWarnings("unchecked")
2807    public static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory)
2808        throws IOException {
2809        return (T)load(location, resources, builderFactory, null);
2810    }
2811
2812    /**
2813     * Loads an object hierarchy from a FXML document.
2814     *
2815     * @param location
2816     * @param resources
2817     * @param builderFactory
2818     * @param controllerFactory
2819     */
2820    @SuppressWarnings("unchecked")
2821    public static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2822        Callback<Class<?>, Object> controllerFactory) throws IOException {
2823        return (T)load(location, resources, builderFactory, controllerFactory, Charset.forName(DEFAULT_CHARSET_NAME));
2824    }
2825
2826    /**
2827     * Loads an object hierarchy from a FXML document.
2828     *
2829     * @param location
2830     * @param resources
2831     * @param builderFactory
2832     * @param controllerFactory
2833     * @param charset
2834     */
2835    @SuppressWarnings("unchecked")
2836    public static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2837        Callback<Class<?>, Object> controllerFactory, Charset charset) throws IOException {
2838        if (location == null) {
2839            throw new NullPointerException("Location is required.");
2840        }
2841
2842        FXMLLoader fxmlLoader = new FXMLLoader(location, resources, builderFactory, controllerFactory, charset);
2843
2844        return (T)fxmlLoader.load();
2845    }
2846
2847    /**
2848     * Utility method for comparing two JavaFX version strings (such as 2.2.5, 8.0.0-ea)
2849     * @param rtVer String representation of JavaFX runtime version, including - or _ appendix
2850     * @param nsVer String representation of JavaFX version to compare against runtime version
2851     * @return number &lt; 0 if runtime version is lower, 0 when both versions are the same,
2852     *          number &gt; 0 if runtime is higher version
2853     */
2854    static int compareJFXVersions(String rtVer, String nsVer) {
2855
2856        int retVal = 0;
2857
2858        if (rtVer == null || "".equals(rtVer) ||
2859            nsVer == null || "".equals(nsVer)) {
2860            return retVal;
2861        }
2862
2863        if (rtVer.equals(nsVer)) {
2864            return retVal;
2865        }
2866
2867        // version string can contain '-'
2868        int dashIndex = rtVer.indexOf("-");
2869        if (dashIndex > 0) {
2870            rtVer = rtVer.substring(0, dashIndex);
2871        }
2872
2873        // or "_"
2874        int underIndex = rtVer.indexOf("_");
2875        if (underIndex > 0) {
2876            rtVer = rtVer.substring(0, underIndex);
2877        }
2878
2879        // do not try to compare if the string is not valid version format
2880        if (!Pattern.matches("^(\\d+)(\\.\\d+)*$", rtVer) ||
2881            !Pattern.matches("^(\\d+)(\\.\\d+)*$", nsVer)) {
2882            return retVal;
2883        }
2884
2885        StringTokenizer nsVerTokenizer = new StringTokenizer(nsVer, ".");
2886        StringTokenizer rtVerTokenizer = new StringTokenizer(rtVer, ".");
2887        int nsDigit = 0, rtDigit = 0;
2888        boolean rtVerEnd = false;
2889
2890        while (nsVerTokenizer.hasMoreTokens() && retVal == 0) {
2891            nsDigit = Integer.parseInt(nsVerTokenizer.nextToken());
2892            if (rtVerTokenizer.hasMoreTokens()) {
2893                rtDigit = Integer.parseInt(rtVerTokenizer.nextToken());
2894                retVal = rtDigit - nsDigit;
2895            } else {
2896                rtVerEnd = true;
2897                break;
2898            }
2899        }
2900
2901        if (rtVerTokenizer.hasMoreTokens() && retVal == 0) {
2902            rtDigit = Integer.parseInt(rtVerTokenizer.nextToken());
2903            if (rtDigit > 0) {
2904                retVal = 1;
2905            }
2906        }
2907
2908        if (rtVerEnd) {
2909            if (nsDigit > 0) {
2910                retVal = -1;
2911            } else {
2912                while (nsVerTokenizer.hasMoreTokens()) {
2913                    nsDigit = Integer.parseInt(nsVerTokenizer.nextToken());
2914                    if (nsDigit > 0) {
2915                        retVal = -1;
2916                        break;
2917                    }
2918                }
2919            }
2920        }
2921
2922        return retVal;
2923    }
2924
2925}