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