1 /*
   2  * Copyright (c) 2010, 2018, 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 
 622                             eventHandler = new ScriptEventHandler(handlerName, scriptEngine);
 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 
1561                     InputStreamReader scriptReader = null;
1562                     try {
1563                         scriptReader = new InputStreamReader(location.openStream(), charset);
1564                         engine.eval(scriptReader);
1565                     } catch(ScriptException exception) {
1566                         exception.printStackTrace();
1567                     } finally {
1568                         if (scriptReader != null) {
1569                             scriptReader.close();
1570                         }
1571                     }
1572                 } catch (IOException exception) {
1573                     throw constructLoadException(exception);
1574                 }
1575             }
1576         }
1577 
1578         @Override
1579         public void processEndElement() throws IOException {
1580             super.processEndElement();
1581 
1582             if (value != null && !staticLoad) {
1583                 // Evaluate the script
1584                 try {
1585                     scriptEngine.eval((String)value);
1586                 } catch (ScriptException exception) {
1587                     System.err.println(exception.getMessage());
1588                 }
1589             }
1590         }
1591 
1592         @Override
1593         public void processCharacters() throws LoadException {
1594             if (source != null) {
1595                 throw constructLoadException("Script source already specified.");
1596             }
1597 
1598             if (scriptEngine == null && !staticLoad) {
1599                 throw constructLoadException("Page language not specified.");
1600             }
1601 
1602             updateValue(xmlStreamReader.getText());
1603         }
1604 
1605         @Override
1606         public void processAttribute(String prefix, String localName, String value)
1607             throws IOException {
1608             if (prefix == null
1609                 && localName.equals(SCRIPT_SOURCE_ATTRIBUTE)) {
1610                 if (loadListener != null) {
1611                     loadListener.readInternalAttribute(localName, value);
1612                 }
1613 
1614                 source = value;
1615             } else if (localName.equals(SCRIPT_CHARSET_ATTRIBUTE)) {
1616                 if (loadListener != null) {
1617                     loadListener.readInternalAttribute(localName, value);
1618                 }
1619 
1620                 charset = Charset.forName(value);
1621             } else {
1622                 throw constructLoadException(prefix == null ? localName : prefix + ":" + localName
1623                     + " is not a valid attribute.");
1624             }
1625         }
1626     }
1627 
1628     // Element representing a define block
1629     private class DefineElement extends Element {
1630         @Override
1631         public boolean isCollection() {
1632             return true;
1633         }
1634 
1635         @Override
1636         public void add(Object element) {
1637             // No-op
1638         }
1639 
1640         @Override
1641         public void processAttribute(String prefix, String localName, String value)
1642             throws LoadException{
1643             throw constructLoadException("Element does not support attributes.");
1644         }
1645     }
1646 
1647     // Class representing an attribute of an element
1648     private static class Attribute {
1649         public final String name;
1650         public final Class<?> sourceType;
1651         public final String value;
1652 
1653         public Attribute(String name, Class<?> sourceType, String value) {
1654             this.name = name;
1655             this.sourceType = sourceType;
1656             this.value = value;
1657         }
1658     }
1659 
1660     // Event handler that delegates to a method defined by the controller object
1661     private static class ControllerMethodEventHandler<T extends Event> implements EventHandler<T> {
1662         private final MethodHandler handler;
1663 
1664         public ControllerMethodEventHandler(MethodHandler handler) {
1665             this.handler = handler;
1666         }
1667 
1668         @Override
1669         public void handle(T event) {
1670             handler.invoke(event);
1671         }
1672     }
1673 
1674     // Event handler implemented in script code
1675     private static class ScriptEventHandler implements EventHandler<Event> {
1676         public final String script;
1677         public final ScriptEngine scriptEngine;
1678 
1679         public ScriptEventHandler(String script, ScriptEngine scriptEngine) {
1680             this.script = script;
1681             this.scriptEngine = scriptEngine;
1682         }
1683 
1684         @Override
1685         public void handle(Event event) {
1686             // Don't pollute the page namespace with values defined in the script
1687             Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
1688             Bindings localBindings = scriptEngine.createBindings();
1689             localBindings.put(EVENT_KEY, event);
1690             localBindings.putAll(engineBindings);
1691             scriptEngine.setBindings(localBindings, ScriptContext.ENGINE_SCOPE);
1692 
1693             // Execute the script
1694             try {
1695                 scriptEngine.eval(script);
1696             } catch (ScriptException exception){
1697                 throw new RuntimeException(exception);
1698             }
1699 
1700             // Restore the original bindings
1701             scriptEngine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);
1702         }
1703     }
1704 
1705     // Observable list change listener
1706     private static class ObservableListChangeAdapter implements ListChangeListener {
1707         private final MethodHandler handler;
1708 
1709         public ObservableListChangeAdapter(MethodHandler handler) {
1710             this.handler = handler;
1711         }
1712 
1713         @Override
1714         @SuppressWarnings("unchecked")
1715         public void onChanged(Change change) {
1716             if (handler != null) {
1717                 handler.invoke(change);
1718             }
1719         }
1720     }
1721 
1722     // Observable map change listener
1723     private static class ObservableMapChangeAdapter implements MapChangeListener {
1724         public final MethodHandler handler;
1725 
1726         public ObservableMapChangeAdapter(MethodHandler handler) {
1727             this.handler = handler;
1728         }
1729 
1730         @Override
1731         public void onChanged(Change change) {
1732             if (handler != null) {
1733                 handler.invoke(change);
1734             }
1735         }
1736     }
1737 
1738     // Observable set change listener
1739     private static class ObservableSetChangeAdapter implements SetChangeListener {
1740         public final MethodHandler handler;
1741 
1742         public ObservableSetChangeAdapter(MethodHandler handler) {
1743             this.handler = handler;
1744         }
1745 
1746         @Override
1747         public void onChanged(Change change) {
1748             if (handler != null) {
1749                 handler.invoke(change);
1750             }
1751         }
1752     }
1753 
1754     // Property model change listener
1755     private static class PropertyChangeAdapter implements ChangeListener<Object> {
1756         public final MethodHandler handler;
1757 
1758         public PropertyChangeAdapter(MethodHandler handler) {
1759             this.handler = handler;
1760         }
1761 
1762         @Override
1763         public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
1764             handler.invoke(observable, oldValue, newValue);
1765         }
1766     }
1767 
1768     private static class MethodHandler {
1769         private final Object controller;
1770         private final Method method;
1771         private final SupportedType type;
1772 
1773         private MethodHandler(Object controller, Method method, SupportedType type) {
1774             this.method = method;
1775             this.controller = controller;
1776             this.type = type;
1777         }
1778 
1779         public void invoke(Object... params) {
1780             try {
1781                 if (type != SupportedType.PARAMETERLESS) {
1782                     MethodHelper.invoke(method, controller, params);
1783                 } else {
1784                     MethodHelper.invoke(method, controller, new Object[] {});
1785                 }
1786             } catch (InvocationTargetException exception) {
1787                 throw new RuntimeException(exception);
1788             } catch (IllegalAccessException exception) {
1789                 throw new RuntimeException(exception);
1790             }
1791         }
1792     }
1793 
1794     private URL location;
1795     private ResourceBundle resources;
1796 
1797     private ObservableMap<String, Object> namespace = FXCollections.observableHashMap();
1798 
1799     private Object root = null;
1800     private Object controller = null;
1801 
1802     private BuilderFactory builderFactory;
1803     private Callback<Class<?>, Object> controllerFactory;
1804     private Charset charset;
1805 
1806     private final LinkedList<FXMLLoader> loaders;
1807 
1808     private ClassLoader classLoader = null;
1809     private boolean staticLoad = false;
1810     private LoadListener loadListener = null;
1811 
1812     private FXMLLoader parentLoader;
1813 
1814     private XMLStreamReader xmlStreamReader = null;
1815     private Element current = null;
1816 
1817     private ScriptEngine scriptEngine = null;
1818 
1819     private List<String> packages = new LinkedList<String>();
1820     private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
1821 
1822     private ScriptEngineManager scriptEngineManager = null;
1823 
1824     private static ClassLoader defaultClassLoader = null;
1825 
1826     private static final Pattern extraneousWhitespacePattern = Pattern.compile("\\s+");
1827 
1828     private static BuilderFactory DEFAULT_BUILDER_FACTORY = new JavaFXBuilderFactory();
1829 
1830     /**
1831      * The character set used when character set is not explicitly specified.
1832      */
1833     public static final String DEFAULT_CHARSET_NAME = "UTF-8";
1834 
1835     /**
1836      * The tag name of language processing instruction.
1837      */
1838     public static final String LANGUAGE_PROCESSING_INSTRUCTION = "language";
1839     /**
1840      * The tag name of import processing instruction.
1841      */
1842     public static final String IMPORT_PROCESSING_INSTRUCTION = "import";
1843 
1844     /**
1845      * Prefix of 'fx' namespace.
1846      */
1847     public static final String FX_NAMESPACE_PREFIX = "fx";
1848     /**
1849      * The name of fx:controller attribute of a root.
1850      */
1851     public static final String FX_CONTROLLER_ATTRIBUTE = "controller";
1852     /**
1853      * The name of fx:id attribute.
1854      */
1855     public static final String FX_ID_ATTRIBUTE = "id";
1856     /**
1857      * The name of fx:value attribute.
1858      */
1859     public static final String FX_VALUE_ATTRIBUTE = "value";
1860     /**
1861      * The tag name of 'fx:constant'.
1862      * @since JavaFX 2.2
1863      */
1864     public static final String FX_CONSTANT_ATTRIBUTE = "constant";
1865     /**
1866      * The name of 'fx:factory' attribute.
1867      */
1868     public static final String FX_FACTORY_ATTRIBUTE = "factory";
1869 
1870     /**
1871      * The tag name of {@literal <fx:include>}.
1872      */
1873     public static final String INCLUDE_TAG = "include";
1874     /**
1875      * The {@literal <fx:include>} 'source' attribute.
1876      */
1877     public static final String INCLUDE_SOURCE_ATTRIBUTE = "source";
1878     /**
1879      * The {@literal <fx:include>} 'resources' attribute.
1880      */
1881     public static final String INCLUDE_RESOURCES_ATTRIBUTE = "resources";
1882     /**
1883      * The {@literal <fx:include>} 'charset' attribute.
1884      */
1885     public static final String INCLUDE_CHARSET_ATTRIBUTE = "charset";
1886 
1887     /**
1888      * The tag name of {@literal <fx:script>}.
1889      */
1890     public static final String SCRIPT_TAG = "script";
1891     /**
1892      * The {@literal <fx:script>} 'source' attribute.
1893      */
1894     public static final String SCRIPT_SOURCE_ATTRIBUTE = "source";
1895     /**
1896      * The {@literal <fx:script>} 'charset' attribute.
1897      */
1898     public static final String SCRIPT_CHARSET_ATTRIBUTE = "charset";
1899 
1900     /**
1901      * The tag name of {@literal <fx:define>}.
1902      */
1903     public static final String DEFINE_TAG = "define";
1904 
1905     /**
1906      * The tag name of {@literal <fx:reference>}.
1907      */
1908     public static final String REFERENCE_TAG = "reference";
1909     /**
1910      * The {@literal <fx:reference>} 'source' attribute.
1911      */
1912     public static final String REFERENCE_SOURCE_ATTRIBUTE = "source";
1913 
1914     /**
1915      * The tag name of {@literal <fx:root>}.
1916      * @since JavaFX 2.2
1917      */
1918     public static final String ROOT_TAG = "root";
1919     /**
1920      * The {@literal <fx:root>} 'type' attribute.
1921      * @since JavaFX 2.2
1922      */
1923     public static final String ROOT_TYPE_ATTRIBUTE = "type";
1924 
1925     /**
1926      * The tag name of {@literal <fx:copy>}.
1927      */
1928     public static final String COPY_TAG = "copy";
1929     /**
1930      * The {@literal <fx:copy>} 'source' attribute.
1931      */
1932     public static final String COPY_SOURCE_ATTRIBUTE = "source";
1933 
1934     /**
1935      * The prefix of event handler attributes.
1936      */
1937     public static final String EVENT_HANDLER_PREFIX = "on";
1938     /**
1939      * The name of the Event object in event handler scripts.
1940      */
1941     public static final String EVENT_KEY = "event";
1942     /**
1943      * Suffix for property change/invalidation handlers.
1944      */
1945     public static final String CHANGE_EVENT_HANDLER_SUFFIX = "Change";
1946     private static final String COLLECTION_HANDLER_NAME = EVENT_HANDLER_PREFIX + CHANGE_EVENT_HANDLER_SUFFIX;
1947 
1948     /**
1949      * Value that represents 'null'.
1950      */
1951     public static final String NULL_KEYWORD = "null";
1952 
1953     /**
1954      * Escape prefix for escaping special characters inside attribute values.
1955      * Serves as an escape for {@link #ESCAPE_PREFIX}, {@link #RELATIVE_PATH_PREFIX},
1956      * {@link #RESOURCE_KEY_PREFIX}, {@link #EXPRESSION_PREFIX},
1957      * {@link #BI_DIRECTIONAL_BINDING_PREFIX}
1958      * @since JavaFX 2.1
1959      */
1960     public static final String ESCAPE_PREFIX = "\\";
1961     /**
1962      * Prefix for relative location resolution.
1963      */
1964     public static final String RELATIVE_PATH_PREFIX = "@";
1965     /**
1966      * Prefix for resource resolution.
1967      */
1968     public static final String RESOURCE_KEY_PREFIX = "%";
1969     /**
1970      * Prefix for (variable) expression resolution.
1971      */
1972     public static final String EXPRESSION_PREFIX = "$";
1973     /**
1974      * Prefix for binding expression resolution.
1975      */
1976     public static final String BINDING_EXPRESSION_PREFIX = "${";
1977     /**
1978      * Suffix for binding expression resolution.
1979      */
1980     public static final String BINDING_EXPRESSION_SUFFIX = "}";
1981 
1982     /**
1983      * Prefix for bidirectional-binding expression resolution.
1984      * @since JavaFX 2.1
1985      */
1986     public static final String BI_DIRECTIONAL_BINDING_PREFIX = "#{";
1987     /**
1988      * Suffix for bidirectional-binding expression resolution.
1989      * @since JavaFX 2.1
1990      */
1991     public static final String BI_DIRECTIONAL_BINDING_SUFFIX = "}";
1992 
1993     /**
1994      * Delimiter for arrays as values.
1995      * @since JavaFX 2.1
1996      */
1997     public static final String ARRAY_COMPONENT_DELIMITER = ",";
1998 
1999     /**
2000      * A key for location URL in namespace map.
2001      * @see #getNamespace()
2002      * @since JavaFX 2.2
2003      */
2004     public static final String LOCATION_KEY = "location";
2005     /**
2006      * A key for ResourceBundle in namespace map.
2007      * @see #getNamespace()
2008      * @since JavaFX 2.2
2009      */
2010     public static final String RESOURCES_KEY = "resources";
2011 
2012     /**
2013      * Prefix for controller method resolution.
2014      */
2015     public static final String CONTROLLER_METHOD_PREFIX = "#";
2016     /**
2017      * A key for controller in namespace map.
2018      * @see #getNamespace()
2019      * @since JavaFX 2.1
2020      */
2021     public static final String CONTROLLER_KEYWORD = "controller";
2022     /**
2023      * A suffix for controllers of included fxml files.
2024      * The full key is stored in namespace map.
2025      * @see #getNamespace()
2026      * @since JavaFX 2.2
2027      */
2028     public static final String CONTROLLER_SUFFIX = "Controller";
2029 
2030     /**
2031      * The name of initialize method.
2032      * @since JavaFX 2.2
2033      */
2034     public static final String INITIALIZE_METHOD_NAME = "initialize";
2035 
2036     /**
2037      * Contains the current javafx version.
2038      * @since JavaFX 8.0
2039      */
2040     public static final String JAVAFX_VERSION;
2041 
2042     /**
2043      * Contains the current fx namepsace version.
2044      * @since JavaFX 8.0
2045      */
2046     public static final String FX_NAMESPACE_VERSION = "1";
2047 
2048     static {
2049         JAVAFX_VERSION = AccessController.doPrivileged(new PrivilegedAction<String>() {
2050             @Override
2051             public String run() {
2052                 return System.getProperty("javafx.version");
2053             }
2054         });
2055 
2056         FXMLLoaderHelper.setFXMLLoaderAccessor(new FXMLLoaderHelper.FXMLLoaderAccessor() {
2057             @Override
2058             public void setStaticLoad(FXMLLoader fxmlLoader, boolean staticLoad) {
2059                 fxmlLoader.setStaticLoad(staticLoad);
2060             }
2061         });
2062     }
2063 
2064     /**
2065      * Creates a new FXMLLoader instance.
2066      */
2067     public FXMLLoader() {
2068         this((URL)null);
2069     }
2070 
2071     /**
2072      * Creates a new FXMLLoader instance.
2073      *
2074      * @param location the location used to resolve relative path attribute values
2075      * @since JavaFX 2.1
2076      */
2077     public FXMLLoader(URL location) {
2078         this(location, null);
2079     }
2080 
2081     /**
2082      * Creates a new FXMLLoader instance.
2083      *
2084      * @param location the location used to resolve relative path attribute values
2085      * @param resources the resources used to resolve resource key attribute values
2086      * @since JavaFX 2.1
2087      */
2088     public FXMLLoader(URL location, ResourceBundle resources) {
2089         this(location, resources, null);
2090     }
2091 
2092     /**
2093      * Creates a new FXMLLoader instance.
2094      *
2095      * @param location the location used to resolve relative path attribute values
2096      * @param resources resources used to resolve resource key attribute values
2097      * @param builderFactory the builder factory used by this loader
2098      * @since JavaFX 2.1
2099      */
2100     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory) {
2101         this(location, resources, builderFactory, null);
2102     }
2103 
2104     /**
2105      * Creates a new FXMLLoader instance.
2106      *
2107      * @param location the location used to resolve relative path attribute values
2108      * @param resources resources used to resolve resource key attribute values
2109      * @param builderFactory the builder factory used by this loader
2110      * @param controllerFactory the controller factory used by this loader
2111      * @since JavaFX 2.1
2112      */
2113     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2114         Callback<Class<?>, Object> controllerFactory) {
2115         this(location, resources, builderFactory, controllerFactory, Charset.forName(DEFAULT_CHARSET_NAME));
2116     }
2117 
2118     /**
2119      * Creates a new FXMLLoader instance.
2120      *
2121      * @param charset the character set used by this loader
2122      */
2123     public FXMLLoader(Charset charset) {
2124         this(null, null, null, null, charset);
2125     }
2126 
2127     /**
2128      * Creates a new FXMLLoader instance.
2129      *
2130      * @param location the location used to resolve relative path attribute values
2131      * @param resources resources used to resolve resource key attribute values
2132      * @param builderFactory the builder factory used by this loader
2133      * @param controllerFactory the controller factory used by this loader
2134      * @param charset the character set used by this loader
2135      * @since JavaFX 2.1
2136      */
2137     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2138         Callback<Class<?>, Object> controllerFactory, Charset charset) {
2139         this(location, resources, builderFactory, controllerFactory, charset,
2140             new LinkedList<FXMLLoader>());
2141     }
2142 
2143     /**
2144      * Creates a new FXMLLoader instance.
2145      *
2146      * @param location the location used to resolve relative path attribute values
2147      * @param resources resources used to resolve resource key attribute values
2148      * @param builderFactory the builder factory used by this loader
2149      * @param controllerFactory the controller factory used by this loader
2150      * @param charset the character set used by this loader
2151      * @param loaders list of loaders
2152      * @since JavaFX 2.1
2153      */
2154     public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory,
2155         Callback<Class<?>, Object> controllerFactory, Charset charset,
2156         LinkedList<FXMLLoader> loaders) {
2157         setLocation(location);
2158         setResources(resources);
2159         setBuilderFactory(builderFactory);
2160         setControllerFactory(controllerFactory);
2161         setCharset(charset);
2162 
2163         this.loaders = new LinkedList(loaders);
2164     }
2165 
2166     /**
2167      * Returns the location used to resolve relative path attribute values.
2168      * @return the location used to resolve relative path attribute values
2169      */
2170     public URL getLocation() {
2171         return location;
2172     }
2173 
2174     /**
2175      * Sets the location used to resolve relative path attribute values.
2176      *
2177      * @param location the location
2178      */
2179     public void setLocation(URL location) {
2180         this.location = location;
2181     }
2182 
2183     /**
2184      * Returns the resources used to resolve resource key attribute values.
2185      * @return the resources used to resolve resource key attribute values
2186      */
2187     public ResourceBundle getResources() {
2188         return resources;
2189     }
2190 
2191     /**
2192      * Sets the resources used to resolve resource key attribute values.
2193      *
2194      * @param resources the resources
2195      */
2196     public void setResources(ResourceBundle resources) {
2197         this.resources = resources;
2198     }
2199 
2200     /**
2201      * Returns the namespace used by this loader.
2202      * @return the namespace
2203      */
2204     public ObservableMap<String, Object> getNamespace() {
2205         return namespace;
2206     }
2207 
2208     /**
2209      * Returns the root of the object hierarchy.
2210      * @param <T> the type of the root object
2211      * @return the root of the object hierarchy
2212      */
2213     @SuppressWarnings("unchecked")
2214     public <T> T getRoot() {
2215         return (T)root;
2216     }
2217 
2218     /**
2219      * Sets the root of the object hierarchy. The value passed to this method
2220      * is used as the value of the {@code <fx:root>} tag. This method
2221      * must be called prior to loading the document when using
2222      * {@code <fx:root>}.
2223      *
2224      * @param root the root of the object hierarchy
2225      *
2226      * @since JavaFX 2.2
2227      */
2228     public void setRoot(Object root) {
2229         this.root = root;
2230     }
2231 
2232     @Override
2233     public boolean equals(Object obj) {
2234         if (obj instanceof FXMLLoader) {
2235             FXMLLoader loader = (FXMLLoader)obj;
2236             if (location == null || loader.location == null) {
2237                 return loader.location == location;
2238             }
2239             return location.toExternalForm().equals(
2240                     loader.location.toExternalForm());
2241         }
2242         return false;
2243     }
2244 
2245     private boolean isCyclic(
2246                             FXMLLoader currentLoader,
2247                             FXMLLoader node) {
2248         if (currentLoader == null) {
2249             return false;
2250         }
2251         if (currentLoader.equals(node)) {
2252             return true;
2253         }
2254         return isCyclic(currentLoader.parentLoader, node);
2255     }
2256 
2257     /**
2258      * Returns the controller associated with the root object.
2259      * @param <T> the type of the controller
2260      * @return the controller associated with the root object
2261      */
2262     @SuppressWarnings("unchecked")
2263     public <T> T getController() {
2264         return (T)controller;
2265     }
2266 
2267     /**
2268      * Sets the controller associated with the root object. The value passed to
2269      * this method is used as the value of the {@code fx:controller} attribute.
2270      * This method must be called prior to loading the document when using
2271      * controller event handlers when an {@code fx:controller} attribute is not
2272      * specified in the document.
2273      *
2274      * @param controller the controller to associate with the root object
2275      *
2276      * @since JavaFX 2.2
2277      */
2278     public void setController(Object controller) {
2279         this.controller = controller;
2280 
2281         if (controller == null) {
2282             namespace.remove(CONTROLLER_KEYWORD);
2283         } else {
2284             namespace.put(CONTROLLER_KEYWORD, controller);
2285         }
2286 
2287         controllerAccessor.setController(controller);
2288     }
2289 
2290     /**
2291      * Returns the builder factory used by this loader.
2292      * @return the builder factory
2293      */
2294     public BuilderFactory getBuilderFactory() {
2295         return builderFactory;
2296     }
2297 
2298     /**
2299      * Sets the builder factory used by this loader.
2300      *
2301      * @param builderFactory the builder factory
2302      */
2303     public void setBuilderFactory(BuilderFactory builderFactory) {
2304         this.builderFactory = builderFactory;
2305     }
2306 
2307     /**
2308      * Returns the controller factory used by this loader.
2309      * @return the controller factory
2310      * @since JavaFX 2.1
2311      */
2312     public Callback<Class<?>, Object> getControllerFactory() {
2313         return controllerFactory;
2314     }
2315 
2316     /**
2317      * Sets the controller factory used by this loader.
2318      *
2319      * @param controllerFactory the controller factory
2320      * @since JavaFX 2.1
2321      */
2322     public void setControllerFactory(Callback<Class<?>, Object> controllerFactory) {
2323         this.controllerFactory = controllerFactory;
2324     }
2325 
2326     /**
2327      * Returns the character set used by this loader.
2328      * @return the character set
2329      */
2330     public Charset getCharset() {
2331         return charset;
2332     }
2333 
2334     /**
2335      * Sets the character set used by this loader.
2336      *
2337      * @param charset the character set
2338      * @since JavaFX 2.1
2339      */
2340     public void setCharset(Charset charset) {
2341         if (charset == null) {
2342             throw new NullPointerException("charset is null.");
2343         }
2344 
2345         this.charset = charset;
2346     }
2347 
2348     /**
2349      * Returns the classloader used by this loader.
2350      * @return the classloader
2351      * @since JavaFX 2.1
2352      */
2353     public ClassLoader getClassLoader() {
2354         if (classLoader == null) {
2355             final SecurityManager sm = System.getSecurityManager();
2356             final Class caller = (sm != null) ?
2357                     walker.getCallerClass() :
2358                     null;
2359             return getDefaultClassLoader(caller);
2360         }
2361         return classLoader;
2362     }
2363 
2364     /**
2365      * Sets the classloader used by this loader and clears any existing
2366      * imports.
2367      *
2368      * @param classLoader the classloader
2369      * @since JavaFX 2.1
2370      */
2371     public void setClassLoader(ClassLoader classLoader) {
2372         if (classLoader == null) {
2373             throw new IllegalArgumentException();
2374         }
2375 
2376         this.classLoader = classLoader;
2377 
2378         clearImports();
2379     }
2380 
2381     /*
2382      * Returns the static load flag.
2383      */
2384     boolean isStaticLoad() {
2385         // SB-dependency: RT-21226 has been filed to track this
2386         return staticLoad;
2387     }
2388 
2389     /*
2390      * Sets the static load flag.
2391      *
2392      * @param staticLoad
2393      */
2394     void setStaticLoad(boolean staticLoad) {
2395         // SB-dependency: RT-21226 has been filed to track this
2396         this.staticLoad = staticLoad;
2397     }
2398 
2399     /**
2400      * Returns this loader's load listener.
2401      *
2402      * @return the load listener
2403      *
2404      * @since 9
2405      */
2406     public LoadListener getLoadListener() {
2407         // SB-dependency: RT-21228 has been filed to track this
2408         return loadListener;
2409     }
2410 
2411     /**
2412      * Sets this loader's load listener.
2413      *
2414      * @param loadListener the load listener
2415      *
2416      * @since 9
2417      */
2418     public final void setLoadListener(LoadListener loadListener) {
2419         // SB-dependency: RT-21228 has been filed to track this
2420         this.loadListener = loadListener;
2421     }
2422 
2423     /**
2424      * Loads an object hierarchy from a FXML document. The location from which
2425      * the document will be loaded must have been set by a prior call to
2426      * {@link #setLocation(URL)}.
2427      *
2428      * @param <T> the type of the root object
2429      * @throws IOException if an error occurs during loading
2430      * @return the loaded object hierarchy
2431      *
2432      * @since JavaFX 2.1
2433      */
2434     public <T> T load() throws IOException {
2435         return loadImpl((System.getSecurityManager() != null)
2436                             ? walker.getCallerClass()
2437                             : null);
2438     }
2439 
2440     /**
2441      * Loads an object hierarchy from a FXML document.
2442      *
2443      * @param <T> the type of the root object
2444      * @param inputStream an input stream containing the FXML data to load
2445      *
2446      * @throws IOException if an error occurs during loading
2447      * @return the loaded object hierarchy
2448      */
2449     public <T> T load(InputStream inputStream) throws IOException {
2450         return loadImpl(inputStream, (System.getSecurityManager() != null)
2451                                          ? walker.getCallerClass()
2452                                          : null);
2453     }
2454 
2455     private Class<?> callerClass;
2456 
2457     private <T> T loadImpl(final Class<?> callerClass) throws IOException {
2458         if (location == null) {
2459             throw new IllegalStateException("Location is not set.");
2460         }
2461 
2462         InputStream inputStream = null;
2463         T value;
2464         try {
2465             inputStream = location.openStream();
2466             value = loadImpl(inputStream, callerClass);
2467         } finally {
2468             if (inputStream != null) {
2469                 inputStream.close();
2470             }
2471         }
2472 
2473         return value;
2474     }
2475 
2476     @SuppressWarnings({ "dep-ann", "unchecked" })
2477     private <T> T loadImpl(InputStream inputStream,
2478                            Class<?> callerClass) throws IOException {
2479         if (inputStream == null) {
2480             throw new NullPointerException("inputStream is null.");
2481         }
2482 
2483         this.callerClass = callerClass;
2484         controllerAccessor.setCallerClass(callerClass);
2485         try {
2486             clearImports();
2487 
2488             // Initialize the namespace
2489             namespace.put(LOCATION_KEY, location);
2490             namespace.put(RESOURCES_KEY, resources);
2491 
2492             // Clear the script engine
2493             scriptEngine = null;
2494 
2495             // Create the parser
2496             try {
2497                 XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
2498                 xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true);
2499 
2500                 // Some stream readers incorrectly report an empty string as the prefix
2501                 // for the default namespace; correct this as needed
2502                 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset);
2503                 xmlStreamReader = new StreamReaderDelegate(xmlInputFactory.createXMLStreamReader(inputStreamReader)) {
2504                     @Override
2505                     public String getPrefix() {
2506                         String prefix = super.getPrefix();
2507 
2508                         if (prefix != null
2509                             && prefix.length() == 0) {
2510                             prefix = null;
2511                         }
2512 
2513                         return prefix;
2514                     }
2515 
2516                     @Override
2517                     public String getAttributePrefix(int index) {
2518                         String attributePrefix = super.getAttributePrefix(index);
2519 
2520                         if (attributePrefix != null
2521                             && attributePrefix.length() == 0) {
2522                             attributePrefix = null;
2523                         }
2524 
2525                         return attributePrefix;
2526                     }
2527                 };
2528             } catch (XMLStreamException exception) {
2529                 throw constructLoadException(exception);
2530             }
2531 
2532             // Push this loader onto the stack
2533             loaders.push(this);
2534 
2535             // Parse the XML stream
2536             try {
2537                 while (xmlStreamReader.hasNext()) {
2538                     int event = xmlStreamReader.next();
2539 
2540                     switch (event) {
2541                         case XMLStreamConstants.PROCESSING_INSTRUCTION: {
2542                             processProcessingInstruction();
2543                             break;
2544                         }
2545 
2546                         case XMLStreamConstants.COMMENT: {
2547                             processComment();
2548                             break;
2549                         }
2550 
2551                         case XMLStreamConstants.START_ELEMENT: {
2552                             processStartElement();
2553                             break;
2554                         }
2555 
2556                         case XMLStreamConstants.END_ELEMENT: {
2557                             processEndElement();
2558                             break;
2559                         }
2560 
2561                         case XMLStreamConstants.CHARACTERS: {
2562                             processCharacters();
2563                             break;
2564                         }
2565                     }
2566                 }
2567             } catch (XMLStreamException exception) {
2568                 throw constructLoadException(exception);
2569             }
2570 
2571             if (controller != null) {
2572                 if (controller instanceof Initializable) {
2573                     ((Initializable)controller).initialize(location, resources);
2574                 } else {
2575                     // Inject controller fields
2576                     Map<String, List<Field>> controllerFields =
2577                             controllerAccessor.getControllerFields();
2578 
2579                     injectFields(LOCATION_KEY, location);
2580 
2581                     injectFields(RESOURCES_KEY, resources);
2582 
2583                     // Initialize the controller
2584                     Method initializeMethod = controllerAccessor
2585                                                   .getControllerMethods()
2586                                                   .get(SupportedType.PARAMETERLESS)
2587                                                   .get(INITIALIZE_METHOD_NAME);
2588 
2589                     if (initializeMethod != null) {
2590                         try {
2591                             MethodHelper.invoke(initializeMethod, controller, new Object [] {});
2592                         } catch (IllegalAccessException exception) {
2593                             throw constructLoadException(exception);
2594                         } catch (InvocationTargetException exception) {
2595                             throw constructLoadException(exception);
2596                         }
2597                     }
2598                 }
2599             }
2600         } catch (final LoadException exception) {
2601             throw exception;
2602         } catch (final Exception exception) {
2603             throw constructLoadException(exception);
2604         } finally {
2605             controllerAccessor.setCallerClass(null);
2606             // Clear controller accessor caches
2607             controllerAccessor.reset();
2608             // Clear the parser
2609             xmlStreamReader = null;
2610         }
2611 
2612         return (T)root;
2613     }
2614 
2615     private void clearImports() {
2616         packages.clear();
2617         classes.clear();
2618     }
2619 
2620     private LoadException constructLoadException(String message){
2621         return new LoadException(message + constructFXMLTrace());
2622     }
2623 
2624     private LoadException constructLoadException(Throwable cause) {
2625         return new LoadException(constructFXMLTrace(), cause);
2626     }
2627 
2628     private LoadException constructLoadException(String message, Throwable cause){
2629         return new LoadException(message + constructFXMLTrace(), cause);
2630     }
2631 
2632     private String constructFXMLTrace() {
2633         StringBuilder messageBuilder = new StringBuilder("\n");
2634 
2635         for (FXMLLoader loader : loaders) {
2636             messageBuilder.append(loader.location != null ? loader.location.getPath() : "unknown path");
2637 
2638             if (loader.current != null) {
2639                 messageBuilder.append(":");
2640                 messageBuilder.append(loader.getLineNumber());
2641             }
2642 
2643             messageBuilder.append("\n");
2644         }
2645         return messageBuilder.toString();
2646     }
2647 
2648     /**
2649      * Returns the current line number.
2650      */
2651     int getLineNumber() {
2652         return xmlStreamReader.getLocation().getLineNumber();
2653     }
2654 
2655     /**
2656      * Returns the current parse trace.
2657      */
2658     ParseTraceElement[] getParseTrace() {
2659         ParseTraceElement[] parseTrace = new ParseTraceElement[loaders.size()];
2660 
2661         int i = 0;
2662         for (FXMLLoader loader : loaders) {
2663             parseTrace[i++] = new ParseTraceElement(loader.location, (loader.current != null) ?
2664                 loader.getLineNumber() : -1);
2665         }
2666 
2667         return parseTrace;
2668     }
2669 
2670     private void processProcessingInstruction() throws LoadException {
2671         String piTarget = xmlStreamReader.getPITarget().trim();
2672 
2673         if (piTarget.equals(LANGUAGE_PROCESSING_INSTRUCTION)) {
2674             processLanguage();
2675         } else if (piTarget.equals(IMPORT_PROCESSING_INSTRUCTION)) {
2676             processImport();
2677         }
2678     }
2679 
2680     private void processLanguage() throws LoadException {
2681         if (scriptEngine != null) {
2682             throw constructLoadException("Page language already set.");
2683         }
2684 
2685         String language = xmlStreamReader.getPIData();
2686 
2687         if (loadListener != null) {
2688             loadListener.readLanguageProcessingInstruction(language);
2689         }
2690 
2691         if (!staticLoad) {
2692             ScriptEngineManager scriptEngineManager = getScriptEngineManager();
2693             scriptEngine = scriptEngineManager.getEngineByName(language);
2694         }
2695     }
2696 
2697     private void processImport() throws LoadException {
2698         String target = xmlStreamReader.getPIData().trim();
2699 
2700         if (loadListener != null) {
2701             loadListener.readImportProcessingInstruction(target);
2702         }
2703 
2704         if (target.endsWith(".*")) {
2705             importPackage(target.substring(0, target.length() - 2));
2706         } else {
2707             importClass(target);
2708         }
2709     }
2710 
2711     private void processComment() throws LoadException {
2712         if (loadListener != null) {
2713             loadListener.readComment(xmlStreamReader.getText());
2714         }
2715     }
2716 
2717     private void processStartElement() throws IOException {
2718         // Create the element
2719         createElement();
2720 
2721         // Process the start tag
2722         current.processStartElement();
2723 
2724         // Set the root value
2725         if (root == null) {
2726             root = current.value;
2727         }
2728     }
2729 
2730     private void createElement() throws IOException {
2731         String prefix = xmlStreamReader.getPrefix();
2732         String localName = xmlStreamReader.getLocalName();
2733 
2734         if (prefix == null) {
2735             int i = localName.lastIndexOf('.');
2736 
2737             if (Character.isLowerCase(localName.charAt(i + 1))) {
2738                 String name = localName.substring(i + 1);
2739 
2740                 if (i == -1) {
2741                     // This is an instance property
2742                     if (loadListener != null) {
2743                         loadListener.beginPropertyElement(name, null);
2744                     }
2745 
2746                     current = new PropertyElement(name, null);
2747                 } else {
2748                     // This is a static property
2749                     Class<?> sourceType = getType(localName.substring(0, i));
2750 
2751                     if (sourceType != null) {
2752                         if (loadListener != null) {
2753                             loadListener.beginPropertyElement(name, sourceType);
2754                         }
2755 
2756                         current = new PropertyElement(name, sourceType);
2757                     } else if (staticLoad) {
2758                         // The source type was not recognized
2759                         if (loadListener != null) {
2760                             loadListener.beginUnknownStaticPropertyElement(localName);
2761                         }
2762 
2763                         current = new UnknownStaticPropertyElement();
2764                     } else {
2765                         throw constructLoadException(localName + " is not a valid property.");
2766                     }
2767                 }
2768             } else {
2769                 if (current == null && root != null) {
2770                     throw constructLoadException("Root value already specified.");
2771                 }
2772 
2773                 Class<?> type = getType(localName);
2774 
2775                 if (type != null) {
2776                     if (loadListener != null) {
2777                         loadListener.beginInstanceDeclarationElement(type);
2778                     }
2779 
2780                     current = new InstanceDeclarationElement(type);
2781                 } else if (staticLoad) {
2782                     // The type was not recognized
2783                     if (loadListener != null) {
2784                         loadListener.beginUnknownTypeElement(localName);
2785                     }
2786 
2787                     current = new UnknownTypeElement();
2788                 } else {
2789                     throw constructLoadException(localName + " is not a valid type.");
2790                 }
2791             }
2792         } else if (prefix.equals(FX_NAMESPACE_PREFIX)) {
2793             if (localName.equals(INCLUDE_TAG)) {
2794                 if (loadListener != null) {
2795                     loadListener.beginIncludeElement();
2796                 }
2797 
2798                 current = new IncludeElement();
2799             } else if (localName.equals(REFERENCE_TAG)) {
2800                 if (loadListener != null) {
2801                     loadListener.beginReferenceElement();
2802                 }
2803 
2804                 current = new ReferenceElement();
2805             } else if (localName.equals(COPY_TAG)) {
2806                 if (loadListener != null) {
2807                     loadListener.beginCopyElement();
2808                 }
2809 
2810                 current = new CopyElement();
2811             } else if (localName.equals(ROOT_TAG)) {
2812                 if (loadListener != null) {
2813                     loadListener.beginRootElement();
2814                 }
2815 
2816                 current = new RootElement();
2817             } else if (localName.equals(SCRIPT_TAG)) {
2818                 if (loadListener != null) {
2819                     loadListener.beginScriptElement();
2820                 }
2821 
2822                 current = new ScriptElement();
2823             } else if (localName.equals(DEFINE_TAG)) {
2824                 if (loadListener != null) {
2825                     loadListener.beginDefineElement();
2826                 }
2827 
2828                 current = new DefineElement();
2829             } else {
2830                 throw constructLoadException(prefix + ":" + localName + " is not a valid element.");
2831             }
2832         } else {
2833             throw constructLoadException("Unexpected namespace prefix: " + prefix + ".");
2834         }
2835     }
2836 
2837     private void processEndElement() throws IOException {
2838         current.processEndElement();
2839 
2840         if (loadListener != null) {
2841             loadListener.endElement(current.value);
2842         }
2843 
2844         // Move up the stack
2845         current = current.parent;
2846     }
2847 
2848     private void processCharacters() throws IOException {
2849         // Process the characters
2850         if (!xmlStreamReader.isWhiteSpace()) {
2851             current.processCharacters();
2852         }
2853     }
2854 
2855     private void importPackage(String name) throws LoadException {
2856         packages.add(name);
2857     }
2858 
2859     private void importClass(String name) throws LoadException {
2860         try {
2861             loadType(name, true);
2862         } catch (ClassNotFoundException exception) {
2863             throw constructLoadException(exception);
2864         }
2865     }
2866 
2867     private Class<?> getType(String name) throws LoadException {
2868         Class<?> type = null;
2869 
2870         if (Character.isLowerCase(name.charAt(0))) {
2871             // This is a fully-qualified class name
2872             try {
2873                 type = loadType(name, false);
2874             } catch (ClassNotFoundException exception) {
2875                 // No-op
2876             }
2877         } else {
2878             // This is an unqualified class name
2879             type = classes.get(name);
2880 
2881             if (type == null) {
2882                 // The class has not been loaded yet; look it up
2883                 for (String packageName : packages) {
2884                     try {
2885                         type = loadTypeForPackage(packageName, name);
2886                     } catch (ClassNotFoundException exception) {
2887                         // No-op
2888                     }
2889 
2890                     if (type != null) {
2891                         break;
2892                     }
2893                 }
2894 
2895                 if (type != null) {
2896                     classes.put(name, type);
2897                 }
2898             }
2899         }
2900 
2901         return type;
2902     }
2903 
2904     private Class<?> loadType(String name, boolean cache) throws ClassNotFoundException {
2905         int i = name.indexOf('.');
2906         int n = name.length();
2907         while (i != -1
2908             && i < n
2909             && Character.isLowerCase(name.charAt(i + 1))) {
2910             i = name.indexOf('.', i + 1);
2911         }
2912 
2913         if (i == -1 || i == n) {
2914             throw new ClassNotFoundException();
2915         }
2916 
2917         String packageName = name.substring(0, i);
2918         String className = name.substring(i + 1);
2919 
2920         Class<?> type = loadTypeForPackage(packageName, className);
2921 
2922         if (cache) {
2923             classes.put(className, type);
2924         }
2925 
2926         return type;
2927     }
2928 
2929     // TODO Rename to loadType() when deprecated static version is removed
2930     private Class<?> loadTypeForPackage(String packageName, String className) throws ClassNotFoundException {
2931         return getClassLoader().loadClass(packageName + "." + className.replace('.', '$'));
2932     }
2933 
2934     private static enum SupportedType {
2935         PARAMETERLESS {
2936 
2937             @Override
2938             protected boolean methodIsOfType(Method m) {
2939                 return m.getParameterTypes().length == 0;
2940             }
2941 
2942         },
2943         EVENT {
2944 
2945             @Override
2946             protected boolean methodIsOfType(Method m) {
2947                 return m.getParameterTypes().length == 1 &&
2948                         Event.class.isAssignableFrom(m.getParameterTypes()[0]);
2949             }
2950 
2951         },
2952         LIST_CHANGE_LISTENER {
2953 
2954             @Override
2955             protected boolean methodIsOfType(Method m) {
2956                 return m.getParameterTypes().length == 1 &&
2957                         m.getParameterTypes()[0].equals(ListChangeListener.Change.class);
2958             }
2959 
2960         },
2961         MAP_CHANGE_LISTENER {
2962 
2963             @Override
2964             protected boolean methodIsOfType(Method m) {
2965                 return m.getParameterTypes().length == 1 &&
2966                         m.getParameterTypes()[0].equals(MapChangeListener.Change.class);
2967             }
2968 
2969         },
2970         SET_CHANGE_LISTENER {
2971 
2972             @Override
2973             protected boolean methodIsOfType(Method m) {
2974                 return m.getParameterTypes().length == 1 &&
2975                         m.getParameterTypes()[0].equals(SetChangeListener.Change.class);
2976             }
2977 
2978         },
2979         PROPERTY_CHANGE_LISTENER {
2980 
2981             @Override
2982             protected boolean methodIsOfType(Method m) {
2983                 return m.getParameterTypes().length == 3 &&
2984                         ObservableValue.class.isAssignableFrom(m.getParameterTypes()[0])
2985                         && m.getParameterTypes()[1].equals(m.getParameterTypes()[2]);
2986             }
2987 
2988         };
2989 
2990         protected abstract boolean methodIsOfType(Method m);
2991     }
2992 
2993     private static SupportedType toSupportedType(Method m) {
2994         for (SupportedType t : SupportedType.values()) {
2995             if (t.methodIsOfType(m)) {
2996                 return t;
2997             }
2998         }
2999         return null;
3000     }
3001 
3002     private ScriptEngineManager getScriptEngineManager() {
3003         if (scriptEngineManager == null) {
3004             scriptEngineManager = new javax.script.ScriptEngineManager();
3005             scriptEngineManager.setBindings(new SimpleBindings(namespace));
3006         }
3007 
3008         return scriptEngineManager;
3009     }
3010 
3011     /**
3012      * Loads a type using the default class loader.
3013      *
3014      * @param packageName the package name of the class to load
3015      * @param className the name of the class to load
3016      *
3017      * @throws ClassNotFoundException if the specified class cannot be found
3018      * @return the class
3019      *
3020      * @deprecated
3021      * This method now delegates to {@link #getDefaultClassLoader()}.
3022      */
3023     @Deprecated
3024     public static Class<?> loadType(String packageName, String className) throws ClassNotFoundException {
3025         return loadType(packageName + "." + className.replace('.', '$'));
3026     }
3027 
3028     /**
3029      * Loads a type using the default class loader.
3030      *
3031      * @param className the name of the class to load
3032      * @throws ClassNotFoundException if the specified class cannot be found
3033      * @return the class
3034      *
3035      * @deprecated
3036      * This method now delegates to {@link #getDefaultClassLoader()}.
3037      */
3038     @Deprecated
3039     public static Class<?> loadType(String className) throws ClassNotFoundException {
3040         ReflectUtil.checkPackageAccess(className);
3041         return Class.forName(className, true, getDefaultClassLoader());
3042     }
3043 
3044     private static boolean needsClassLoaderPermissionCheck(ClassLoader from, ClassLoader to) {
3045         if (from == to) {
3046             return false;
3047         }
3048         if (from == null) {
3049             return false;
3050         }
3051         if (to == null) {
3052             return true;
3053         }
3054         ClassLoader acl = to;
3055         do {
3056             acl = acl.getParent();
3057             if (from == acl) {
3058                 return false;
3059             }
3060         } while (acl != null);
3061         return true;
3062     }
3063 
3064     private static ClassLoader getDefaultClassLoader(Class caller) {
3065         if (defaultClassLoader == null) {
3066             final SecurityManager sm = System.getSecurityManager();
3067             if (sm != null) {
3068                 final ClassLoader callerClassLoader = (caller != null) ?
3069                         caller.getClassLoader() :
3070                         null;
3071                 if (needsClassLoaderPermissionCheck(callerClassLoader, FXMLLoader.class.getClassLoader())) {
3072                     sm.checkPermission(GET_CLASSLOADER_PERMISSION);
3073                 }
3074             }
3075             return Thread.currentThread().getContextClassLoader();
3076         }
3077         return defaultClassLoader;
3078     }
3079 
3080     /**
3081      * Returns the default class loader.
3082      * @return the default class loader
3083      * @since JavaFX 2.1
3084      */
3085     public static ClassLoader getDefaultClassLoader() {
3086         final SecurityManager sm = System.getSecurityManager();
3087         final Class caller = (sm != null) ?
3088                 walker.getCallerClass() :
3089                 null;
3090         return getDefaultClassLoader(caller);
3091     }
3092 
3093     /**
3094      * Sets the default class loader.
3095      *
3096      * @param defaultClassLoader
3097      * The default class loader to use when loading classes.
3098      * @since JavaFX 2.1
3099      */
3100     public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {
3101         if (defaultClassLoader == null) {
3102             throw new NullPointerException();
3103         }
3104         final SecurityManager sm = System.getSecurityManager();
3105         if (sm != null) {
3106             sm.checkPermission(MODIFY_FXML_CLASS_LOADER_PERMISSION);
3107         }
3108 
3109         FXMLLoader.defaultClassLoader = defaultClassLoader;
3110     }
3111 
3112     /**
3113      * Loads an object hierarchy from a FXML document.
3114      *
3115      * @param <T> the type of the root object
3116      * @param location the location used to resolve relative path attribute values
3117      *
3118      * @throws IOException if an error occurs during loading
3119      * @return the loaded object hierarchy
3120      */
3121     public static <T> T load(URL location) throws IOException {
3122         return loadImpl(location, (System.getSecurityManager() != null)
3123                                       ? walker.getCallerClass()
3124                                       : null);
3125     }
3126 
3127     private static <T> T loadImpl(URL location, Class<?> callerClass)
3128             throws IOException {
3129         return loadImpl(location, null, callerClass);
3130     }
3131 
3132     /**
3133      * Loads an object hierarchy from a FXML document.
3134      *
3135      * @param <T> the type of the root object
3136      * @param location the location used to resolve relative path attribute values
3137      * @param resources the resources used to resolve resource key attribute values
3138      *
3139      * @throws IOException if an error occurs during loading
3140      * @return the loaded object hierarchy
3141      */
3142     public static <T> T load(URL location, ResourceBundle resources)
3143                                      throws IOException {
3144         return loadImpl(location, resources,
3145                         (System.getSecurityManager() != null)
3146                             ? walker.getCallerClass()
3147                             : null);
3148     }
3149 
3150     private static <T> T loadImpl(URL location, ResourceBundle resources,
3151                                   Class<?> callerClass) throws IOException {
3152         return loadImpl(location, resources,  null,
3153                         callerClass);
3154     }
3155 
3156     /**
3157      * Loads an object hierarchy from a FXML document.
3158      *
3159      * @param <T> the type of the root object
3160      * @param location the location used to resolve relative path attribute values
3161      * @param resources the resources used to resolve resource key attribute values
3162      * @param builderFactory the builder factory used to load the document
3163      *
3164      * @throws IOException if an error occurs during loading
3165      * @return the loaded object hierarchy
3166      */
3167     public static <T> T load(URL location, ResourceBundle resources,
3168                              BuilderFactory builderFactory)
3169                                      throws IOException {
3170         return loadImpl(location, resources, builderFactory,
3171                         (System.getSecurityManager() != null)
3172                             ? walker.getCallerClass()
3173                             : null);
3174     }
3175 
3176     private static <T> T loadImpl(URL location, ResourceBundle resources,
3177                                   BuilderFactory builderFactory,
3178                                   Class<?> callerClass) throws IOException {
3179         return loadImpl(location, resources, builderFactory, null, callerClass);
3180     }
3181 
3182     /**
3183      * Loads an object hierarchy from a FXML document.
3184      *
3185      * @param <T> the type of the root object
3186      * @param location the location used to resolve relative path attribute values
3187      * @param resources the resources used to resolve resource key attribute values
3188      * @param builderFactory the builder factory used when loading the document
3189      * @param controllerFactory the controller factory used when loading the document
3190      *
3191      * @throws IOException if an error occurs during loading
3192      * @return the loaded object hierarchy
3193      *
3194      * @since JavaFX 2.1
3195      */
3196     public static <T> T load(URL location, ResourceBundle resources,
3197                              BuilderFactory builderFactory,
3198                              Callback<Class<?>, Object> controllerFactory)
3199                                      throws IOException {
3200         return loadImpl(location, resources, builderFactory, controllerFactory,
3201                         (System.getSecurityManager() != null)
3202                             ? walker.getCallerClass()
3203                             : null);
3204     }
3205 
3206     private static <T> T loadImpl(URL location, ResourceBundle resources,
3207                                   BuilderFactory builderFactory,
3208                                   Callback<Class<?>, Object> controllerFactory,
3209                                   Class<?> callerClass) throws IOException {
3210         return loadImpl(location, resources, builderFactory, controllerFactory,
3211                         Charset.forName(DEFAULT_CHARSET_NAME), callerClass);
3212     }
3213 
3214     /**
3215      * Loads an object hierarchy from a FXML document.
3216      *
3217      * @param <T> the type of the root object
3218      * @param location the location used to resolve relative path attribute values
3219      * @param resources the resources used to resolve resource key attribute values
3220      * @param builderFactory the builder factory used when loading the document
3221      * @param controllerFactory the controller factory used when loading the document
3222      * @param charset the character set used when loading the document
3223      *
3224      * @throws IOException if an error occurs during loading
3225      * @return the loaded object hierarchy
3226      *
3227      * @since JavaFX 2.1
3228      */
3229     public static <T> T load(URL location, ResourceBundle resources,
3230                              BuilderFactory builderFactory,
3231                              Callback<Class<?>, Object> controllerFactory,
3232                              Charset charset) throws IOException {
3233         return loadImpl(location, resources, builderFactory, controllerFactory,
3234                         charset,
3235                         (System.getSecurityManager() != null)
3236                             ? walker.getCallerClass()
3237                             : null);
3238     }
3239 
3240     private static <T> T loadImpl(URL location, ResourceBundle resources,
3241                                   BuilderFactory builderFactory,
3242                                   Callback<Class<?>, Object> controllerFactory,
3243                                   Charset charset, Class<?> callerClass)
3244                                           throws IOException {
3245         if (location == null) {
3246             throw new NullPointerException("Location is required.");
3247         }
3248 
3249         FXMLLoader fxmlLoader =
3250                 new FXMLLoader(location, resources, builderFactory,
3251                                controllerFactory, charset);
3252 
3253         return fxmlLoader.<T>loadImpl(callerClass);
3254     }
3255 
3256     /**
3257      * Utility method for comparing two JavaFX version strings (such as 2.2.5, 8.0.0-ea)
3258      * @param rtVer String representation of JavaFX runtime version, including - or _ appendix
3259      * @param nsVer String representation of JavaFX version to compare against runtime version
3260      * @return number &lt; 0 if runtime version is lower, 0 when both versions are the same,
3261      *          number &gt; 0 if runtime is higher version
3262      */
3263     static int compareJFXVersions(String rtVer, String nsVer) {
3264 
3265         int retVal = 0;
3266 
3267         if (rtVer == null || "".equals(rtVer) ||
3268             nsVer == null || "".equals(nsVer)) {
3269             return retVal;
3270         }
3271 
3272         if (rtVer.equals(nsVer)) {
3273             return retVal;
3274         }
3275 
3276         // version string can contain '-'
3277         int dashIndex = rtVer.indexOf("-");
3278         if (dashIndex > 0) {
3279             rtVer = rtVer.substring(0, dashIndex);
3280         }
3281 
3282         // or "_"
3283         int underIndex = rtVer.indexOf("_");
3284         if (underIndex > 0) {
3285             rtVer = rtVer.substring(0, underIndex);
3286         }
3287 
3288         // do not try to compare if the string is not valid version format
3289         if (!Pattern.matches("^(\\d+)(\\.\\d+)*$", rtVer) ||
3290             !Pattern.matches("^(\\d+)(\\.\\d+)*$", nsVer)) {
3291             return retVal;
3292         }
3293 
3294         StringTokenizer nsVerTokenizer = new StringTokenizer(nsVer, ".");
3295         StringTokenizer rtVerTokenizer = new StringTokenizer(rtVer, ".");
3296         int nsDigit = 0, rtDigit = 0;
3297         boolean rtVerEnd = false;
3298 
3299         while (nsVerTokenizer.hasMoreTokens() && retVal == 0) {
3300             nsDigit = Integer.parseInt(nsVerTokenizer.nextToken());
3301             if (rtVerTokenizer.hasMoreTokens()) {
3302                 rtDigit = Integer.parseInt(rtVerTokenizer.nextToken());
3303                 retVal = rtDigit - nsDigit;
3304             } else {
3305                 rtVerEnd = true;
3306                 break;
3307             }
3308         }
3309 
3310         if (rtVerTokenizer.hasMoreTokens() && retVal == 0) {
3311             rtDigit = Integer.parseInt(rtVerTokenizer.nextToken());
3312             if (rtDigit > 0) {
3313                 retVal = 1;
3314             }
3315         }
3316 
3317         if (rtVerEnd) {
3318             if (nsDigit > 0) {
3319                 retVal = -1;
3320             } else {
3321                 while (nsVerTokenizer.hasMoreTokens()) {
3322                     nsDigit = Integer.parseInt(nsVerTokenizer.nextToken());
3323                     if (nsDigit > 0) {
3324                         retVal = -1;
3325                         break;
3326                     }
3327                 }
3328             }
3329         }
3330 
3331         return retVal;
3332     }
3333 
3334     private static void checkClassLoaderPermission() {
3335         final SecurityManager securityManager = System.getSecurityManager();
3336         if (securityManager != null) {
3337             securityManager.checkPermission(MODIFY_FXML_CLASS_LOADER_PERMISSION);
3338         }
3339     }
3340 
3341     private final ControllerAccessor controllerAccessor =
3342             new ControllerAccessor();
3343 
3344     private static final class ControllerAccessor {
3345         private static final int PUBLIC = 1;
3346         private static final int PROTECTED = 2;
3347         private static final int PACKAGE = 4;
3348         private static final int PRIVATE = 8;
3349         private static final int INITIAL_CLASS_ACCESS =
3350                 PUBLIC | PROTECTED | PACKAGE | PRIVATE;
3351         private static final int INITIAL_MEMBER_ACCESS =
3352                 PUBLIC | PROTECTED | PACKAGE | PRIVATE;
3353 
3354         private static final int METHODS = 0;
3355         private static final int FIELDS = 1;
3356 
3357         private Object controller;
3358         private ClassLoader callerClassLoader;
3359 
3360         private Map<String, List<Field>> controllerFields;
3361         private Map<SupportedType, Map<String, Method>> controllerMethods;
3362 
3363         void setController(final Object controller) {
3364             if (this.controller != controller) {
3365                 this.controller = controller;
3366                 reset();
3367             }
3368         }
3369 
3370         void setCallerClass(final Class<?> callerClass) {
3371             final ClassLoader newCallerClassLoader =
3372                     (callerClass != null) ? callerClass.getClassLoader()
3373                                           : null;
3374             if (callerClassLoader != newCallerClassLoader) {
3375                 callerClassLoader = newCallerClassLoader;
3376                 reset();
3377             }
3378         }
3379 
3380         void reset() {
3381             controllerFields = null;
3382             controllerMethods = null;
3383         }
3384 
3385         Map<String, List<Field>> getControllerFields() {
3386             if (controllerFields == null) {
3387                 controllerFields = new HashMap<>();
3388 
3389                 if (callerClassLoader == null) {
3390                     // allow null class loader only with permission check
3391                     checkClassLoaderPermission();
3392                 }
3393 
3394                 addAccessibleMembers(controller.getClass(),
3395                                      INITIAL_CLASS_ACCESS,
3396                                      INITIAL_MEMBER_ACCESS,
3397                                      FIELDS);
3398             }
3399 
3400             return controllerFields;
3401         }
3402 
3403         Map<SupportedType, Map<String, Method>> getControllerMethods() {
3404             if (controllerMethods == null) {
3405                 controllerMethods = new EnumMap<>(SupportedType.class);
3406                 for (SupportedType t: SupportedType.values()) {
3407                     controllerMethods.put(t, new HashMap<String, Method>());
3408                 }
3409 
3410                 if (callerClassLoader == null) {
3411                     // allow null class loader only with permission check
3412                     checkClassLoaderPermission();
3413                 }
3414 
3415                 addAccessibleMembers(controller.getClass(),
3416                                      INITIAL_CLASS_ACCESS,
3417                                      INITIAL_MEMBER_ACCESS,
3418                                      METHODS);
3419             }
3420 
3421             return controllerMethods;
3422         }
3423 
3424         private void addAccessibleMembers(final Class<?> type,
3425                                           final int prevAllowedClassAccess,
3426                                           final int prevAllowedMemberAccess,
3427                                           final int membersType) {
3428             if (type == Object.class) {
3429                 return;
3430             }
3431 
3432             int allowedClassAccess = prevAllowedClassAccess;
3433             int allowedMemberAccess = prevAllowedMemberAccess;
3434             if ((callerClassLoader != null)
3435                     && (type.getClassLoader() != callerClassLoader)) {
3436                 // restrict further access
3437                 allowedClassAccess &= PUBLIC;
3438                 allowedMemberAccess &= PUBLIC;
3439             }
3440 
3441             final int classAccess = getAccess(type.getModifiers());
3442             if ((classAccess & allowedClassAccess) == 0) {
3443                 // we are done
3444                 return;
3445             }
3446 
3447             ReflectUtil.checkPackageAccess(type);
3448 
3449             addAccessibleMembers(type.getSuperclass(),
3450                                  allowedClassAccess,
3451                                  allowedMemberAccess,
3452                                  membersType);
3453 
3454             final int finalAllowedMemberAccess = allowedMemberAccess;
3455             AccessController.doPrivileged(
3456                     new PrivilegedAction<Void>() {
3457                         @Override
3458                         public Void run() {
3459                             if (membersType == FIELDS) {
3460                                 addAccessibleFields(type,
3461                                                     finalAllowedMemberAccess);
3462                             } else {
3463                                 addAccessibleMethods(type,
3464                                                      finalAllowedMemberAccess);
3465                             }
3466 
3467                             return null;
3468                         }
3469                     });
3470         }
3471 
3472         private void addAccessibleFields(final Class<?> type,
3473                                          final int allowedMemberAccess) {
3474             final boolean isPublicType = Modifier.isPublic(type.getModifiers());
3475 
3476             final Field[] fields = type.getDeclaredFields();
3477             for (int i = 0; i < fields.length; ++i) {
3478                 final Field field = fields[i];
3479                 final int memberModifiers = field.getModifiers();
3480 
3481                 if (((memberModifiers & (Modifier.STATIC
3482                                              | Modifier.FINAL)) != 0)
3483                         || ((getAccess(memberModifiers) & allowedMemberAccess)
3484                                 == 0)) {
3485                     continue;
3486                 }
3487 
3488                 if (!isPublicType || !Modifier.isPublic(memberModifiers)) {
3489                     if (field.getAnnotation(FXML.class) == null) {
3490                         // no fxml annotation on a non-public field
3491                         continue;
3492                     }
3493 
3494                     // Ensure that the field is accessible
3495                     field.setAccessible(true);
3496                 }
3497 
3498                 List<Field> list = controllerFields.get(field.getName());
3499                 if (list == null) {
3500                     list = new ArrayList<>(1);
3501                     controllerFields.put(field.getName(), list);
3502                 }
3503                 list.add(field);
3504 
3505             }
3506         }
3507 
3508         private void addAccessibleMethods(final Class<?> type,
3509                                           final int allowedMemberAccess) {
3510             final boolean isPublicType = Modifier.isPublic(type.getModifiers());
3511 
3512             final Method[] methods = type.getDeclaredMethods();
3513             for (int i = 0; i < methods.length; ++i) {
3514                 final Method method = methods[i];
3515                 final int memberModifiers = method.getModifiers();
3516 
3517                 if (((memberModifiers & (Modifier.STATIC
3518                                              | Modifier.NATIVE)) != 0)
3519                         || ((getAccess(memberModifiers) & allowedMemberAccess)
3520                                 == 0)) {
3521                     continue;
3522                 }
3523 
3524                 if (!isPublicType || !Modifier.isPublic(memberModifiers)) {
3525                     if (method.getAnnotation(FXML.class) == null) {
3526                         // no fxml annotation on a non-public method
3527                         continue;
3528                     }
3529 
3530                     // Ensure that the method is accessible
3531                     method.setAccessible(true);
3532                 }
3533 
3534                 // Add this method to the map if:
3535                 // a) it is the initialize() method, or
3536                 // b) it takes a single event argument, or
3537                 // c) it takes no arguments and a handler with this
3538                 //    name has not already been defined
3539                 final String methodName = method.getName();
3540                 final SupportedType convertedType;
3541 
3542                 if ((convertedType = toSupportedType(method)) != null) {
3543                     controllerMethods.get(convertedType)
3544                                      .put(methodName, method);
3545                 }
3546             }
3547         }
3548 
3549         private static int getAccess(final int fullModifiers) {
3550             final int untransformedAccess =
3551                     fullModifiers & (Modifier.PRIVATE | Modifier.PROTECTED
3552                                                       | Modifier.PUBLIC);
3553 
3554             switch (untransformedAccess) {
3555                 case Modifier.PUBLIC:
3556                     return PUBLIC;
3557 
3558                 case Modifier.PROTECTED:
3559                     return PROTECTED;
3560 
3561                 case Modifier.PRIVATE:
3562                     return PRIVATE;
3563 
3564                 default:
3565                     return PACKAGE;
3566             }
3567         }
3568     }
3569 }