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