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