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