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