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