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