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