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