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