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