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