1 /*
   2  * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.fxml;
  27 
  28 import com.sun.javafx.util.Logging;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.InputStreamReader;
  32 import java.lang.reflect.Array;
  33 import java.lang.reflect.Constructor;
  34 import java.lang.reflect.Field;
  35 import java.lang.reflect.InvocationTargetException;
  36 import java.lang.reflect.Method;
  37 import java.lang.reflect.Modifier;
  38 import java.lang.reflect.ParameterizedType;
  39 import java.lang.reflect.Type;
  40 import java.net.URL;
  41 import java.nio.charset.Charset;
  42 import java.util.AbstractMap;
  43 import java.util.ArrayList;
  44 import java.util.Collections;
  45 import java.util.HashMap;
  46 import java.util.LinkedList;
  47 import java.util.List;
  48 import java.util.Map;
  49 import java.util.ResourceBundle;
  50 import java.util.Set;
  51 import java.util.regex.Pattern;
  52 
  53 import javafx.beans.DefaultProperty;
  54 import javafx.beans.InvalidationListener;
  55 import javafx.beans.property.Property;
  56 import javafx.beans.value.ChangeListener;
  57 import javafx.beans.value.ObservableValue;
  58 import javafx.collections.*;
  59 import javafx.event.Event;
  60 import javafx.event.EventHandler;
  61 import javafx.util.Builder;
  62 import javafx.util.BuilderFactory;
  63 import javafx.util.Callback;
  64 
  65 import javax.script.Bindings;
  66 import javax.script.ScriptContext;
  67 import javax.script.ScriptEngine;
  68 import javax.script.ScriptEngineManager;
  69 import javax.script.ScriptException;
  70 import javax.script.SimpleBindings;
  71 import javax.xml.stream.XMLInputFactory;
  72 import javax.xml.stream.XMLStreamConstants;
  73 import javax.xml.stream.XMLStreamException;
  74 import javax.xml.stream.XMLStreamReader;
  75 import javax.xml.stream.util.StreamReaderDelegate;
  76 
  77 import com.sun.javafx.beans.IDProperty;
  78 import com.sun.javafx.fxml.BeanAdapter;
  79 import com.sun.javafx.fxml.ParseTraceElement;
  80 import com.sun.javafx.fxml.PropertyNotFoundException;
  81 import com.sun.javafx.fxml.expression.Expression;
  82 import com.sun.javafx.fxml.expression.ExpressionValue;
  83 import com.sun.javafx.fxml.expression.KeyPath;
  84 import static com.sun.javafx.FXPermissions.MODIFY_FXML_CLASS_LOADER_PERMISSION;
  85 import java.net.MalformedURLException;
  86 import java.security.AccessController;
  87 import java.security.PrivilegedAction;
  88 import java.util.EnumMap;
  89 import java.util.Locale;
  90 import java.util.StringTokenizer;
  91 import sun.reflect.misc.ConstructorUtil;
  92 import sun.reflect.misc.MethodUtil;
  93 import sun.reflect.misc.ReflectUtil;
  94 
  95 /**
  96  * Loads an object hierarchy from an XML document.
  97  * @since JavaFX 2.0
  98  */
  99 public class FXMLLoader {
 100 
 101     // Indicates permission to get the ClassLoader
 102     private static final RuntimePermission GET_CLASSLOADER_PERMISSION =
 103         new RuntimePermission("getClassLoader");
 104 
 105     // Instance of StackWalker used to get caller class (must be private)
 106     private static final StackWalker walker =
 107         AccessController.doPrivileged((PrivilegedAction<StackWalker>) () ->
 108             StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE));
 109 
 110     // Abstract base class for elements
 111     private abstract class Element {
 112         public final Element parent;
 113 
 114         public Object value = null;
 115         private BeanAdapter valueAdapter = null;
 116 
 117         public final LinkedList<Attribute> eventHandlerAttributes = new LinkedList<Attribute>();
 118         public final LinkedList<Attribute> instancePropertyAttributes = new LinkedList<Attribute>();
 119         public final LinkedList<Attribute> staticPropertyAttributes = new LinkedList<Attribute>();
 120         public final LinkedList<PropertyElement> staticPropertyElements = new LinkedList<PropertyElement>();
 121 
 122         public Element() {
 123             parent = current;
 124         }
 125 
 126         public boolean isCollection() {
 127             // Return true if value is a list, or if the value's type defines
 128             // a default property that is a list
 129             boolean collection;
 130             if (value instanceof List<?>) {
 131                 collection = true;
 132             } else {
 133                 Class<?> type = value.getClass();
 134                 DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
 135 
 136                 if (defaultProperty != null) {
 137                     collection = getProperties().get(defaultProperty.value()) instanceof List<?>;
 138                 } else {
 139                     collection = false;
 140                 }
 141             }
 142 
 143             return collection;
 144         }
 145 
 146         @SuppressWarnings("unchecked")
 147         public void add(Object element) throws LoadException {
 148             // If value is a list, add element to it; otherwise, get the value
 149             // of the default property, which is assumed to be a list and add
 150             // to that (coerce to the appropriate type)
 151             List<Object> list;
 152             if (value instanceof List<?>) {
 153                 list = (List<Object>)value;
 154             } else {
 155                 Class<?> type = value.getClass();
 156                 DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
 157                 String defaultPropertyName = defaultProperty.value();
 158 
 159                 // Get the list value
 160                 list = (List<Object>)getProperties().get(defaultPropertyName);
 161 
 162                 // Coerce the element to the list item type
 163                 if (!Map.class.isAssignableFrom(type)) {
 164                     Type listType = getValueAdapter().getGenericType(defaultPropertyName);
 165                     element = BeanAdapter.coerce(element, BeanAdapter.getListItemType(listType));
 166                 }
 167             }
 168 
 169             list.add(element);
 170         }
 171 
 172         public void set(Object value) throws LoadException {
 173             if (this.value == null) {
 174                 throw constructLoadException("Cannot set value on this element.");
 175             }
 176 
 177             // Apply value to this element's properties
 178             Class<?> type = this.value.getClass();
 179             DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
 180             if (defaultProperty == null) {
 181                 throw constructLoadException("Element does not define a default property.");
 182             }
 183 
 184             getProperties().put(defaultProperty.value(), value);
 185         }
 186 
 187         public void updateValue(Object value) {
 188             this.value = value;
 189             valueAdapter = null;
 190         }
 191 
 192         public boolean isTyped() {
 193             return !(value instanceof Map<?, ?>);
 194         }
 195 
 196         public BeanAdapter getValueAdapter() {
 197             if (valueAdapter == null) {
 198                 valueAdapter = new BeanAdapter(value);
 199             }
 200 
 201             return valueAdapter;
 202         }
 203 
 204         @SuppressWarnings("unchecked")
 205         public Map<String, Object> getProperties() {
 206             return (isTyped()) ? getValueAdapter() : (Map<String, Object>)value;
 207         }
 208 
 209         public void processStartElement() throws IOException {
 210             for (int i = 0, n = xmlStreamReader.getAttributeCount(); i < n; i++) {
 211                 String prefix = xmlStreamReader.getAttributePrefix(i);
 212                 String localName = xmlStreamReader.getAttributeLocalName(i);
 213                 String value = xmlStreamReader.getAttributeValue(i);
 214 
 215                 if (loadListener != null
 216                     && prefix != null
 217                     && prefix.equals(FX_NAMESPACE_PREFIX)) {
 218                     loadListener.readInternalAttribute(prefix + ":" + localName, value);
 219                 }
 220 
 221                 processAttribute(prefix, localName, value);
 222             }
 223         }
 224 
 225         public void processEndElement() throws IOException {
 226             // No-op
 227         }
 228 
 229         public void processCharacters() throws IOException {
 230             throw constructLoadException("Unexpected characters in input stream.");
 231         }
 232 
 233         public void processInstancePropertyAttributes() throws IOException {
 234             if (instancePropertyAttributes.size() > 0) {
 235                 for (Attribute attribute : instancePropertyAttributes) {
 236                     processPropertyAttribute(attribute);
 237                 }
 238             }
 239         }
 240 
 241         public void processAttribute(String prefix, String localName, String value)
 242             throws IOException{
 243             if (prefix == null) {
 244                 // Add the attribute to the appropriate list
 245                 if (localName.startsWith(EVENT_HANDLER_PREFIX)) {
 246                     if (loadListener != null) {
 247                         loadListener.readEventHandlerAttribute(localName, value);
 248                     }
 249 
 250                     eventHandlerAttributes.add(new Attribute(localName, null, value));
 251                 } else {
 252                     int i = localName.lastIndexOf('.');
 253 
 254                     if (i == -1) {
 255                         // The attribute represents an instance property
 256                         if (loadListener != null) {
 257                             loadListener.readPropertyAttribute(localName, null, value);
 258                         }
 259 
 260                         instancePropertyAttributes.add(new Attribute(localName, null, value));
 261                     } else {
 262                         // The attribute represents a static property
 263                         String name = localName.substring(i + 1);
 264                         Class<?> sourceType = getType(localName.substring(0, i));
 265 
 266                         if (sourceType != null) {
 267                             if (loadListener != null) {
 268                                 loadListener.readPropertyAttribute(name, sourceType, value);
 269                             }
 270 
 271                             staticPropertyAttributes.add(new Attribute(name, sourceType, value));
 272                         } else if (staticLoad) {
 273                             if (loadListener != null) {
 274                                 loadListener.readUnknownStaticPropertyAttribute(localName, value);
 275                             }
 276                         } else {
 277                             throw constructLoadException(localName + " is not a valid attribute.");
 278                         }
 279                     }
 280 
 281                 }
 282             } else {
 283                 throw constructLoadException(prefix + ":" + localName
 284                     + " is not a valid attribute.");
 285             }
 286         }
 287 
 288         @SuppressWarnings("unchecked")
 289         public void processPropertyAttribute(Attribute attribute) throws IOException {
 290             String value = attribute.value;
 291             if (isBindingExpression(value)) {
 292                 // Resolve the expression
 293                 Expression expression;
 294 
 295                 if (attribute.sourceType != null) {
 296                     throw constructLoadException("Cannot bind to static property.");
 297                 }
 298 
 299                 if (!isTyped()) {
 300                     throw constructLoadException("Cannot bind to untyped object.");
 301                 }
 302 
 303                 // TODO We may want to identify binding properties in processAttribute()
 304                 // and apply them after build() has been called
 305                 if (this.value instanceof Builder) {
 306                     throw constructLoadException("Cannot bind to builder property.");
 307                 }
 308 
 309                 if (!impl_isStaticLoad()) {
 310                     value = value.substring(BINDING_EXPRESSION_PREFIX.length(),
 311                             value.length() - 1);
 312                     expression = Expression.valueOf(value);
 313 
 314                     // Create the binding
 315                     BeanAdapter targetAdapter = new BeanAdapter(this.value);
 316                     ObservableValue<Object> propertyModel = targetAdapter.getPropertyModel(attribute.name);
 317                     Class<?> type = targetAdapter.getType(attribute.name);
 318 
 319                     if (propertyModel instanceof Property<?>) {
 320                         ((Property<Object>) propertyModel).bind(new ExpressionValue(namespace, expression, type));
 321                     }
 322                 }
 323             } else if (isBidirectionalBindingExpression(value)) {
 324                 throw constructLoadException(new UnsupportedOperationException("This feature is not currently enabled."));
 325             } else {
 326                 processValue(attribute.sourceType, attribute.name, value);
 327             }
 328         }
 329 
 330         private boolean isBindingExpression(String aValue) {
 331             return aValue.startsWith(BINDING_EXPRESSION_PREFIX)
 332                    && aValue.endsWith(BINDING_EXPRESSION_SUFFIX);
 333         }
 334 
 335         private boolean isBidirectionalBindingExpression(String aValue) {
 336             return aValue.startsWith(BI_DIRECTIONAL_BINDING_PREFIX);
 337         }
 338 
 339         private boolean processValue(Class sourceType, String propertyName, String aValue)
 340             throws LoadException {
 341 
 342             boolean processed = false;
 343                 //process list or array first
 344                 if (sourceType == null && isTyped()) {
 345                     BeanAdapter valueAdapter = getValueAdapter();
 346                     Class<?> type = valueAdapter.getType(propertyName);
 347 
 348                     if (type == null) {
 349                         throw new PropertyNotFoundException("Property \"" + propertyName
 350                             + "\" does not exist" + " or is read-only.");
 351                     }
 352 
 353                     if (List.class.isAssignableFrom(type)
 354                         && valueAdapter.isReadOnly(propertyName)) {
 355                         populateListFromString(valueAdapter, propertyName, aValue);
 356                         processed = true;
 357                     } else if (type.isArray()) {
 358                         applyProperty(propertyName, sourceType,
 359                                 populateArrayFromString(type, aValue));
 360                         processed = true;
 361                     }
 362                 }
 363                 if (!processed) {
 364                     applyProperty(propertyName, sourceType, resolvePrefixedValue(aValue));
 365                     processed = true;
 366                 }
 367                 return processed;
 368         }
 369 
 370         /**
 371          * Resolves value prefixed with RELATIVE_PATH_PREFIX and RESOURCE_KEY_PREFIX.
 372          */
 373         private Object resolvePrefixedValue(String aValue) throws LoadException {
 374             if (aValue.startsWith(ESCAPE_PREFIX)) {
 375                 aValue = aValue.substring(ESCAPE_PREFIX.length());
 376 
 377                 if (aValue.length() == 0
 378                     || !(aValue.startsWith(ESCAPE_PREFIX)
 379                         || aValue.startsWith(RELATIVE_PATH_PREFIX)
 380                         || aValue.startsWith(RESOURCE_KEY_PREFIX)
 381                         || aValue.startsWith(EXPRESSION_PREFIX)
 382                         || aValue.startsWith(BI_DIRECTIONAL_BINDING_PREFIX))) {
 383                     throw constructLoadException("Invalid escape sequence.");
 384                 }
 385                 return aValue;
 386             } else if (aValue.startsWith(RELATIVE_PATH_PREFIX)) {
 387                 aValue = aValue.substring(RELATIVE_PATH_PREFIX.length());
 388                 if (aValue.length() == 0) {
 389                     throw constructLoadException("Missing relative path.");
 390                 }
 391                 if (aValue.startsWith(RELATIVE_PATH_PREFIX)) {
 392                     // The prefix was escaped
 393                     warnDeprecatedEscapeSequence(RELATIVE_PATH_PREFIX);
 394                     return aValue;
 395                 } else {
 396                         if (aValue.charAt(0) == '/') {
 397                             // FIXME: JIGSAW -- use Class.getResourceAsStream if resource is in a module
 398                             final URL res = getClassLoader().getResource(aValue.substring(1));
 399                             if (res == null) {
 400                                 throw constructLoadException("Invalid resource: " + aValue + " not found on the classpath");
 401                             }
 402                             return res.toString();
 403                         } else {
 404                             try {
 405                                 return new URL(FXMLLoader.this.location, aValue).toString();
 406                             } catch (MalformedURLException e) {
 407                                 System.err.println(FXMLLoader.this.location + "/" + aValue);
 408                             }
 409                         }
 410                 }
 411             } else if (aValue.startsWith(RESOURCE_KEY_PREFIX)) {
 412                 aValue = aValue.substring(RESOURCE_KEY_PREFIX.length());
 413                 if (aValue.length() == 0) {
 414                     throw constructLoadException("Missing resource key.");
 415                 }
 416                 if (aValue.startsWith(RESOURCE_KEY_PREFIX)) {
 417                     // The prefix was escaped
 418                     warnDeprecatedEscapeSequence(RESOURCE_KEY_PREFIX);
 419                     return aValue;
 420                 } else {
 421                     // Resolve the resource value
 422                     if (resources == null) {
 423                         throw constructLoadException("No resources specified.");
 424                     }
 425                     if (!resources.containsKey(aValue)) {
 426                         throw constructLoadException("Resource \"" + aValue + "\" not found.");
 427                     }
 428 
 429                     return resources.getString(aValue);
 430                 }
 431             } else if (aValue.startsWith(EXPRESSION_PREFIX)) {
 432                 aValue = aValue.substring(EXPRESSION_PREFIX.length());
 433                 if (aValue.length() == 0) {
 434                     throw constructLoadException("Missing expression.");
 435                 }
 436                 if (aValue.startsWith(EXPRESSION_PREFIX)) {
 437                     // The prefix was escaped
 438                     warnDeprecatedEscapeSequence(EXPRESSION_PREFIX);
 439                     return aValue;
 440                 } else if (aValue.equals(NULL_KEYWORD)) {
 441                     // The attribute value is null
 442                     return null;
 443                 }
 444                 return Expression.get(namespace, KeyPath.parse(aValue));
 445             }
 446             return aValue;
 447         }
 448 
 449         /**
 450          * Creates an array of given type and populates it with values from
 451          * a string where tokens are separated by ARRAY_COMPONENT_DELIMITER.
 452          * If token is prefixed with RELATIVE_PATH_PREFIX a value added to
 453          * the array becomes relative to document location.
 454          */
 455         private Object populateArrayFromString(
 456                 Class<?>type,
 457                 String stringValue) throws LoadException {
 458 
 459             Object propertyValue = null;
 460             // Split the string and set the values as an array
 461             Class<?> componentType = type.getComponentType();
 462 
 463             if (stringValue.length() > 0) {
 464                 String[] values = stringValue.split(ARRAY_COMPONENT_DELIMITER);
 465                 propertyValue = Array.newInstance(componentType, values.length);
 466                 for (int i = 0; i < values.length; i++) {
 467                     Array.set(propertyValue, i,
 468                             BeanAdapter.coerce(resolvePrefixedValue(values[i].trim()),
 469                             type.getComponentType()));
 470                 }
 471             } else {
 472                 propertyValue = Array.newInstance(componentType, 0);
 473             }
 474             return propertyValue;
 475         }
 476 
 477         /**
 478          * Populates list with values from a string where tokens are separated
 479          * by ARRAY_COMPONENT_DELIMITER. If token is prefixed with RELATIVE_PATH_PREFIX
 480          * a value added to the list becomes relative to document location.
 481          */
 482         private void populateListFromString(
 483                 BeanAdapter valueAdapter,
 484                 String listPropertyName,
 485                 String stringValue) throws LoadException {
 486             // Split the string and add the values to the list
 487             List<Object> list = (List<Object>)valueAdapter.get(listPropertyName);
 488             Type listType = valueAdapter.getGenericType(listPropertyName);
 489             Type itemType = (Class<?>)BeanAdapter.getGenericListItemType(listType);
 490 
 491             if (itemType instanceof ParameterizedType) {
 492                 itemType = ((ParameterizedType)itemType).getRawType();
 493             }
 494 
 495             if (stringValue.length() > 0) {
 496                 String[] values = stringValue.split(ARRAY_COMPONENT_DELIMITER);
 497 
 498                 for (String aValue: values) {
 499                     aValue = aValue.trim();
 500                     list.add(
 501                             BeanAdapter.coerce(resolvePrefixedValue(aValue),
 502                                                (Class<?>)itemType));
 503                 }
 504             }
 505         }
 506 
 507         public void warnDeprecatedEscapeSequence(String prefix) {
 508             System.err.println(prefix + prefix + " is a deprecated escape sequence. "
 509                 + "Please use \\" + prefix + " instead.");
 510         }
 511 
 512         public void applyProperty(String name, Class<?> sourceType, Object value) {
 513             if (sourceType == null) {
 514                 getProperties().put(name, value);
 515             } else {
 516                 BeanAdapter.put(this.value, sourceType, name, value);
 517             }
 518         }
 519 
 520         private Object getExpressionObject(String handlerValue) throws LoadException{
 521             if (handlerValue.startsWith(EXPRESSION_PREFIX)) {
 522                 handlerValue = handlerValue.substring(EXPRESSION_PREFIX.length());
 523 
 524                 if (handlerValue.length() == 0) {
 525                     throw constructLoadException("Missing expression reference.");
 526                 }
 527 
 528                 Object expression = Expression.get(namespace, KeyPath.parse(handlerValue));
 529                 if (expression == null) {
 530                     throw constructLoadException("Unable to resolve expression : $" + handlerValue);
 531                 }
 532                 return expression;
 533             }
 534             return null;
 535         }
 536 
 537         private <T> T getExpressionObjectOfType(String handlerValue, Class<T> type) throws LoadException{
 538             Object expression = getExpressionObject(handlerValue);
 539             if (expression != null) {
 540                 if (type.isInstance(expression)) {
 541                     return (T) expression;
 542                 }
 543                 throw constructLoadException("Error resolving \"" + handlerValue +"\" expression."
 544                         + "Does not point to a " + type.getName());
 545             }
 546             return null;
 547         }
 548 
 549         private MethodHandler getControllerMethodHandle(String handlerName, SupportedType... types) throws LoadException {
 550             if (handlerName.startsWith(CONTROLLER_METHOD_PREFIX)) {
 551                 handlerName = handlerName.substring(CONTROLLER_METHOD_PREFIX.length());
 552 
 553                 if (!handlerName.startsWith(CONTROLLER_METHOD_PREFIX)) {
 554                     if (handlerName.length() == 0) {
 555                         throw constructLoadException("Missing controller method.");
 556                     }
 557 
 558                     if (controller == null) {
 559                         throw constructLoadException("No controller specified.");
 560                     }
 561 
 562                     for (SupportedType t : types) {
 563                         Method method = controllerAccessor
 564                                             .getControllerMethods()
 565                                             .get(t)
 566                                             .get(handlerName);
 567                         if (method != null) {
 568                             return new MethodHandler(controller, method, t);
 569                         }
 570                     }
 571                     Method method = controllerAccessor
 572                                         .getControllerMethods()
 573                                         .get(SupportedType.PARAMETERLESS)
 574                                         .get(handlerName);
 575                     if (method != null) {
 576                         return new MethodHandler(controller, method, SupportedType.PARAMETERLESS);
 577                     }
 578 
 579                     return null;
 580 
 581                 }
 582 
 583             }
 584             return null;
 585         }
 586 
 587         public void processEventHandlerAttributes() throws LoadException {
 588             if (eventHandlerAttributes.size() > 0 && !staticLoad) {
 589                 for (Attribute attribute : eventHandlerAttributes) {
 590                     String handlerName = attribute.value;
 591                     if (value instanceof ObservableList && attribute.name.equals(COLLECTION_HANDLER_NAME)) {
 592                         processObservableListHandler(handlerName);
 593                     } else if (value instanceof ObservableMap && attribute.name.equals(COLLECTION_HANDLER_NAME)) {
 594                         processObservableMapHandler(handlerName);
 595                     } else if (value instanceof ObservableSet && attribute.name.equals(COLLECTION_HANDLER_NAME)) {
 596                         processObservableSetHandler(handlerName);
 597                     } else if (attribute.name.endsWith(CHANGE_EVENT_HANDLER_SUFFIX)) {
 598                         processPropertyHandler(attribute.name, handlerName);
 599                     } else {
 600                         EventHandler<? extends Event> eventHandler = null;
 601                         MethodHandler handler = getControllerMethodHandle(handlerName, SupportedType.EVENT);
 602                         if (handler != null) {
 603                             eventHandler = new ControllerMethodEventHandler<>(handler);
 604                         }
 605 
 606                         if (eventHandler == null) {
 607                             eventHandler = getExpressionObjectOfType(handlerName, EventHandler.class);
 608                         }
 609 
 610                         if (eventHandler == null) {
 611                             if (handlerName.length() == 0 || scriptEngine == null) {
 612                                 throw constructLoadException("Error resolving " + attribute.name + "='" + attribute.value
 613                                         + "', either the event handler is not in the Namespace or there is an error in the script.");
 614                             }
 615 
 616                             eventHandler = new ScriptEventHandler(handlerName, scriptEngine);
 617                         }
 618 
 619                         // Add the handler
 620                         getValueAdapter().put(attribute.name, eventHandler);
 621                     }
 622                 }
 623             }
 624         }
 625 
 626         private void processObservableListHandler(String handlerValue) throws LoadException {
 627             ObservableList list = (ObservableList)value;
 628             if (handlerValue.startsWith(CONTROLLER_METHOD_PREFIX)) {
 629                 MethodHandler handler = getControllerMethodHandle(handlerValue, SupportedType.LIST_CHANGE_LISTENER);
 630                 if (handler != null) {
 631                     list.addListener(new ObservableListChangeAdapter(handler));
 632                 } else {
 633                     throw constructLoadException("Controller method \"" + handlerValue + "\" not found.");
 634                 }
 635             } else if (handlerValue.startsWith(EXPRESSION_PREFIX)) {
 636                 Object listener = getExpressionObject(handlerValue);
 637                    if (listener instanceof ListChangeListener) {
 638                     list.addListener((ListChangeListener) listener);
 639                 } else if (listener instanceof InvalidationListener) {
 640                     list.addListener((InvalidationListener) listener);
 641                 } else {
 642                     throw constructLoadException("Error resolving \"" + handlerValue + "\" expression."
 643                             + "Must be either ListChangeListener or InvalidationListener");
 644                 }
 645             }
 646         }
 647 
 648         private void processObservableMapHandler(String handlerValue) throws LoadException {
 649             ObservableMap map = (ObservableMap)value;
 650             if (handlerValue.startsWith(CONTROLLER_METHOD_PREFIX)) {
 651                 MethodHandler handler = getControllerMethodHandle(handlerValue, SupportedType.MAP_CHANGE_LISTENER);
 652                 if (handler != null) {
 653                     map.addListener(new ObservableMapChangeAdapter(handler));
 654                 } else {
 655                     throw constructLoadException("Controller method \"" + handlerValue + "\" not found.");
 656                 }
 657             } else if (handlerValue.startsWith(EXPRESSION_PREFIX)) {
 658                 Object listener = getExpressionObject(handlerValue);
 659                 if (listener instanceof MapChangeListener) {
 660                     map.addListener((MapChangeListener) listener);
 661                 } else if (listener instanceof InvalidationListener) {
 662                     map.addListener((InvalidationListener) listener);
 663                 } else {
 664                     throw constructLoadException("Error resolving \"" + handlerValue + "\" expression."
 665                             + "Must be either MapChangeListener or InvalidationListener");
 666                 }
 667             }
 668         }
 669 
 670         private void processObservableSetHandler(String handlerValue) throws LoadException {
 671             ObservableSet set = (ObservableSet)value;
 672             if (handlerValue.startsWith(CONTROLLER_METHOD_PREFIX)) {
 673                 MethodHandler handler = getControllerMethodHandle(handlerValue, SupportedType.SET_CHANGE_LISTENER);
 674                 if (handler != null) {
 675                     set.addListener(new ObservableSetChangeAdapter(handler));
 676                 } else {
 677                     throw constructLoadException("Controller method \"" + handlerValue + "\" not found.");
 678                 }
 679             } else if (handlerValue.startsWith(EXPRESSION_PREFIX)) {
 680                 Object listener = getExpressionObject(handlerValue);
 681                 if (listener instanceof SetChangeListener) {
 682                     set.addListener((SetChangeListener) listener);
 683                 } else if (listener instanceof InvalidationListener) {
 684                     set.addListener((InvalidationListener) listener);
 685                 } else {
 686                     throw constructLoadException("Error resolving \"" + handlerValue + "\" expression."
 687                             + "Must be either SetChangeListener or InvalidationListener");
 688                 }
 689             }
 690         }
 691 
 692         private void processPropertyHandler(String attributeName, String handlerValue) throws LoadException {
 693             int i = EVENT_HANDLER_PREFIX.length();
 694             int j = attributeName.length() - CHANGE_EVENT_HANDLER_SUFFIX.length();
 695 
 696             if (i != j) {
 697                 String key = Character.toLowerCase(attributeName.charAt(i))
 698                         + attributeName.substring(i + 1, j);
 699 
 700                 ObservableValue<Object> propertyModel = getValueAdapter().getPropertyModel(key);
 701                 if (propertyModel == null) {
 702                     throw constructLoadException(value.getClass().getName() + " does not define"
 703                             + " a property model for \"" + key + "\".");
 704                 }
 705 
 706                 if (handlerValue.startsWith(CONTROLLER_METHOD_PREFIX)) {
 707                     final MethodHandler handler = getControllerMethodHandle(handlerValue, SupportedType.PROPERTY_CHANGE_LISTENER, SupportedType.EVENT);
 708                     if (handler != null) {
 709                         if (handler.type == SupportedType.EVENT) {
 710                             // Note: this part is solely for purpose of 2.2 backward compatibility where an Event object
 711                             // has been used instead of usual property change parameters
 712                             propertyModel.addListener(new ChangeListener<Object>() {
 713                                 @Override
 714                                 public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
 715                                     handler.invoke(new Event(value, null, Event.ANY));
 716                                 }
 717                             });
 718                         } else {
 719                             propertyModel.addListener(new PropertyChangeAdapter(handler));
 720                         }
 721                     } else {
 722                     throw constructLoadException("Controller method \"" + handlerValue + "\" not found.");
 723                     }
 724                 } else if (handlerValue.startsWith(EXPRESSION_PREFIX)) {
 725                     Object listener = getExpressionObject(handlerValue);
 726                     if (listener instanceof ChangeListener) {
 727                         propertyModel.addListener((ChangeListener) listener);
 728                     } else if (listener instanceof InvalidationListener) {
 729                         propertyModel.addListener((InvalidationListener) listener);
 730                     } else {
 731                         throw constructLoadException("Error resolving \"" + handlerValue + "\" expression."
 732                                 + "Must be either ChangeListener or InvalidationListener");
 733                     }
 734                 }
 735 
 736             }
 737         }
 738     }
 739 
 740     // Element representing a value
 741     private abstract class ValueElement extends Element {
 742         public String fx_id = null;
 743 
 744         @Override
 745         public void processStartElement() throws IOException {
 746             super.processStartElement();
 747 
 748             updateValue(constructValue());
 749 
 750             if (value instanceof Builder<?>) {
 751                 processInstancePropertyAttributes();
 752             } else {
 753                 processValue();
 754             }
 755         }
 756 
 757         @Override
 758         @SuppressWarnings("unchecked")
 759         public void processEndElement() throws IOException {
 760             super.processEndElement();
 761 
 762             // Build the value, if necessary
 763             if (value instanceof Builder<?>) {
 764                 Builder<Object> builder = (Builder<Object>)value;
 765                 updateValue(builder.build());
 766 
 767                 processValue();
 768             } else {
 769                 processInstancePropertyAttributes();
 770             }
 771 
 772             processEventHandlerAttributes();
 773 
 774             // Process static property attributes
 775             if (staticPropertyAttributes.size() > 0) {
 776                 for (Attribute attribute : staticPropertyAttributes) {
 777                     processPropertyAttribute(attribute);
 778                 }
 779             }
 780 
 781             // Process static property elements
 782             if (staticPropertyElements.size() > 0) {
 783                 for (PropertyElement element : staticPropertyElements) {
 784                     BeanAdapter.put(value, element.sourceType, element.name, element.value);
 785                 }
 786             }
 787 
 788             if (parent != null) {
 789                 if (parent.isCollection()) {
 790                     parent.add(value);
 791                 } else {
 792                     parent.set(value);
 793                 }
 794             }
 795         }
 796 
 797         private Object getListValue(Element parent, String listPropertyName, Object value) {
 798             // If possible, coerce the value to the list item type
 799             if (parent.isTyped()) {
 800                 Type listType = parent.getValueAdapter().getGenericType(listPropertyName);
 801 
 802                 if (listType != null) {
 803                     Type itemType = BeanAdapter.getGenericListItemType(listType);
 804 
 805                     if (itemType instanceof ParameterizedType) {
 806                         itemType = ((ParameterizedType)itemType).getRawType();
 807                     }
 808 
 809                     value = BeanAdapter.coerce(value, (Class<?>)itemType);
 810                 }
 811             }
 812 
 813             return value;
 814         }
 815 
 816         private void processValue() throws LoadException {
 817             // If this is the root element, update the value
 818             if (parent == null) {
 819                 root = value;
 820 
 821                 // checking version of fx namespace - throw exception if not supported
 822                 String fxNSURI = xmlStreamReader.getNamespaceContext().getNamespaceURI("fx");
 823                 if (fxNSURI != null) {
 824                     String fxVersion = fxNSURI.substring(fxNSURI.lastIndexOf("/") + 1);
 825                     if (compareJFXVersions(FX_NAMESPACE_VERSION, fxVersion) < 0) {
 826                         throw constructLoadException("Loading FXML document of version " +
 827                                 fxVersion + " by JavaFX runtime supporting version " + FX_NAMESPACE_VERSION);
 828                     }
 829                 }
 830 
 831                 // checking the version JavaFX API - print warning if not supported
 832                 String defaultNSURI = xmlStreamReader.getNamespaceContext().getNamespaceURI("");
 833                 if (defaultNSURI != null) {
 834                     String nsVersion = defaultNSURI.substring(defaultNSURI.lastIndexOf("/") + 1);
 835                     if (compareJFXVersions(JAVAFX_VERSION, nsVersion) < 0) {
 836                         Logging.getJavaFXLogger().warning("Loading FXML document with JavaFX API of version " +
 837                                 nsVersion + " by JavaFX runtime of version " + JAVAFX_VERSION);
 838                     }
 839                 }
 840             }
 841 
 842             // Add the value to the namespace
 843             if (fx_id != null) {
 844                 namespace.put(fx_id, value);
 845 
 846                 // If the value defines an ID property, set it
 847                 IDProperty idProperty = value.getClass().getAnnotation(IDProperty.class);
 848 
 849                 if (idProperty != null) {
 850                     Map<String, Object> properties = getProperties();
 851                     // set fx:id property value to Node.id only if Node.id was not
 852                     // already set when processing start element attributes
 853                     if (properties.get(idProperty.value()) == null) {
 854                         properties.put(idProperty.value(), fx_id);
 855                     }
 856                 }
 857 
 858                 // Set the controller field value
 859                 injectFields(fx_id, value);
 860             }
 861         }
 862 
 863         @Override
 864         @SuppressWarnings("unchecked")
 865         public void processCharacters() throws LoadException {
 866             Class<?> type = value.getClass();
 867             DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class);
 868 
 869             // If the default property is a read-only list, add the value to it;
 870             // otherwise, set the value as the default property
 871             if (defaultProperty != null) {
 872                 String text = xmlStreamReader.getText();
 873                 text = extraneousWhitespacePattern.matcher(text).replaceAll(" ");
 874 
 875                 String defaultPropertyName = defaultProperty.value();
 876                 BeanAdapter valueAdapter = getValueAdapter();
 877 
 878                 if (valueAdapter.isReadOnly(defaultPropertyName)
 879                     && List.class.isAssignableFrom(valueAdapter.getType(defaultPropertyName))) {
 880                     List<Object> list = (List<Object>)valueAdapter.get(defaultPropertyName);
 881                     list.add(getListValue(this, defaultPropertyName, text));
 882                 } else {
 883                     valueAdapter.put(defaultPropertyName, text.trim());
 884                 }
 885             } else {
 886                 throw constructLoadException(type.getName() + " does not have a default property.");
 887             }
 888         }
 889 
 890         @Override
 891         public void processAttribute(String prefix, String localName, String value)
 892             throws IOException{
 893             if (prefix != null
 894                 && prefix.equals(FX_NAMESPACE_PREFIX)) {
 895                 if (localName.equals(FX_ID_ATTRIBUTE)) {
 896                     // Verify that ID is a valid identifier
 897                     if (value.equals(NULL_KEYWORD)) {
 898                         throw constructLoadException("Invalid identifier.");
 899                     }
 900 
 901                     for (int i = 0, n = value.length(); i < n; i++) {
 902                         if (!Character.isJavaIdentifierPart(value.charAt(i))) {
 903                             throw constructLoadException("Invalid identifier.");
 904                         }
 905                     }
 906 
 907                     fx_id = value;
 908 
 909                 } else if (localName.equals(FX_CONTROLLER_ATTRIBUTE)) {
 910                     if (current.parent != null) {
 911                         throw constructLoadException(FX_NAMESPACE_PREFIX + ":" + FX_CONTROLLER_ATTRIBUTE
 912                             + " can only be applied to root element.");
 913                     }
 914 
 915                     if (controller != null) {
 916                         throw constructLoadException("Controller value already specified.");
 917                     }
 918 
 919                     if (!staticLoad) {
 920                         Class<?> type;
 921                         try {
 922                             type = getClassLoader().loadClass(value);
 923                         } catch (ClassNotFoundException exception) {
 924                             throw constructLoadException(exception);
 925                         }
 926 
 927                         try {
 928                             if (controllerFactory == null) {
 929                                 ReflectUtil.checkPackageAccess(type);
 930                                 setController(type.newInstance());
 931                             } else {
 932                                 setController(controllerFactory.call(type));
 933                             }
 934                         } catch (InstantiationException exception) {
 935                             throw constructLoadException(exception);
 936                         } catch (IllegalAccessException exception) {
 937                             throw constructLoadException(exception);
 938                         }
 939                     }
 940                 } else {
 941                     throw constructLoadException("Invalid attribute.");
 942                 }
 943             } else {
 944                 super.processAttribute(prefix, localName, value);
 945             }
 946         }
 947 
 948         public abstract Object constructValue() throws IOException;
 949     }
 950 
 951     // Element representing a class instance
 952     private class InstanceDeclarationElement extends ValueElement {
 953         public Class<?> type;
 954 
 955         public String constant = null;
 956         public String factory = null;
 957 
 958         public InstanceDeclarationElement(Class<?> type) throws LoadException {
 959             this.type = type;
 960         }
 961 
 962         @Override
 963         public void processAttribute(String prefix, String localName, String value)
 964             throws IOException {
 965             if (prefix != null
 966                 && prefix.equals(FX_NAMESPACE_PREFIX)) {
 967                 if (localName.equals(FX_VALUE_ATTRIBUTE)) {
 968                     this.value = value;
 969                 } else if (localName.equals(FX_CONSTANT_ATTRIBUTE)) {
 970                     constant = value;
 971                 } else if (localName.equals(FX_FACTORY_ATTRIBUTE)) {
 972                     factory = value;
 973                 } else {
 974                     super.processAttribute(prefix, localName, value);
 975                 }
 976             } else {
 977                 super.processAttribute(prefix, localName, value);
 978             }
 979         }
 980 
 981         @Override
 982         public Object constructValue() throws IOException {
 983             Object value;
 984             if (this.value != null) {
 985                 value = BeanAdapter.coerce(this.value, type);
 986             } else if (constant != null) {
 987                 value = BeanAdapter.getConstantValue(type, constant);
 988             } else if (factory != null) {
 989                 Method factoryMethod;
 990                 try {
 991                     factoryMethod = MethodUtil.getMethod(type, factory, new Class[] {});
 992                 } catch (NoSuchMethodException exception) {
 993                     throw constructLoadException(exception);
 994                 }
 995 
 996                 try {
 997                     value = MethodUtil.invoke(factoryMethod, null, new Object [] {});
 998                 } catch (IllegalAccessException exception) {
 999                     throw constructLoadException(exception);
1000                 } catch (InvocationTargetException exception) {
1001                     throw constructLoadException(exception);
1002                 }
1003             } else {
1004                 value = (builderFactory == null) ? null : builderFactory.getBuilder(type);
1005 
1006                 if (value == null) {
1007                     value = DEFAULT_BUILDER_FACTORY.getBuilder(type);
1008                 }
1009 
1010                 if (value == null) {
1011                     try {
1012                         ReflectUtil.checkPackageAccess(type);
1013                         value = type.newInstance();
1014                     } catch (InstantiationException exception) {
1015                         throw constructLoadException(exception);
1016                     } catch (IllegalAccessException exception) {
1017                         throw constructLoadException(exception);
1018                     }
1019                 }
1020             }
1021 
1022             return value;
1023         }
1024     }
1025 
1026     // Element representing an unknown type
1027     private class UnknownTypeElement extends ValueElement {
1028         // Map type representing an unknown value
1029         @DefaultProperty("items")
1030         public class UnknownValueMap extends AbstractMap<String, Object> {
1031             private ArrayList<?> items = new ArrayList<Object>();
1032             private HashMap<String, Object> values = new HashMap<String, Object>();
1033 
1034             @Override
1035             public Object get(Object key) {
1036                 if (key == null) {
1037                     throw new NullPointerException();
1038                 }
1039 
1040                 return (key.equals(getClass().getAnnotation(DefaultProperty.class).value())) ?
1041                     items : values.get(key);
1042             }
1043 
1044             @Override
1045             public Object put(String key, Object value) {
1046                 if (key == null) {
1047                     throw new NullPointerException();
1048                 }
1049 
1050                 if (key.equals(getClass().getAnnotation(DefaultProperty.class).value())) {
1051                     throw new IllegalArgumentException();
1052                 }
1053 
1054                 return values.put(key, value);
1055             }
1056 
1057             @Override
1058             public Set<Entry<String, Object>> entrySet() {
1059                 return Collections.emptySet();
1060             }
1061         }
1062 
1063         @Override
1064         public void processEndElement() throws IOException {
1065             // No-op
1066         }
1067 
1068         @Override
1069         public Object constructValue() throws LoadException {
1070             return new UnknownValueMap();
1071         }
1072     }
1073 
1074     // Element representing an include
1075     private class IncludeElement extends ValueElement {
1076         public String source = null;
1077         public ResourceBundle resources = FXMLLoader.this.resources;
1078         public Charset charset = FXMLLoader.this.charset;
1079 
1080         @Override
1081         public void processAttribute(String prefix, String localName, String value)
1082             throws IOException {
1083             if (prefix == null) {
1084                 if (localName.equals(INCLUDE_SOURCE_ATTRIBUTE)) {
1085                     if (loadListener != null) {
1086                         loadListener.readInternalAttribute(localName, value);
1087                     }
1088 
1089                     source = value;
1090                 } else if (localName.equals(INCLUDE_RESOURCES_ATTRIBUTE)) {
1091                     if (loadListener != null) {
1092                         loadListener.readInternalAttribute(localName, value);
1093                     }
1094 
1095                     resources = ResourceBundle.getBundle(value, Locale.getDefault(),
1096                             FXMLLoader.this.resources.getClass().getClassLoader());
1097                 } else if (localName.equals(INCLUDE_CHARSET_ATTRIBUTE)) {
1098                     if (loadListener != null) {
1099                         loadListener.readInternalAttribute(localName, value);
1100                     }
1101 
1102                     charset = Charset.forName(value);
1103                 } else {
1104                     super.processAttribute(prefix, localName, value);
1105                 }
1106             } else {
1107                 super.processAttribute(prefix, localName, value);
1108             }
1109         }
1110 
1111         @Override
1112         public Object constructValue() throws IOException {
1113             if (source == null) {
1114                 throw constructLoadException(INCLUDE_SOURCE_ATTRIBUTE + " is required.");
1115             }
1116 
1117             URL location;
1118             final ClassLoader cl = getClassLoader();
1119             if (source.charAt(0) == '/') {
1120             // FIXME: JIGSAW -- use Class.getResourceAsStream if resource is in a module
1121                 location = cl.getResource(source.substring(1));
1122                 if (location == null) {
1123                     throw constructLoadException("Cannot resolve path: " + source);
1124                 }
1125             } else {
1126                 if (FXMLLoader.this.location == null) {
1127                     throw constructLoadException("Base location is undefined.");
1128                 }
1129 
1130                 location = new URL(FXMLLoader.this.location, source);
1131             }
1132 
1133             FXMLLoader fxmlLoader = new FXMLLoader(location, resources,
1134                 builderFactory, controllerFactory, charset,
1135                 loaders);
1136             fxmlLoader.parentLoader = FXMLLoader.this;
1137 
1138             if (isCyclic(FXMLLoader.this, fxmlLoader)) {
1139                 throw new IOException(
1140                         String.format(
1141                         "Including \"%s\" in \"%s\" created cyclic reference.",
1142                         fxmlLoader.location.toExternalForm(),
1143                         FXMLLoader.this.location.toExternalForm()));
1144             }
1145             fxmlLoader.setClassLoader(cl);
1146             fxmlLoader.impl_setStaticLoad(staticLoad);
1147 
1148             Object value = fxmlLoader.loadImpl(callerClass);
1149 
1150             if (fx_id != null) {
1151                 String id = this.fx_id + CONTROLLER_SUFFIX;
1152                 Object controller = fxmlLoader.getController();
1153 
1154                 namespace.put(id, controller);
1155                 injectFields(id, controller);
1156             }
1157 
1158             return value;
1159         }
1160     }
1161 
1162     private void injectFields(String fieldName, Object value) throws LoadException {
1163         if (controller != null && fieldName != null) {
1164             List<Field> fields = controllerAccessor.getControllerFields().get(fieldName);
1165             if (fields != null) {
1166                 try {
1167                     for (Field f : fields) {
1168                         f.set(controller, value);
1169                     }
1170                 } catch (IllegalAccessException exception) {
1171                     throw constructLoadException(exception);
1172                 }
1173             }
1174         }
1175     }
1176 
1177     // Element representing a reference
1178     private class ReferenceElement extends ValueElement {
1179         public String source = null;
1180 
1181         @Override
1182         public void processAttribute(String prefix, String localName, String value)
1183             throws IOException {
1184             if (prefix == null) {
1185                 if (localName.equals(REFERENCE_SOURCE_ATTRIBUTE)) {
1186                     if (loadListener != null) {
1187                         loadListener.readInternalAttribute(localName, value);
1188                     }
1189 
1190                     source = value;
1191                 } else {
1192                     super.processAttribute(prefix, localName, value);
1193                 }
1194             } else {
1195                 super.processAttribute(prefix, localName, value);
1196             }
1197         }
1198 
1199         @Override
1200         public Object constructValue() throws LoadException {
1201             if (source == null) {
1202                 throw constructLoadException(REFERENCE_SOURCE_ATTRIBUTE + " is required.");
1203             }
1204 
1205             KeyPath path = KeyPath.parse(source);
1206             if (!Expression.isDefined(namespace, path)) {
1207                 throw constructLoadException("Value \"" + source + "\" does not exist.");
1208             }
1209 
1210             return Expression.get(namespace, path);
1211         }
1212     }
1213 
1214     // Element representing a copy
1215     private class CopyElement extends ValueElement {
1216         public String source = null;
1217 
1218         @Override
1219         public void processAttribute(String prefix, String localName, String value)
1220             throws IOException {
1221             if (prefix == null) {
1222                 if (localName.equals(COPY_SOURCE_ATTRIBUTE)) {
1223                     if (loadListener != null) {
1224                         loadListener.readInternalAttribute(localName, value);
1225                     }
1226 
1227                     source = value;
1228                 } else {
1229                     super.processAttribute(prefix, localName, value);
1230                 }
1231             } else {
1232                 super.processAttribute(prefix, localName, value);
1233             }
1234         }
1235 
1236         @Override
1237         public Object constructValue() throws LoadException {
1238             if (source == null) {
1239                 throw constructLoadException(COPY_SOURCE_ATTRIBUTE + " is required.");
1240             }
1241 
1242             KeyPath path = KeyPath.parse(source);
1243             if (!Expression.isDefined(namespace, path)) {
1244                 throw constructLoadException("Value \"" + source + "\" does not exist.");
1245             }
1246 
1247             Object sourceValue = Expression.get(namespace, path);
1248             Class<?> sourceValueType = sourceValue.getClass();
1249 
1250             Constructor<?> constructor = null;
1251             try {
1252                 constructor = ConstructorUtil.getConstructor(sourceValueType, new Class[] { sourceValueType });
1253             } catch (NoSuchMethodException exception) {
1254                 // No-op
1255             }
1256 
1257             Object value;
1258             if (constructor != null) {
1259                 try {
1260                     ReflectUtil.checkPackageAccess(sourceValueType);
1261                     value = constructor.newInstance(sourceValue);
1262                 } catch (InstantiationException exception) {
1263                     throw constructLoadException(exception);
1264                 } catch (IllegalAccessException exception) {
1265                     throw constructLoadException(exception);
1266                 } catch (InvocationTargetException exception) {
1267                     throw constructLoadException(exception);
1268                 }
1269             } else {
1270                 throw constructLoadException("Can't copy value " + sourceValue + ".");
1271             }
1272 
1273             return value;
1274         }
1275     }
1276 
1277     // Element representing a predefined root value
1278     private class RootElement extends ValueElement {
1279         public String type = null;
1280 
1281         @Override
1282         public void processAttribute(String prefix, String localName, String value)
1283             throws IOException {
1284             if (prefix == null) {
1285                 if (localName.equals(ROOT_TYPE_ATTRIBUTE)) {
1286                     if (loadListener != null) {
1287                         loadListener.readInternalAttribute(localName, value);
1288                     }
1289 
1290                     type = value;
1291                 } else {
1292                     super.processAttribute(prefix, localName, value);
1293                 }
1294             } else {
1295                 super.processAttribute(prefix, localName, value);
1296             }
1297         }
1298 
1299         @Override
1300         public Object constructValue() throws LoadException {
1301             if (type == null) {
1302                 throw constructLoadException(ROOT_TYPE_ATTRIBUTE + " is required.");
1303             }
1304 
1305             Class<?> type = getType(this.type);
1306 
1307             if (type == null) {
1308                 throw constructLoadException(this.type + " is not a valid type.");
1309             }
1310 
1311             Object value;
1312             if (root == null) {
1313                 if (staticLoad) {
1314                     value = (builderFactory == null) ? null : builderFactory.getBuilder(type);
1315 
1316                     if (value == null) {
1317                         value = DEFAULT_BUILDER_FACTORY.getBuilder(type);
1318                     }
1319 
1320                     if (value == null) {
1321                         try {
1322                             ReflectUtil.checkPackageAccess(type);
1323                             value = type.newInstance();
1324                         } catch (InstantiationException exception) {
1325                             throw constructLoadException(exception);
1326                         } catch (IllegalAccessException exception) {
1327                             throw constructLoadException(exception);
1328                         }
1329                     }
1330                     root = value;
1331                 } else {
1332                     throw constructLoadException("Root hasn't been set. Use method setRoot() before load.");
1333                 }
1334             } else {
1335                 if (!type.isAssignableFrom(root.getClass())) {
1336                     throw constructLoadException("Root is not an instance of "
1337                         + type.getName() + ".");
1338                 }
1339 
1340                 value = root;
1341             }
1342 
1343             return value;
1344         }
1345     }
1346 
1347     // Element representing a property
1348     private class PropertyElement extends Element {
1349         public final String name;
1350         public final Class<?> sourceType;
1351         public final boolean readOnly;
1352 
1353         public PropertyElement(String name, Class<?> sourceType) throws LoadException {
1354             if (parent == null) {
1355                 throw constructLoadException("Invalid root element.");
1356             }
1357 
1358             if (parent.value == null) {
1359                 throw constructLoadException("Parent element does not support property elements.");
1360             }
1361 
1362             this.name = name;
1363             this.sourceType = sourceType;
1364 
1365             if (sourceType == null) {
1366                 // The element represents an instance property
1367                 if (name.startsWith(EVENT_HANDLER_PREFIX)) {
1368                     throw constructLoadException("\"" + name + "\" is not a valid element name.");
1369                 }
1370 
1371                 Map<String, Object> parentProperties = parent.getProperties();
1372 
1373                 if (parent.isTyped()) {
1374                     readOnly = parent.getValueAdapter().isReadOnly(name);
1375                 } else {
1376                 // If the map already defines a value for the property, assume
1377                     // that it is read-only
1378                     readOnly = parentProperties.containsKey(name);
1379                 }
1380 
1381                 if (readOnly) {
1382                     Object value = parentProperties.get(name);
1383                     if (value == null) {
1384                         throw constructLoadException("Invalid property.");
1385                     }
1386 
1387                     updateValue(value);
1388                 }
1389             } else {
1390                 // The element represents a static property
1391                 readOnly = false;
1392             }
1393         }
1394 
1395         @Override
1396         public boolean isCollection() {
1397             return (readOnly) ? super.isCollection() : false;
1398         }
1399 
1400         @Override
1401         public void add(Object element) throws LoadException {
1402             // Coerce the element to the list item type
1403             if (parent.isTyped()) {
1404                 Type listType = parent.getValueAdapter().getGenericType(name);
1405                 element = BeanAdapter.coerce(element, BeanAdapter.getListItemType(listType));
1406             }
1407 
1408             // Add the item to the list
1409             super.add(element);
1410         }
1411 
1412         @Override
1413         public void set(Object value) throws LoadException {
1414             // Update the value
1415             updateValue(value);
1416 
1417             if (sourceType == null) {
1418                 // Apply value to parent element's properties
1419                 parent.getProperties().put(name, value);
1420             } else {
1421                 if (parent.value instanceof Builder) {
1422                     // Defer evaluation of the property
1423                     parent.staticPropertyElements.add(this);
1424                 } else {
1425                     // Apply the static property value
1426                     BeanAdapter.put(parent.value, sourceType, name, value);
1427                 }
1428             }
1429         }
1430 
1431         @Override
1432         public void processAttribute(String prefix, String localName, String value)
1433             throws IOException {
1434             if (!readOnly) {
1435                 throw constructLoadException("Attributes are not supported for writable property elements.");
1436             }
1437 
1438             super.processAttribute(prefix, localName, value);
1439         }
1440 
1441         @Override
1442         public void processEndElement() throws IOException {
1443             super.processEndElement();
1444 
1445             if (readOnly) {
1446                 processInstancePropertyAttributes();
1447                 processEventHandlerAttributes();
1448             }
1449         }
1450 
1451         @Override
1452         public void processCharacters() throws IOException {
1453             String text = xmlStreamReader.getText();
1454             text = extraneousWhitespacePattern.matcher(text).replaceAll(" ").trim();
1455 
1456             if (readOnly) {
1457                 if (isCollection()) {
1458                     add(text);
1459                 } else {
1460                     super.processCharacters();
1461                 }
1462             } else {
1463                 set(text);
1464             }
1465         }
1466     }
1467 
1468     // Element representing an unknown static property
1469     private class UnknownStaticPropertyElement extends Element {
1470         public UnknownStaticPropertyElement() throws LoadException {
1471             if (parent == null) {
1472                 throw constructLoadException("Invalid root element.");
1473             }
1474 
1475             if (parent.value == null) {
1476                 throw constructLoadException("Parent element does not support property elements.");
1477             }
1478         }
1479 
1480         @Override
1481         public boolean isCollection() {
1482             return false;
1483         }
1484 
1485         @Override
1486         public void set(Object value) {
1487             updateValue(value);
1488         }
1489 
1490         @Override
1491         public void processCharacters() throws IOException {
1492             String text = xmlStreamReader.getText();
1493             text = extraneousWhitespacePattern.matcher(text).replaceAll(" ");
1494 
1495             updateValue(text.trim());
1496         }
1497     }
1498 
1499     // Element representing a script block
1500     private class ScriptElement extends Element {
1501         public String source = null;
1502         public Charset charset = FXMLLoader.this.charset;
1503 
1504         @Override
1505         public boolean isCollection() {
1506             return false;
1507         }
1508 
1509         @Override
1510         public void processStartElement() throws IOException {
1511             super.processStartElement();
1512 
1513             if (source != null && !staticLoad) {
1514                 int i = source.lastIndexOf(".");
1515                 if (i == -1) {
1516                     throw constructLoadException("Cannot determine type of script \""
1517                         + source + "\".");
1518                 }
1519 
1520                 String extension = source.substring(i + 1);
1521                 ScriptEngine engine;
1522                 final ClassLoader cl = getClassLoader();
1523                 if (scriptEngine != null && scriptEngine.getFactory().getExtensions().contains(extension)) {
1524                     // If we have a page language and it's engine supports the extension, use the same engine
1525                     engine = scriptEngine;
1526                 } else {
1527                     ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
1528                     try {
1529                         Thread.currentThread().setContextClassLoader(cl);
1530                         ScriptEngineManager scriptEngineManager = getScriptEngineManager();
1531                         engine = scriptEngineManager.getEngineByExtension(extension);
1532                     } finally {
1533                         Thread.currentThread().setContextClassLoader(oldLoader);
1534                     }
1535                 }
1536 
1537                 if (engine == null) {
1538                     throw constructLoadException("Unable to locate scripting engine for"
1539                         + " extension " + extension + ".");
1540                 }
1541 
1542                 try {
1543                     URL location;
1544                     if (source.charAt(0) == '/') {
1545                         // FIXME: JIGSAW -- use Class.getResourceAsStream if resource is in a module
1546                         location = cl.getResource(source.substring(1));
1547                     } else {
1548                         if (FXMLLoader.this.location == null) {
1549                             throw constructLoadException("Base location is undefined.");
1550                         }
1551 
1552                         location = new URL(FXMLLoader.this.location, source);
1553                     }
1554 
1555                     InputStreamReader scriptReader = null;
1556                     try {
1557                         scriptReader = new InputStreamReader(location.openStream(), charset);
1558                         engine.eval(scriptReader);
1559                     } catch(ScriptException exception) {
1560                         exception.printStackTrace();
1561                     } finally {
1562                         if (scriptReader != null) {
1563                             scriptReader.close();
1564                         }
1565                     }
1566                 } catch (IOException exception) {
1567                     throw constructLoadException(exception);
1568                 }
1569             }
1570         }
1571 
1572         @Override
1573         public void processEndElement() throws IOException {
1574             super.processEndElement();
1575 
1576             if (value != null && !staticLoad) {
1577                 // Evaluate the script
1578                 try {
1579                     scriptEngine.eval((String)value);
1580                 } catch (ScriptException exception) {
1581                     System.err.println(exception.getMessage());
1582                 }
1583             }
1584         }
1585 
1586         @Override
1587         public void processCharacters() throws LoadException {
1588             if (source != null) {
1589                 throw constructLoadException("Script source already specified.");
1590             }
1591 
1592             if (scriptEngine == null && !staticLoad) {
1593                 throw constructLoadException("Page language not specified.");
1594             }
1595 
1596             updateValue(xmlStreamReader.getText());
1597         }
1598 
1599         @Override
1600         public void processAttribute(String prefix, String localName, String value)
1601             throws IOException {
1602             if (prefix == null
1603                 && localName.equals(SCRIPT_SOURCE_ATTRIBUTE)) {
1604                 if (loadListener != null) {
1605                     loadListener.readInternalAttribute(localName, value);
1606                 }
1607 
1608                 source = value;
1609             } else if (localName.equals(SCRIPT_CHARSET_ATTRIBUTE)) {
1610                 if (loadListener != null) {
1611                     loadListener.readInternalAttribute(localName, value);
1612                 }
1613 
1614                 charset = Charset.forName(value);
1615             } else {
1616                 throw constructLoadException(prefix == null ? localName : prefix + ":" + localName
1617                     + " is not a valid attribute.");
1618             }
1619         }
1620     }
1621 
1622     // Element representing a define block
1623     private class DefineElement extends Element {
1624         @Override
1625         public boolean isCollection() {
1626             return true;
1627         }
1628 
1629         @Override
1630         public void add(Object element) {
1631             // No-op
1632         }
1633 
1634         @Override
1635         public void processAttribute(String prefix, String localName, String value)
1636             throws LoadException{
1637             throw constructLoadException("Element does not support attributes.");
1638         }
1639     }
1640 
1641     // Class representing an attribute of an element
1642     private static class Attribute {
1643         public final String name;
1644         public final Class<?> sourceType;
1645         public final String value;
1646 
1647         public Attribute(String name, Class<?> sourceType, String value) {
1648             this.name = name;
1649             this.sourceType = sourceType;
1650             this.value = value;
1651         }
1652     }
1653 
1654     // Event handler that delegates to a method defined by the controller object
1655     private static class ControllerMethodEventHandler<T extends Event> implements EventHandler<T> {
1656         private final MethodHandler handler;
1657 
1658         public ControllerMethodEventHandler(MethodHandler handler) {
1659             this.handler = handler;
1660         }
1661 
1662         @Override
1663         public void handle(T event) {
1664             handler.invoke(event);
1665         }
1666     }
1667 
1668     // Event handler implemented in script code
1669     private static class ScriptEventHandler implements EventHandler<Event> {
1670         public final String script;
1671         public final ScriptEngine scriptEngine;
1672 
1673         public ScriptEventHandler(String script, ScriptEngine scriptEngine) {
1674             this.script = script;
1675             this.scriptEngine = scriptEngine;
1676         }
1677 
1678         @Override
1679         public void handle(Event event) {
1680             // Don't pollute the page namespace with values defined in the script
1681             Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
1682             Bindings localBindings = scriptEngine.createBindings();
1683             localBindings.put(EVENT_KEY, event);
1684             localBindings.putAll(engineBindings);
1685             scriptEngine.setBindings(localBindings, ScriptContext.ENGINE_SCOPE);
1686 
1687             // Execute the script
1688             try {
1689                 scriptEngine.eval(script);
1690             } catch (ScriptException exception){
1691                 throw new RuntimeException(exception);
1692             }
1693 
1694             // Restore the original bindings
1695             scriptEngine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);
1696         }
1697     }
1698 
1699     // Observable list change listener
1700     private static class ObservableListChangeAdapter implements ListChangeListener {
1701         private final MethodHandler handler;
1702 
1703         public ObservableListChangeAdapter(MethodHandler handler) {
1704             this.handler = handler;
1705         }
1706 
1707         @Override
1708         @SuppressWarnings("unchecked")
1709         public void onChanged(Change change) {
1710             if (handler != null) {
1711                 handler.invoke(change);
1712             }
1713         }
1714     }
1715 
1716     // Observable map change listener
1717     private static class ObservableMapChangeAdapter implements MapChangeListener {
1718         public final MethodHandler handler;
1719 
1720         public ObservableMapChangeAdapter(MethodHandler handler) {
1721             this.handler = handler;
1722         }
1723 
1724         @Override
1725         public void onChanged(Change change) {
1726             if (handler != null) {
1727                 handler.invoke(change);
1728             }
1729         }
1730     }
1731 
1732     // Observable set change listener
1733     private static class ObservableSetChangeAdapter implements SetChangeListener {
1734         public final MethodHandler handler;
1735 
1736         public ObservableSetChangeAdapter(MethodHandler handler) {
1737             this.handler = handler;
1738         }
1739 
1740         @Override
1741         public void onChanged(Change change) {
1742             if (handler != null) {
1743                 handler.invoke(change);
1744             }
1745         }
1746     }
1747 
1748     // Property model change listener
1749     private static class PropertyChangeAdapter implements ChangeListener<Object> {
1750         public final MethodHandler handler;
1751 
1752         public PropertyChangeAdapter(MethodHandler handler) {
1753             this.handler = handler;
1754         }
1755 
1756         @Override
1757         public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
1758             handler.invoke(observable, oldValue, newValue);
1759         }
1760     }
1761 
1762     private static class MethodHandler {
1763         private final Object controller;
1764         private final Method method;
1765         private final SupportedType type;
1766 
1767         private MethodHandler(Object controller, Method method, SupportedType type) {
1768             this.method = method;
1769             this.controller = controller;
1770             this.type = type;
1771         }
1772 
1773         public void invoke(Object... params) {
1774             try {
1775                 if (type != SupportedType.PARAMETERLESS) {
1776                     MethodUtil.invoke(method, controller, params);
1777                 } else {
1778                     MethodUtil.invoke(method, controller, new Object[] {});
1779                 }
1780             } catch (InvocationTargetException exception) {
1781                 throw new RuntimeException(exception);
1782             } catch (IllegalAccessException exception) {
1783                 throw new RuntimeException(exception);
1784             }
1785         }
1786     }
1787 
1788     private URL location;
1789     private ResourceBundle resources;
1790 
1791     private ObservableMap<String, Object> namespace = FXCollections.observableHashMap();
1792 
1793     private Object root = null;
1794     private Object controller = null;
1795 
1796     private BuilderFactory builderFactory;
1797     private Callback<Class<?>, Object> controllerFactory;
1798     private Charset charset;
1799 
1800     private final LinkedList<FXMLLoader> loaders;
1801 
1802     private ClassLoader classLoader = null;
1803     private boolean staticLoad = false;
1804     private LoadListener loadListener = null;
1805 
1806     private FXMLLoader parentLoader;
1807 
1808     private XMLStreamReader xmlStreamReader = null;
1809     private Element current = null;
1810 
1811     private ScriptEngine scriptEngine = null;
1812 
1813     private List<String> packages = new LinkedList<String>();
1814     private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
1815 
1816     private ScriptEngineManager scriptEngineManager = null;
1817 
1818     private static ClassLoader defaultClassLoader = null;
1819 
1820     private static final Pattern extraneousWhitespacePattern = Pattern.compile("\\s+");
1821 
1822     private static BuilderFactory DEFAULT_BUILDER_FACTORY = new JavaFXBuilderFactory();
1823 
1824     /**
1825      * The character set used when character set is not explicitly specified
1826      */
1827     public static final String DEFAULT_CHARSET_NAME = "UTF-8";
1828 
1829     /**
1830      * The tag name of language processing instruction
1831      */
1832     public static final String LANGUAGE_PROCESSING_INSTRUCTION = "language";
1833     /**
1834      * The tag name of import processing instruction
1835      */
1836     public static final String IMPORT_PROCESSING_INSTRUCTION = "import";
1837 
1838     /**
1839      * Prefix of 'fx' namespace
1840      */
1841     public static final String FX_NAMESPACE_PREFIX = "fx";
1842     /**
1843      * The name of fx:controller attribute of a root
1844      */
1845     public static final String FX_CONTROLLER_ATTRIBUTE = "controller";
1846     /**
1847      * The name of fx:id attribute
1848      */
1849     public static final String FX_ID_ATTRIBUTE = "id";
1850     /**
1851      * The name of fx:value attribute
1852      */
1853     public static final String FX_VALUE_ATTRIBUTE = "value";
1854     /**
1855      * The tag name of 'fx:constant'
1856      * @since JavaFX 2.2
1857      */
1858     public static final String FX_CONSTANT_ATTRIBUTE = "constant";
1859     /**
1860      * The name of 'fx:factory' attribute
1861      */
1862     public static final String FX_FACTORY_ATTRIBUTE = "factory";
1863 
1864     /**
1865      * The tag name of &lt;fx:include&gt;
1866      */
1867     public static final String INCLUDE_TAG = "include";
1868     /**
1869      * &lt;fx:include&gt; 'source' attribute
1870      */
1871     public static final String INCLUDE_SOURCE_ATTRIBUTE = "source";
1872     /**
1873      * &lt;fx:include&gt; 'resources' attribute
1874      */
1875     public static final String INCLUDE_RESOURCES_ATTRIBUTE = "resources";
1876     /**
1877      * &lt;fx:include&gt; 'charset' attribute
1878      */
1879     public static final String INCLUDE_CHARSET_ATTRIBUTE = "charset";
1880 
1881     /**
1882      * The tag name of &lt;fx:script&gt;
1883      */
1884     public static final String SCRIPT_TAG = "script";
1885     /**
1886      * &lt;fx:script&gt; 'source' attribute
1887      */
1888     public static final String SCRIPT_SOURCE_ATTRIBUTE = "source";
1889     /**
1890      * &lt;fx:script&gt; 'charset' attribute
1891      */
1892     public static final String SCRIPT_CHARSET_ATTRIBUTE = "charset";
1893 
1894     /**
1895      * The tag name of &lt;fx:define&gt;
1896      */
1897     public static final String DEFINE_TAG = "define";
1898 
1899     /**
1900      * The tag name of &lt;fx:reference&gt;
1901      */
1902     public static final String REFERENCE_TAG = "reference";
1903     /**
1904      * &ltfx:reference&gt 'source' attribute
1905      */
1906     public static final String REFERENCE_SOURCE_ATTRIBUTE = "source";
1907 
1908     /**
1909      * The tag name of &lt;fx:root&gt;
1910      * @since JavaFX 2.2
1911      */
1912     public static final String ROOT_TAG = "root";
1913     /**
1914      * &lt;fx:root&gt; 'type' attribute
1915      * @since JavaFX 2.2
1916      */
1917     public static final String ROOT_TYPE_ATTRIBUTE = "type";
1918 
1919     /**
1920      * The tag name of &lt;fx:copy&gt;
1921      */
1922     public static final String COPY_TAG = "copy";
1923     /**
1924      * &lt;fx:copy&gt; 'source' attribute
1925      */
1926     public static final String COPY_SOURCE_ATTRIBUTE = "source";
1927 
1928     /**
1929      * The prefix of event handler attributes
1930      */
1931     public static final String EVENT_HANDLER_PREFIX = "on";
1932     /**
1933      * The name of the Event object in event handler scripts
1934      */
1935     public static final String EVENT_KEY = "event";
1936     /**
1937      * Suffix for property change/invalidation handlers
1938      */
1939     public static final String CHANGE_EVENT_HANDLER_SUFFIX = "Change";
1940     private static final String COLLECTION_HANDLER_NAME = EVENT_HANDLER_PREFIX + CHANGE_EVENT_HANDLER_SUFFIX;
1941 
1942     /**
1943      * Value that represents 'null'
1944      */
1945     public static final String NULL_KEYWORD = "null";
1946 
1947     /**
1948      * Escape prefix for escaping special characters inside attribute values.
1949      * Serves as an escape for {@link #ESCAPE_PREFIX}, {@link #RELATIVE_PATH_PREFIX},
1950      * {@link #RESOURCE_KEY_PREFIX, {@link #EXPRESSION_PREFIX}, {@link #BI_DIRECTIONAL_BINDING_PREFIX}
1951      * @since JavaFX 2.1
1952      */
1953     public static final String ESCAPE_PREFIX = "\\";
1954     /**
1955      * Prefix for relative location resultion
1956      */
1957     public static final String RELATIVE_PATH_PREFIX = "@";
1958     /**
1959      * Prefix for resource resolution
1960      */
1961     public static final String RESOURCE_KEY_PREFIX = "%";
1962     /**
1963      * Prefix for (variable) expression resolution
1964      */
1965     public static final String EXPRESSION_PREFIX = "$";
1966     /**
1967      * Prefix for binding expression resolution
1968      */
1969     public static final String BINDING_EXPRESSION_PREFIX = "${";
1970     /**
1971      * Suffix for binding expression resolution
1972      */
1973     public static final String BINDING_EXPRESSION_SUFFIX = "}";
1974 
1975     /**
1976      * Prefix for bidirectional-binding expression resolution
1977      * @since JavaFX 2.1
1978      */
1979     public static final String BI_DIRECTIONAL_BINDING_PREFIX = "#{";
1980     /**
1981      * Suffix for bidirectional-binding expression resolution
1982      * @since JavaFX 2.1
1983      */
1984     public static final String BI_DIRECTIONAL_BINDING_SUFFIX = "}";
1985 
1986     /**
1987      * Delimiter for arrays as values
1988      * @since JavaFX 2.1
1989      */
1990     public static final String ARRAY_COMPONENT_DELIMITER = ",";
1991 
1992     /**
1993      * A key for location URL in namespace map
1994      * @see #getNamespace()
1995      * @since JavaFX 2.2
1996      */
1997     public static final String LOCATION_KEY = "location";
1998     /**
1999      * A key for ResourceBundle in namespace map
2000      * @see #getNamespace()
2001      * @since JavaFX 2.2
2002      */
2003     public static final String RESOURCES_KEY = "resources";
2004 
2005     /**
2006      * Prefix for controller method resolution
2007      */
2008     public static final String CONTROLLER_METHOD_PREFIX = "#";
2009     /**
2010      * A key for controller in namespace map
2011      * @see #getNamespace()
2012      * @since JavaFX 2.1
2013      */
2014     public static final String CONTROLLER_KEYWORD = "controller";
2015     /**
2016      * A suffix for controllers of included fxml files.
2017      * The full key is stored in namespace map.
2018      * @see #getNamespace()
2019      * @since JavaFX 2.2
2020      */
2021     public static final String CONTROLLER_SUFFIX = "Controller";
2022 
2023     /**
2024      * The name of initialize method
2025      * @since JavaFX 2.2
2026      */
2027     public static final String INITIALIZE_METHOD_NAME = "initialize";
2028 
2029     /**
2030      * Contains the current javafx version
2031      * @since JavaFX 8.0
2032      */
2033     public static final String JAVAFX_VERSION;
2034 
2035     /**
2036      * Contains the current fx namepsace version
2037      * @since JavaFX 8.0
2038      */
2039     public static final String FX_NAMESPACE_VERSION = "1";
2040 
2041     static {
2042         JAVAFX_VERSION = AccessController.doPrivileged(new PrivilegedAction<String>() {
2043             @Override
2044             public String run() {
2045                 return System.getProperty("javafx.version");
2046             }
2047         });
2048     }
2049 
2050     /**
2051      * Creates a new FXMLLoader instance.
2052      */
2053     public FXMLLoader() {
2054         this((URL)null);
2055     }
2056 
2057     /**
2058      * Creates a new FXMLLoader instance.
2059      *
2060      * @param location
2061      * @since JavaFX 2.1
2062      */
2063     public FXMLLoader(URL location) {
2064         this(location, null);
2065     }
2066 
2067     /**
2068      * Creates a new FXMLLoader instance.
2069      *
2070      * @param location
2071      * @param resources
2072      * @since JavaFX 2.1
2073      */
2074     public FXMLLoader(URL location, ResourceBundle resources) {
2075         this(location, resources, null);
2076     }
2077 
2078     /**
2079      * Creates a new FXMLLoader instance.
2080      *
2081      * @param location
2082      * @param resources
2083      * @param builderFactory
2084      * @since JavaFX 2.1
2085      */
2086     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory) {
2087         this(location, resources, builderFactory, null);
2088     }
2089 
2090     /**
2091      * Creates a new FXMLLoader instance.
2092      *
2093      * @param location
2094      * @param resources
2095      * @param builderFactory
2096      * @param controllerFactory
2097      * @since JavaFX 2.1
2098      */
2099     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2100         Callback<Class<?>, Object> controllerFactory) {
2101         this(location, resources, builderFactory, controllerFactory, Charset.forName(DEFAULT_CHARSET_NAME));
2102     }
2103 
2104     /**
2105      * Creates a new FXMLLoader instance.
2106      *
2107      * @param charset
2108      */
2109     public FXMLLoader(Charset charset) {
2110         this(null, null, null, null, charset);
2111     }
2112 
2113     /**
2114      * Creates a new FXMLLoader instance.
2115      *
2116      * @param location
2117      * @param resources
2118      * @param builderFactory
2119      * @param controllerFactory
2120      * @param charset
2121      * @since JavaFX 2.1
2122      */
2123     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2124         Callback<Class<?>, Object> controllerFactory, Charset charset) {
2125         this(location, resources, builderFactory, controllerFactory, charset,
2126             new LinkedList<FXMLLoader>());
2127     }
2128 
2129     /**
2130      * Creates a new FXMLLoader instance.
2131      *
2132      * @param location
2133      * @param resources
2134      * @param builderFactory
2135      * @param controllerFactory
2136      * @param charset
2137      * @param loaders
2138      * @since JavaFX 2.1
2139      */
2140     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2141         Callback<Class<?>, Object> controllerFactory, Charset charset,
2142         LinkedList<FXMLLoader> loaders) {
2143         setLocation(location);
2144         setResources(resources);
2145         setBuilderFactory(builderFactory);
2146         setControllerFactory(controllerFactory);
2147         setCharset(charset);
2148 
2149         this.loaders = new LinkedList(loaders);
2150     }
2151 
2152     /**
2153      * Returns the location used to resolve relative path attribute values.
2154      */
2155     public URL getLocation() {
2156         return location;
2157     }
2158 
2159     /**
2160      * Sets the location used to resolve relative path attribute values.
2161      *
2162      * @param location
2163      */
2164     public void setLocation(URL location) {
2165         this.location = location;
2166     }
2167 
2168     /**
2169      * Returns the resources used to resolve resource key attribute values.
2170      */
2171     public ResourceBundle getResources() {
2172         return resources;
2173     }
2174 
2175     /**
2176      * Sets the resources used to resolve resource key attribute values.
2177      *
2178      * @param resources
2179      */
2180     public void setResources(ResourceBundle resources) {
2181         this.resources = resources;
2182     }
2183 
2184     /**
2185      * Returns the namespace used by this loader.
2186      */
2187     public ObservableMap<String, Object> getNamespace() {
2188         return namespace;
2189     }
2190 
2191     /**
2192      * Returns the root of the object hierarchy.
2193      */
2194     @SuppressWarnings("unchecked")
2195     public <T> T getRoot() {
2196         return (T)root;
2197     }
2198 
2199     /**
2200      * Sets the root of the object hierarchy. The value passed to this method
2201      * is used as the value of the <tt>&lt;fx:root&gt;</tt> tag. This method
2202      * must be called prior to loading the document when using
2203      * <tt>&lt;fx:root&gt;</tt>.
2204      *
2205      * @param root
2206      * The root of the object hierarchy.
2207      * @since JavaFX 2.2
2208      */
2209     public void setRoot(Object root) {
2210         this.root = root;
2211     }
2212 
2213     @Override
2214     public boolean equals(Object obj) {
2215         if (obj instanceof FXMLLoader) {
2216             FXMLLoader loader = (FXMLLoader)obj;
2217             if (location == null || loader.location == null) {
2218                 return loader.location == location;
2219             }
2220             return location.toExternalForm().equals(
2221                     loader.location.toExternalForm());
2222         }
2223         return false;
2224     }
2225 
2226     private boolean isCyclic(
2227                             FXMLLoader currentLoader,
2228                             FXMLLoader node) {
2229         if (currentLoader == null) {
2230             return false;
2231         }
2232         if (currentLoader.equals(node)) {
2233             return true;
2234         }
2235         return isCyclic(currentLoader.parentLoader, node);
2236     }
2237 
2238     /**
2239      * Returns the controller associated with the root object.
2240      */
2241     @SuppressWarnings("unchecked")
2242     public <T> T getController() {
2243         return (T)controller;
2244     }
2245 
2246     /**
2247      * Sets the controller associated with the root object. The value passed to
2248      * this method is used as the value of the <tt>fx:controller</tt> attribute.
2249      * This method must be called prior to loading the document when using
2250      * controller event handlers when an <tt>fx:controller</tt> attribute is not
2251      * specified in the document.
2252      *
2253      * @param controller
2254      * The controller to associate with the root object.
2255      * @since JavaFX 2.2
2256      */
2257     public void setController(Object controller) {
2258         this.controller = controller;
2259 
2260         if (controller == null) {
2261             namespace.remove(CONTROLLER_KEYWORD);
2262         } else {
2263             namespace.put(CONTROLLER_KEYWORD, controller);
2264         }
2265 
2266         controllerAccessor.setController(controller);
2267     }
2268 
2269     /**
2270      * Returns the builder factory used by this loader.
2271      */
2272     public BuilderFactory getBuilderFactory() {
2273         return builderFactory;
2274     }
2275 
2276     /**
2277      * Sets the builder factory used by this loader.
2278      *
2279      * @param builderFactory
2280      */
2281     public void setBuilderFactory(BuilderFactory builderFactory) {
2282         this.builderFactory = builderFactory;
2283     }
2284 
2285     /**
2286      * Returns the controller factory used by this serializer.
2287      * @since JavaFX 2.1
2288      */
2289     public Callback<Class<?>, Object> getControllerFactory() {
2290         return controllerFactory;
2291     }
2292 
2293     /**
2294      * Sets the controller factory used by this serializer.
2295      *
2296      * @param controllerFactory
2297      * @since JavaFX 2.1
2298      */
2299     public void setControllerFactory(Callback<Class<?>, Object> controllerFactory) {
2300         this.controllerFactory = controllerFactory;
2301     }
2302 
2303     /**
2304      * Returns the character set used by this loader.
2305      */
2306     public Charset getCharset() {
2307         return charset;
2308     }
2309 
2310     /**
2311      * Sets the charset used by this loader.
2312      *
2313      * @param charset
2314      * @since JavaFX 2.1
2315      */
2316     public void setCharset(Charset charset) {
2317         if (charset == null) {
2318             throw new NullPointerException("charset is null.");
2319         }
2320 
2321         this.charset = charset;
2322     }
2323 
2324     /**
2325      * Returns the classloader used by this serializer.
2326      * @since JavaFX 2.1
2327      */
2328     public ClassLoader getClassLoader() {
2329         if (classLoader == null) {
2330             final SecurityManager sm = System.getSecurityManager();
2331             final Class caller = (sm != null) ?
2332                     walker.getCallerClass() :
2333                     null;
2334             return getDefaultClassLoader(caller);
2335         }
2336         return classLoader;
2337     }
2338 
2339     /**
2340      * Sets the classloader used by this serializer and clears any existing
2341      * imports
2342      *
2343      * @param classLoader
2344      * @since JavaFX 2.1
2345      */
2346     public void setClassLoader(ClassLoader classLoader) {
2347         if (classLoader == null) {
2348             throw new IllegalArgumentException();
2349         }
2350 
2351         this.classLoader = classLoader;
2352 
2353         clearImports();
2354     }
2355 
2356     /**
2357      * Returns the static load flag.
2358      *
2359      * @treatAsPrivate
2360      * @deprecated
2361      */
2362     public boolean impl_isStaticLoad() {
2363         // SB-dependency: RT-21226 has been filed to track this
2364         return staticLoad;
2365     }
2366 
2367     /**
2368      * Sets the static load flag.
2369      *
2370      * @param staticLoad
2371      *
2372      * @treatAsPrivate
2373      * @deprecated
2374      */
2375     public void impl_setStaticLoad(boolean staticLoad) {
2376         // SB-dependency: RT-21226 has been filed to track this
2377         this.staticLoad = staticLoad;
2378     }
2379 
2380     /**
2381      * Returns this loader's load listener.
2382      *
2383      * @since 9
2384      */
2385     public LoadListener getLoadListener() {
2386         // SB-dependency: RT-21228 has been filed to track this
2387         return loadListener;
2388     }
2389 
2390     /**
2391      * Sets this loader's load listener.
2392      *
2393      * @since 9
2394      */
2395     public final void setLoadListener(LoadListener loadListener) {
2396         // SB-dependency: RT-21228 has been filed to track this
2397         this.loadListener = loadListener;
2398     }
2399 
2400     /**
2401      * Loads an object hierarchy from a FXML document. The location from which
2402      * the document will be loaded must have been set by a prior call to
2403      * {@link #setLocation(URL)}.
2404      *
2405      * @return
2406      * The loaded object hierarchy.
2407      * @since JavaFX 2.1
2408      */
2409     public <T> T load() throws IOException {
2410         return loadImpl((System.getSecurityManager() != null)
2411                             ? walker.getCallerClass()
2412                             : null);
2413     }
2414 
2415     /**
2416      * Loads an object hierarchy from a FXML document.
2417      *
2418      * @param inputStream
2419      * An input stream containing the FXML data to load.
2420      *
2421      * @return
2422      * The loaded object hierarchy.
2423      */
2424     public <T> T load(InputStream inputStream) throws IOException {
2425         return loadImpl(inputStream, (System.getSecurityManager() != null)
2426                                          ? walker.getCallerClass()
2427                                          : null);
2428     }
2429 
2430     private Class<?> callerClass;
2431 
2432     private <T> T loadImpl(final Class<?> callerClass) throws IOException {
2433         if (location == null) {
2434             throw new IllegalStateException("Location is not set.");
2435         }
2436 
2437         InputStream inputStream = null;
2438         T value;
2439         try {
2440             inputStream = location.openStream();
2441             value = loadImpl(inputStream, callerClass);
2442         } finally {
2443             if (inputStream != null) {
2444                 inputStream.close();
2445             }
2446         }
2447 
2448         return value;
2449     }
2450 
2451     @SuppressWarnings({ "dep-ann", "unchecked" })
2452     private <T> T loadImpl(InputStream inputStream,
2453                            Class<?> callerClass) throws IOException {
2454         if (inputStream == null) {
2455             throw new NullPointerException("inputStream is null.");
2456         }
2457 
2458         this.callerClass = callerClass;
2459         controllerAccessor.setCallerClass(callerClass);
2460         try {
2461             clearImports();
2462 
2463             // Initialize the namespace
2464             namespace.put(LOCATION_KEY, location);
2465             namespace.put(RESOURCES_KEY, resources);
2466 
2467             // Clear the script engine
2468             scriptEngine = null;
2469 
2470             // Create the parser
2471             try {
2472                 XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
2473                 xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true);
2474 
2475                 // Some stream readers incorrectly report an empty string as the prefix
2476                 // for the default namespace; correct this as needed
2477                 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset);
2478                 xmlStreamReader = new StreamReaderDelegate(xmlInputFactory.createXMLStreamReader(inputStreamReader)) {
2479                     @Override
2480                     public String getPrefix() {
2481                         String prefix = super.getPrefix();
2482 
2483                         if (prefix != null
2484                             && prefix.length() == 0) {
2485                             prefix = null;
2486                         }
2487 
2488                         return prefix;
2489                     }
2490 
2491                     @Override
2492                     public String getAttributePrefix(int index) {
2493                         String attributePrefix = super.getAttributePrefix(index);
2494 
2495                         if (attributePrefix != null
2496                             && attributePrefix.length() == 0) {
2497                             attributePrefix = null;
2498                         }
2499 
2500                         return attributePrefix;
2501                     }
2502                 };
2503             } catch (XMLStreamException exception) {
2504                 throw constructLoadException(exception);
2505             }
2506 
2507             // Push this loader onto the stack
2508             loaders.push(this);
2509 
2510             // Parse the XML stream
2511             try {
2512                 while (xmlStreamReader.hasNext()) {
2513                     int event = xmlStreamReader.next();
2514 
2515                     switch (event) {
2516                         case XMLStreamConstants.PROCESSING_INSTRUCTION: {
2517                             processProcessingInstruction();
2518                             break;
2519                         }
2520 
2521                         case XMLStreamConstants.COMMENT: {
2522                             processComment();
2523                             break;
2524                         }
2525 
2526                         case XMLStreamConstants.START_ELEMENT: {
2527                             processStartElement();
2528                             break;
2529                         }
2530 
2531                         case XMLStreamConstants.END_ELEMENT: {
2532                             processEndElement();
2533                             break;
2534                         }
2535 
2536                         case XMLStreamConstants.CHARACTERS: {
2537                             processCharacters();
2538                             break;
2539                         }
2540                     }
2541                 }
2542             } catch (XMLStreamException exception) {
2543                 throw constructLoadException(exception);
2544             }
2545 
2546             if (controller != null) {
2547                 if (controller instanceof Initializable) {
2548                     ((Initializable)controller).initialize(location, resources);
2549                 } else {
2550                     // Inject controller fields
2551                     Map<String, List<Field>> controllerFields =
2552                             controllerAccessor.getControllerFields();
2553 
2554                     injectFields(LOCATION_KEY, location);
2555 
2556                     injectFields(RESOURCES_KEY, resources);
2557 
2558                     // Initialize the controller
2559                     Method initializeMethod = controllerAccessor
2560                                                   .getControllerMethods()
2561                                                   .get(SupportedType.PARAMETERLESS)
2562                                                   .get(INITIALIZE_METHOD_NAME);
2563 
2564                     if (initializeMethod != null) {
2565                         try {
2566                             MethodUtil.invoke(initializeMethod, controller, new Object [] {});
2567                         } catch (IllegalAccessException exception) {
2568                             // TODO Throw when Initializable is deprecated/removed
2569                             // throw constructLoadException(exception);
2570                         } catch (InvocationTargetException exception) {
2571                             throw constructLoadException(exception);
2572                         }
2573                     }
2574                 }
2575             }
2576         } catch (final LoadException exception) {
2577             throw exception;
2578         } catch (final Exception exception) {
2579             throw constructLoadException(exception);
2580         } finally {
2581             controllerAccessor.setCallerClass(null);
2582             // Clear controller accessor caches
2583             controllerAccessor.reset();
2584             // Clear the parser
2585             xmlStreamReader = null;
2586         }
2587 
2588         return (T)root;
2589     }
2590 
2591     private void clearImports() {
2592         packages.clear();
2593         classes.clear();
2594     }
2595 
2596     private LoadException constructLoadException(String message){
2597         return new LoadException(message + constructFXMLTrace());
2598     }
2599 
2600     private LoadException constructLoadException(Throwable cause) {
2601         return new LoadException(constructFXMLTrace(), cause);
2602     }
2603 
2604     private LoadException constructLoadException(String message, Throwable cause){
2605         return new LoadException(message + constructFXMLTrace(), cause);
2606     }
2607 
2608     private String constructFXMLTrace() {
2609         StringBuilder messageBuilder = new StringBuilder("\n");
2610 
2611         for (FXMLLoader loader : loaders) {
2612             messageBuilder.append(loader.location != null ? loader.location.getPath() : "unknown path");
2613 
2614             if (loader.current != null) {
2615                 messageBuilder.append(":");
2616                 messageBuilder.append(loader.impl_getLineNumber());
2617             }
2618 
2619             messageBuilder.append("\n");
2620         }
2621         return messageBuilder.toString();
2622     }
2623 
2624     /**
2625      * Returns the current line number.
2626      *
2627      * @treatAsPrivate
2628      * @deprecated
2629      * @since JavaFX 2.2
2630      */
2631     public int impl_getLineNumber() {
2632         return xmlStreamReader.getLocation().getLineNumber();
2633     }
2634 
2635     /**
2636      * Returns the current parse trace.
2637      *
2638      * @treatAsPrivate
2639      * @deprecated
2640      * @since JavaFX 2.1
2641      */
2642     // SB-dependency: RT-21475 has been filed to track this
2643     public ParseTraceElement[] impl_getParseTrace() {
2644         ParseTraceElement[] parseTrace = new ParseTraceElement[loaders.size()];
2645 
2646         int i = 0;
2647         for (FXMLLoader loader : loaders) {
2648             parseTrace[i++] = new ParseTraceElement(loader.location, (loader.current != null) ?
2649                 loader.impl_getLineNumber() : -1);
2650         }
2651 
2652         return parseTrace;
2653     }
2654 
2655     private void processProcessingInstruction() throws LoadException {
2656         String piTarget = xmlStreamReader.getPITarget().trim();
2657 
2658         if (piTarget.equals(LANGUAGE_PROCESSING_INSTRUCTION)) {
2659             processLanguage();
2660         } else if (piTarget.equals(IMPORT_PROCESSING_INSTRUCTION)) {
2661             processImport();
2662         }
2663     }
2664 
2665     private void processLanguage() throws LoadException {
2666         if (scriptEngine != null) {
2667             throw constructLoadException("Page language already set.");
2668         }
2669 
2670         String language = xmlStreamReader.getPIData();
2671 
2672         if (loadListener != null) {
2673             loadListener.readLanguageProcessingInstruction(language);
2674         }
2675 
2676         if (!staticLoad) {
2677             ScriptEngineManager scriptEngineManager = getScriptEngineManager();
2678             scriptEngine = scriptEngineManager.getEngineByName(language);
2679         }
2680     }
2681 
2682     private void processImport() throws LoadException {
2683         String target = xmlStreamReader.getPIData().trim();
2684 
2685         if (loadListener != null) {
2686             loadListener.readImportProcessingInstruction(target);
2687         }
2688 
2689         if (target.endsWith(".*")) {
2690             importPackage(target.substring(0, target.length() - 2));
2691         } else {
2692             importClass(target);
2693         }
2694     }
2695 
2696     private void processComment() throws LoadException {
2697         if (loadListener != null) {
2698             loadListener.readComment(xmlStreamReader.getText());
2699         }
2700     }
2701 
2702     private void processStartElement() throws IOException {
2703         // Create the element
2704         createElement();
2705 
2706         // Process the start tag
2707         current.processStartElement();
2708 
2709         // Set the root value
2710         if (root == null) {
2711             root = current.value;
2712         }
2713     }
2714 
2715     private void createElement() throws IOException {
2716         String prefix = xmlStreamReader.getPrefix();
2717         String localName = xmlStreamReader.getLocalName();
2718 
2719         if (prefix == null) {
2720             int i = localName.lastIndexOf('.');
2721 
2722             if (Character.isLowerCase(localName.charAt(i + 1))) {
2723                 String name = localName.substring(i + 1);
2724 
2725                 if (i == -1) {
2726                     // This is an instance property
2727                     if (loadListener != null) {
2728                         loadListener.beginPropertyElement(name, null);
2729                     }
2730 
2731                     current = new PropertyElement(name, null);
2732                 } else {
2733                     // This is a static property
2734                     Class<?> sourceType = getType(localName.substring(0, i));
2735 
2736                     if (sourceType != null) {
2737                         if (loadListener != null) {
2738                             loadListener.beginPropertyElement(name, sourceType);
2739                         }
2740 
2741                         current = new PropertyElement(name, sourceType);
2742                     } else if (staticLoad) {
2743                         // The source type was not recognized
2744                         if (loadListener != null) {
2745                             loadListener.beginUnknownStaticPropertyElement(localName);
2746                         }
2747 
2748                         current = new UnknownStaticPropertyElement();
2749                     } else {
2750                         throw constructLoadException(localName + " is not a valid property.");
2751                     }
2752                 }
2753             } else {
2754                 if (current == null && root != null) {
2755                     throw constructLoadException("Root value already specified.");
2756                 }
2757 
2758                 Class<?> type = getType(localName);
2759 
2760                 if (type != null) {
2761                     if (loadListener != null) {
2762                         loadListener.beginInstanceDeclarationElement(type);
2763                     }
2764 
2765                     current = new InstanceDeclarationElement(type);
2766                 } else if (staticLoad) {
2767                     // The type was not recognized
2768                     if (loadListener != null) {
2769                         loadListener.beginUnknownTypeElement(localName);
2770                     }
2771 
2772                     current = new UnknownTypeElement();
2773                 } else {
2774                     throw constructLoadException(localName + " is not a valid type.");
2775                 }
2776             }
2777         } else if (prefix.equals(FX_NAMESPACE_PREFIX)) {
2778             if (localName.equals(INCLUDE_TAG)) {
2779                 if (loadListener != null) {
2780                     loadListener.beginIncludeElement();
2781                 }
2782 
2783                 current = new IncludeElement();
2784             } else if (localName.equals(REFERENCE_TAG)) {
2785                 if (loadListener != null) {
2786                     loadListener.beginReferenceElement();
2787                 }
2788 
2789                 current = new ReferenceElement();
2790             } else if (localName.equals(COPY_TAG)) {
2791                 if (loadListener != null) {
2792                     loadListener.beginCopyElement();
2793                 }
2794 
2795                 current = new CopyElement();
2796             } else if (localName.equals(ROOT_TAG)) {
2797                 if (loadListener != null) {
2798                     loadListener.beginRootElement();
2799                 }
2800 
2801                 current = new RootElement();
2802             } else if (localName.equals(SCRIPT_TAG)) {
2803                 if (loadListener != null) {
2804                     loadListener.beginScriptElement();
2805                 }
2806 
2807                 current = new ScriptElement();
2808             } else if (localName.equals(DEFINE_TAG)) {
2809                 if (loadListener != null) {
2810                     loadListener.beginDefineElement();
2811                 }
2812 
2813                 current = new DefineElement();
2814             } else {
2815                 throw constructLoadException(prefix + ":" + localName + " is not a valid element.");
2816             }
2817         } else {
2818             throw constructLoadException("Unexpected namespace prefix: " + prefix + ".");
2819         }
2820     }
2821 
2822     private void processEndElement() throws IOException {
2823         current.processEndElement();
2824 
2825         if (loadListener != null) {
2826             loadListener.endElement(current.value);
2827         }
2828 
2829         // Move up the stack
2830         current = current.parent;
2831     }
2832 
2833     private void processCharacters() throws IOException {
2834         // Process the characters
2835         if (!xmlStreamReader.isWhiteSpace()) {
2836             current.processCharacters();
2837         }
2838     }
2839 
2840     private void importPackage(String name) throws LoadException {
2841         packages.add(name);
2842     }
2843 
2844     private void importClass(String name) throws LoadException {
2845         try {
2846             loadType(name, true);
2847         } catch (ClassNotFoundException exception) {
2848             throw constructLoadException(exception);
2849         }
2850     }
2851 
2852     private Class<?> getType(String name) throws LoadException {
2853         Class<?> type = null;
2854 
2855         if (Character.isLowerCase(name.charAt(0))) {
2856             // This is a fully-qualified class name
2857             try {
2858                 type = loadType(name, false);
2859             } catch (ClassNotFoundException exception) {
2860                 // No-op
2861             }
2862         } else {
2863             // This is an unqualified class name
2864             type = classes.get(name);
2865 
2866             if (type == null) {
2867                 // The class has not been loaded yet; look it up
2868                 for (String packageName : packages) {
2869                     try {
2870                         type = loadTypeForPackage(packageName, name);
2871                     } catch (ClassNotFoundException exception) {
2872                         // No-op
2873                     }
2874 
2875                     if (type != null) {
2876                         break;
2877                     }
2878                 }
2879 
2880                 if (type != null) {
2881                     classes.put(name, type);
2882                 }
2883             }
2884         }
2885 
2886         return type;
2887     }
2888 
2889     private Class<?> loadType(String name, boolean cache) throws ClassNotFoundException {
2890         int i = name.indexOf('.');
2891         int n = name.length();
2892         while (i != -1
2893             && i < n
2894             && Character.isLowerCase(name.charAt(i + 1))) {
2895             i = name.indexOf('.', i + 1);
2896         }
2897 
2898         if (i == -1 || i == n) {
2899             throw new ClassNotFoundException();
2900         }
2901 
2902         String packageName = name.substring(0, i);
2903         String className = name.substring(i + 1);
2904 
2905         Class<?> type = loadTypeForPackage(packageName, className);
2906 
2907         if (cache) {
2908             classes.put(className, type);
2909         }
2910 
2911         return type;
2912     }
2913 
2914     // TODO Rename to loadType() when deprecated static version is removed
2915     private Class<?> loadTypeForPackage(String packageName, String className) throws ClassNotFoundException {
2916         return getClassLoader().loadClass(packageName + "." + className.replace('.', '$'));
2917     }
2918 
2919     private static enum SupportedType {
2920         PARAMETERLESS {
2921 
2922             @Override
2923             protected boolean methodIsOfType(Method m) {
2924                 return m.getParameterTypes().length == 0;
2925             }
2926 
2927         },
2928         EVENT {
2929 
2930             @Override
2931             protected boolean methodIsOfType(Method m) {
2932                 return m.getParameterTypes().length == 1 &&
2933                         Event.class.isAssignableFrom(m.getParameterTypes()[0]);
2934             }
2935 
2936         },
2937         LIST_CHANGE_LISTENER {
2938 
2939             @Override
2940             protected boolean methodIsOfType(Method m) {
2941                 return m.getParameterTypes().length == 1 &&
2942                         m.getParameterTypes()[0].equals(ListChangeListener.Change.class);
2943             }
2944 
2945         },
2946         MAP_CHANGE_LISTENER {
2947 
2948             @Override
2949             protected boolean methodIsOfType(Method m) {
2950                 return m.getParameterTypes().length == 1 &&
2951                         m.getParameterTypes()[0].equals(MapChangeListener.Change.class);
2952             }
2953 
2954         },
2955         SET_CHANGE_LISTENER {
2956 
2957             @Override
2958             protected boolean methodIsOfType(Method m) {
2959                 return m.getParameterTypes().length == 1 &&
2960                         m.getParameterTypes()[0].equals(SetChangeListener.Change.class);
2961             }
2962 
2963         },
2964         PROPERTY_CHANGE_LISTENER {
2965 
2966             @Override
2967             protected boolean methodIsOfType(Method m) {
2968                 return m.getParameterTypes().length == 3 &&
2969                         ObservableValue.class.isAssignableFrom(m.getParameterTypes()[0])
2970                         && m.getParameterTypes()[1].equals(m.getParameterTypes()[2]);
2971             }
2972 
2973         };
2974 
2975         protected abstract boolean methodIsOfType(Method m);
2976     }
2977 
2978     private static SupportedType toSupportedType(Method m) {
2979         for (SupportedType t : SupportedType.values()) {
2980             if (t.methodIsOfType(m)) {
2981                 return t;
2982             }
2983         }
2984         return null;
2985     }
2986 
2987     private ScriptEngineManager getScriptEngineManager() {
2988         if (scriptEngineManager == null) {
2989             scriptEngineManager = new javax.script.ScriptEngineManager();
2990             scriptEngineManager.setBindings(new SimpleBindings(namespace));
2991         }
2992 
2993         return scriptEngineManager;
2994     }
2995 
2996     /**
2997      * Loads a type using the default class loader.
2998      *
2999      * @param packageName
3000      * @param className
3001      *
3002      * @deprecated
3003      * This method now delegates to {@link #getDefaultClassLoader()}.
3004      */
3005     public static Class<?> loadType(String packageName, String className) throws ClassNotFoundException {
3006         return loadType(packageName + "." + className.replace('.', '$'));
3007     }
3008 
3009     /**
3010      * Loads a type using the default class loader.
3011      *
3012      * @param className
3013      *
3014      * @deprecated
3015      * This method now delegates to {@link #getDefaultClassLoader()}.
3016      */
3017     public static Class<?> loadType(String className) throws ClassNotFoundException {
3018         ReflectUtil.checkPackageAccess(className);
3019         return Class.forName(className, true, getDefaultClassLoader());
3020     }
3021 
3022     private static boolean needsClassLoaderPermissionCheck(ClassLoader from, ClassLoader to) {
3023         if (from == to) {
3024             return false;
3025         }
3026         if (from == null) {
3027             return false;
3028         }
3029         if (to == null) {
3030             return true;
3031         }
3032         ClassLoader acl = to;
3033         do {
3034             acl = acl.getParent();
3035             if (from == acl) {
3036                 return false;
3037             }
3038         } while (acl != null);
3039         return true;
3040     }
3041 
3042     private static ClassLoader getDefaultClassLoader(Class caller) {
3043         if (defaultClassLoader == null) {
3044             final SecurityManager sm = System.getSecurityManager();
3045             if (sm != null) {
3046                 final ClassLoader callerClassLoader = (caller != null) ?
3047                         caller.getClassLoader() :
3048                         null;
3049                 if (needsClassLoaderPermissionCheck(callerClassLoader, FXMLLoader.class.getClassLoader())) {
3050                     sm.checkPermission(GET_CLASSLOADER_PERMISSION);
3051                 }
3052             }
3053             return Thread.currentThread().getContextClassLoader();
3054         }
3055         return defaultClassLoader;
3056     }
3057 
3058     /**
3059      * Returns the default class loader.
3060      * @since JavaFX 2.1
3061      */
3062     public static ClassLoader getDefaultClassLoader() {
3063         final SecurityManager sm = System.getSecurityManager();
3064         final Class caller = (sm != null) ?
3065                 walker.getCallerClass() :
3066                 null;
3067         return getDefaultClassLoader(caller);
3068     }
3069 
3070     /**
3071      * Sets the default class loader.
3072      *
3073      * @param defaultClassLoader
3074      * The default class loader to use when loading classes.
3075      * @since JavaFX 2.1
3076      */
3077     public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {
3078         if (defaultClassLoader == null) {
3079             throw new NullPointerException();
3080         }
3081         final SecurityManager sm = System.getSecurityManager();
3082         if (sm != null) {
3083             sm.checkPermission(MODIFY_FXML_CLASS_LOADER_PERMISSION);
3084         }
3085 
3086         FXMLLoader.defaultClassLoader = defaultClassLoader;
3087     }
3088 
3089     /**
3090      * Loads an object hierarchy from a FXML document.
3091      *
3092      * @param location
3093      */
3094     public static <T> T load(URL location) throws IOException {
3095         return loadImpl(location, (System.getSecurityManager() != null)
3096                                       ? walker.getCallerClass()
3097                                       : null);
3098     }
3099 
3100     private static <T> T loadImpl(URL location, Class<?> callerClass)
3101             throws IOException {
3102         return loadImpl(location, null, callerClass);
3103     }
3104 
3105     /**
3106      * Loads an object hierarchy from a FXML document.
3107      *
3108      * @param location
3109      * @param resources
3110      */
3111     public static <T> T load(URL location, ResourceBundle resources)
3112                                      throws IOException {
3113         return loadImpl(location, resources,
3114                         (System.getSecurityManager() != null)
3115                             ? walker.getCallerClass()
3116                             : null);
3117     }
3118 
3119     private static <T> T loadImpl(URL location, ResourceBundle resources,
3120                                   Class<?> callerClass) throws IOException {
3121         return loadImpl(location, resources,  null,
3122                         callerClass);
3123     }
3124 
3125     /**
3126      * Loads an object hierarchy from a FXML document.
3127      *
3128      * @param location
3129      * @param resources
3130      * @param builderFactory
3131      */
3132     public static <T> T load(URL location, ResourceBundle resources,
3133                              BuilderFactory builderFactory)
3134                                      throws IOException {
3135         return loadImpl(location, resources, builderFactory,
3136                         (System.getSecurityManager() != null)
3137                             ? walker.getCallerClass()
3138                             : null);
3139     }
3140 
3141     private static <T> T loadImpl(URL location, ResourceBundle resources,
3142                                   BuilderFactory builderFactory,
3143                                   Class<?> callerClass) throws IOException {
3144         return loadImpl(location, resources, builderFactory, null, callerClass);
3145     }
3146 
3147     /**
3148      * Loads an object hierarchy from a FXML document.
3149      *
3150      * @param location
3151      * @param resources
3152      * @param builderFactory
3153      * @param controllerFactory
3154      * @since JavaFX 2.1
3155      */
3156     public static <T> T load(URL location, ResourceBundle resources,
3157                              BuilderFactory builderFactory,
3158                              Callback<Class<?>, Object> controllerFactory)
3159                                      throws IOException {
3160         return loadImpl(location, resources, builderFactory, controllerFactory,
3161                         (System.getSecurityManager() != null)
3162                             ? walker.getCallerClass()
3163                             : null);
3164     }
3165 
3166     private static <T> T loadImpl(URL location, ResourceBundle resources,
3167                                   BuilderFactory builderFactory,
3168                                   Callback<Class<?>, Object> controllerFactory,
3169                                   Class<?> callerClass) throws IOException {
3170         return loadImpl(location, resources, builderFactory, controllerFactory,
3171                         Charset.forName(DEFAULT_CHARSET_NAME), callerClass);
3172     }
3173 
3174     /**
3175      * Loads an object hierarchy from a FXML document.
3176      *
3177      * @param location
3178      * @param resources
3179      * @param builderFactory
3180      * @param controllerFactory
3181      * @param charset
3182      * @since JavaFX 2.1
3183      */
3184     public static <T> T load(URL location, ResourceBundle resources,
3185                              BuilderFactory builderFactory,
3186                              Callback<Class<?>, Object> controllerFactory,
3187                              Charset charset) throws IOException {
3188         return loadImpl(location, resources, builderFactory, controllerFactory,
3189                         charset,
3190                         (System.getSecurityManager() != null)
3191                             ? walker.getCallerClass()
3192                             : null);
3193     }
3194 
3195     private static <T> T loadImpl(URL location, ResourceBundle resources,
3196                                   BuilderFactory builderFactory,
3197                                   Callback<Class<?>, Object> controllerFactory,
3198                                   Charset charset, Class<?> callerClass)
3199                                           throws IOException {
3200         if (location == null) {
3201             throw new NullPointerException("Location is required.");
3202         }
3203 
3204         FXMLLoader fxmlLoader =
3205                 new FXMLLoader(location, resources, builderFactory,
3206                                controllerFactory, charset);
3207 
3208         return fxmlLoader.<T>loadImpl(callerClass);
3209     }
3210 
3211     /**
3212      * Utility method for comparing two JavaFX version strings (such as 2.2.5, 8.0.0-ea)
3213      * @param rtVer String representation of JavaFX runtime version, including - or _ appendix
3214      * @param nsVer String representation of JavaFX version to compare against runtime version
3215      * @return number &lt; 0 if runtime version is lower, 0 when both versions are the same,
3216      *          number &gt; 0 if runtime is higher version
3217      */
3218     static int compareJFXVersions(String rtVer, String nsVer) {
3219 
3220         int retVal = 0;
3221 
3222         if (rtVer == null || "".equals(rtVer) ||
3223             nsVer == null || "".equals(nsVer)) {
3224             return retVal;
3225         }
3226 
3227         if (rtVer.equals(nsVer)) {
3228             return retVal;
3229         }
3230 
3231         // version string can contain '-'
3232         int dashIndex = rtVer.indexOf("-");
3233         if (dashIndex > 0) {
3234             rtVer = rtVer.substring(0, dashIndex);
3235         }
3236 
3237         // or "_"
3238         int underIndex = rtVer.indexOf("_");
3239         if (underIndex > 0) {
3240             rtVer = rtVer.substring(0, underIndex);
3241         }
3242 
3243         // do not try to compare if the string is not valid version format
3244         if (!Pattern.matches("^(\\d+)(\\.\\d+)*$", rtVer) ||
3245             !Pattern.matches("^(\\d+)(\\.\\d+)*$", nsVer)) {
3246             return retVal;
3247         }
3248 
3249         StringTokenizer nsVerTokenizer = new StringTokenizer(nsVer, ".");
3250         StringTokenizer rtVerTokenizer = new StringTokenizer(rtVer, ".");
3251         int nsDigit = 0, rtDigit = 0;
3252         boolean rtVerEnd = false;
3253 
3254         while (nsVerTokenizer.hasMoreTokens() && retVal == 0) {
3255             nsDigit = Integer.parseInt(nsVerTokenizer.nextToken());
3256             if (rtVerTokenizer.hasMoreTokens()) {
3257                 rtDigit = Integer.parseInt(rtVerTokenizer.nextToken());
3258                 retVal = rtDigit - nsDigit;
3259             } else {
3260                 rtVerEnd = true;
3261                 break;
3262             }
3263         }
3264 
3265         if (rtVerTokenizer.hasMoreTokens() && retVal == 0) {
3266             rtDigit = Integer.parseInt(rtVerTokenizer.nextToken());
3267             if (rtDigit > 0) {
3268                 retVal = 1;
3269             }
3270         }
3271 
3272         if (rtVerEnd) {
3273             if (nsDigit > 0) {
3274                 retVal = -1;
3275             } else {
3276                 while (nsVerTokenizer.hasMoreTokens()) {
3277                     nsDigit = Integer.parseInt(nsVerTokenizer.nextToken());
3278                     if (nsDigit > 0) {
3279                         retVal = -1;
3280                         break;
3281                     }
3282                 }
3283             }
3284         }
3285 
3286         return retVal;
3287     }
3288 
3289     private static void checkClassLoaderPermission() {
3290         final SecurityManager securityManager = System.getSecurityManager();
3291         if (securityManager != null) {
3292             securityManager.checkPermission(MODIFY_FXML_CLASS_LOADER_PERMISSION);
3293         }
3294     }
3295 
3296     private final ControllerAccessor controllerAccessor =
3297             new ControllerAccessor();
3298 
3299     private static final class ControllerAccessor {
3300         private static final int PUBLIC = 1;
3301         private static final int PROTECTED = 2;
3302         private static final int PACKAGE = 4;
3303         private static final int PRIVATE = 8;
3304         private static final int INITIAL_CLASS_ACCESS =
3305                 PUBLIC | PROTECTED | PACKAGE | PRIVATE;
3306         private static final int INITIAL_MEMBER_ACCESS =
3307                 PUBLIC | PROTECTED | PACKAGE | PRIVATE;
3308 
3309         private static final int METHODS = 0;
3310         private static final int FIELDS = 1;
3311 
3312         private Object controller;
3313         private ClassLoader callerClassLoader;
3314 
3315         private Map<String, List<Field>> controllerFields;
3316         private Map<SupportedType, Map<String, Method>> controllerMethods;
3317 
3318         void setController(final Object controller) {
3319             if (this.controller != controller) {
3320                 this.controller = controller;
3321                 reset();
3322             }
3323         }
3324 
3325         void setCallerClass(final Class<?> callerClass) {
3326             final ClassLoader newCallerClassLoader =
3327                     (callerClass != null) ? callerClass.getClassLoader()
3328                                           : null;
3329             if (callerClassLoader != newCallerClassLoader) {
3330                 callerClassLoader = newCallerClassLoader;
3331                 reset();
3332             }
3333         }
3334 
3335         void reset() {
3336             controllerFields = null;
3337             controllerMethods = null;
3338         }
3339 
3340         Map<String, List<Field>> getControllerFields() {
3341             if (controllerFields == null) {
3342                 controllerFields = new HashMap<>();
3343 
3344                 if (callerClassLoader == null) {
3345                     // allow null class loader only with permission check
3346                     checkClassLoaderPermission();
3347                 }
3348 
3349                 addAccessibleMembers(controller.getClass(),
3350                                      INITIAL_CLASS_ACCESS,
3351                                      INITIAL_MEMBER_ACCESS,
3352                                      FIELDS);
3353             }
3354 
3355             return controllerFields;
3356         }
3357 
3358         Map<SupportedType, Map<String, Method>> getControllerMethods() {
3359             if (controllerMethods == null) {
3360                 controllerMethods = new EnumMap<>(SupportedType.class);
3361                 for (SupportedType t: SupportedType.values()) {
3362                     controllerMethods.put(t, new HashMap<String, Method>());
3363                 }
3364 
3365                 if (callerClassLoader == null) {
3366                     // allow null class loader only with permission check
3367                     checkClassLoaderPermission();
3368                 }
3369 
3370                 addAccessibleMembers(controller.getClass(),
3371                                      INITIAL_CLASS_ACCESS,
3372                                      INITIAL_MEMBER_ACCESS,
3373                                      METHODS);
3374             }
3375 
3376             return controllerMethods;
3377         }
3378 
3379         private void addAccessibleMembers(final Class<?> type,
3380                                           final int prevAllowedClassAccess,
3381                                           final int prevAllowedMemberAccess,
3382                                           final int membersType) {
3383             if (type == Object.class) {
3384                 return;
3385             }
3386 
3387             int allowedClassAccess = prevAllowedClassAccess;
3388             int allowedMemberAccess = prevAllowedMemberAccess;
3389             if ((callerClassLoader != null)
3390                     && (type.getClassLoader() != callerClassLoader)) {
3391                 // restrict further access
3392                 allowedClassAccess &= PUBLIC;
3393                 allowedMemberAccess &= PUBLIC;
3394             }
3395 
3396             final int classAccess = getAccess(type.getModifiers());
3397             if ((classAccess & allowedClassAccess) == 0) {
3398                 // we are done
3399                 return;
3400             }
3401 
3402             ReflectUtil.checkPackageAccess(type);
3403 
3404             addAccessibleMembers(type.getSuperclass(),
3405                                  allowedClassAccess,
3406                                  allowedMemberAccess,
3407                                  membersType);
3408 
3409             final int finalAllowedMemberAccess = allowedMemberAccess;
3410             AccessController.doPrivileged(
3411                     new PrivilegedAction<Void>() {
3412                         @Override
3413                         public Void run() {
3414                             if (membersType == FIELDS) {
3415                                 addAccessibleFields(type,
3416                                                     finalAllowedMemberAccess);
3417                             } else {
3418                                 addAccessibleMethods(type,
3419                                                      finalAllowedMemberAccess);
3420                             }
3421 
3422                             return null;
3423                         }
3424                     });
3425         }
3426 
3427         private void addAccessibleFields(final Class<?> type,
3428                                          final int allowedMemberAccess) {
3429             final boolean isPublicType = Modifier.isPublic(type.getModifiers());
3430 
3431             final Field[] fields = type.getDeclaredFields();
3432             for (int i = 0; i < fields.length; ++i) {
3433                 final Field field = fields[i];
3434                 final int memberModifiers = field.getModifiers();
3435 
3436                 if (((memberModifiers & (Modifier.STATIC
3437                                              | Modifier.FINAL)) != 0)
3438                         || ((getAccess(memberModifiers) & allowedMemberAccess)
3439                                 == 0)) {
3440                     continue;
3441                 }
3442 
3443                 if (!isPublicType || !Modifier.isPublic(memberModifiers)) {
3444                     if (field.getAnnotation(FXML.class) == null) {
3445                         // no fxml annotation on a non-public field
3446                         continue;
3447                     }
3448 
3449                     // Ensure that the field is accessible
3450                     field.setAccessible(true);
3451                 }
3452 
3453                 List<Field> list = controllerFields.get(field.getName());
3454                 if (list == null) {
3455                     list = new ArrayList<>(1);
3456                     controllerFields.put(field.getName(), list);
3457                 }
3458                 list.add(field);
3459 
3460             }
3461         }
3462 
3463         private void addAccessibleMethods(final Class<?> type,
3464                                           final int allowedMemberAccess) {
3465             final boolean isPublicType = Modifier.isPublic(type.getModifiers());
3466 
3467             final Method[] methods = type.getDeclaredMethods();
3468             for (int i = 0; i < methods.length; ++i) {
3469                 final Method method = methods[i];
3470                 final int memberModifiers = method.getModifiers();
3471 
3472                 if (((memberModifiers & (Modifier.STATIC
3473                                              | Modifier.NATIVE)) != 0)
3474                         || ((getAccess(memberModifiers) & allowedMemberAccess)
3475                                 == 0)) {
3476                     continue;
3477                 }
3478 
3479                 if (!isPublicType || !Modifier.isPublic(memberModifiers)) {
3480                     if (method.getAnnotation(FXML.class) == null) {
3481                         // no fxml annotation on a non-public method
3482                         continue;
3483                     }
3484 
3485                     // Ensure that the method is accessible
3486                     method.setAccessible(true);
3487                 }
3488 
3489                 // Add this method to the map if:
3490                 // a) it is the initialize() method, or
3491                 // b) it takes a single event argument, or
3492                 // c) it takes no arguments and a handler with this
3493                 //    name has not already been defined
3494                 final String methodName = method.getName();
3495                 final SupportedType convertedType;
3496 
3497                 if ((convertedType = toSupportedType(method)) != null) {
3498                     controllerMethods.get(convertedType)
3499                                      .put(methodName, method);
3500                 }
3501             }
3502         }
3503 
3504         private static int getAccess(final int fullModifiers) {
3505             final int untransformedAccess =
3506                     fullModifiers & (Modifier.PRIVATE | Modifier.PROTECTED
3507                                                       | Modifier.PUBLIC);
3508 
3509             switch (untransformedAccess) {
3510                 case Modifier.PUBLIC:
3511                     return PUBLIC;
3512 
3513                 case Modifier.PROTECTED:
3514                     return PROTECTED;
3515 
3516                 case Modifier.PRIVATE:
3517                     return PRIVATE;
3518 
3519                 default:
3520                     return PACKAGE;
3521             }
3522         }
3523     }
3524 }