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