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