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