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