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