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