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