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