Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.fxml; 027 028import com.sun.javafx.Logging; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.InputStreamReader; 032import java.lang.reflect.Array; 033import java.lang.reflect.Constructor; 034import java.lang.reflect.Field; 035import java.lang.reflect.InvocationTargetException; 036import java.lang.reflect.Method; 037import java.lang.reflect.Modifier; 038import java.lang.reflect.ParameterizedType; 039import java.lang.reflect.Type; 040import java.net.URL; 041import java.nio.charset.Charset; 042import java.util.AbstractMap; 043import java.util.ArrayList; 044import java.util.Collections; 045import java.util.HashMap; 046import java.util.LinkedList; 047import java.util.List; 048import java.util.Map; 049import java.util.ResourceBundle; 050import java.util.Set; 051import java.util.regex.Pattern; 052 053import javafx.beans.DefaultProperty; 054import javafx.beans.property.Property; 055import javafx.beans.value.ChangeListener; 056import javafx.beans.value.ObservableValue; 057import javafx.collections.FXCollections; 058import javafx.collections.ListChangeListener; 059import javafx.collections.MapChangeListener; 060import javafx.collections.ObservableList; 061import javafx.collections.ObservableMap; 062import javafx.event.Event; 063import javafx.event.EventHandler; 064import javafx.event.EventType; 065import javafx.util.Builder; 066import javafx.util.BuilderFactory; 067import javafx.util.Callback; 068 069import javax.script.Bindings; 070import javax.script.ScriptContext; 071import javax.script.ScriptEngine; 072import javax.script.ScriptEngineManager; 073import javax.script.ScriptException; 074import javax.script.SimpleBindings; 075import javax.xml.stream.XMLInputFactory; 076import javax.xml.stream.XMLStreamConstants; 077import javax.xml.stream.XMLStreamException; 078import javax.xml.stream.XMLStreamReader; 079import javax.xml.stream.util.StreamReaderDelegate; 080 081import com.sun.javafx.beans.IDProperty; 082import com.sun.javafx.fxml.BeanAdapter; 083import com.sun.javafx.fxml.LoadListener; 084import com.sun.javafx.fxml.ObservableListChangeEvent; 085import com.sun.javafx.fxml.ObservableMapChangeEvent; 086import com.sun.javafx.fxml.PropertyChangeEvent; 087import com.sun.javafx.fxml.PropertyNotFoundException; 088import com.sun.javafx.fxml.expression.Expression; 089import com.sun.javafx.fxml.expression.ExpressionValue; 090import com.sun.javafx.fxml.expression.KeyPath; 091import java.net.MalformedURLException; 092import java.security.AccessController; 093import java.security.PrivilegedAction; 094import java.util.Locale; 095import java.util.StringTokenizer; 096import sun.reflect.misc.ConstructorUtil; 097import sun.reflect.misc.FieldUtil; 098import sun.reflect.misc.MethodUtil; 099import sun.reflect.misc.ReflectUtil; 100 101/** 102 * Loads an object hierarchy from an XML document. 103 */ 104public class FXMLLoader { 105 // Abstract base class for elements 106 private abstract class Element { 107 public final Element parent; 108 public final int lineNumber; 109 110 public Object value = null; 111 private BeanAdapter valueAdapter = null; 112 113 public final LinkedList<Attribute> eventHandlerAttributes = new LinkedList<Attribute>(); 114 public final LinkedList<Attribute> instancePropertyAttributes = new LinkedList<Attribute>(); 115 public final LinkedList<Attribute> staticPropertyAttributes = new LinkedList<Attribute>(); 116 public final LinkedList<PropertyElement> staticPropertyElements = new LinkedList<PropertyElement>(); 117 118 public Element() { 119 parent = current; 120 lineNumber = getLineNumber(); 121 } 122 123 public boolean isCollection() { 124 // Return true if value is a list, or if the value's type defines 125 // a default property that is a list 126 boolean collection; 127 if (value instanceof List<?>) { 128 collection = true; 129 } else { 130 Class<?> type = value.getClass(); 131 DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class); 132 133 if (defaultProperty != null) { 134 collection = getProperties().get(defaultProperty.value()) instanceof List<?>; 135 } else { 136 collection = false; 137 } 138 } 139 140 return collection; 141 } 142 143 @SuppressWarnings("unchecked") 144 public void add(Object element) throws LoadException { 145 // If value is a list, add element to it; otherwise, get the value 146 // of the default property, which is assumed to be a list and add 147 // to that (coerce to the appropriate type) 148 List<Object> list; 149 if (value instanceof List<?>) { 150 list = (List<Object>)value; 151 } else { 152 Class<?> type = value.getClass(); 153 DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class); 154 String defaultPropertyName = defaultProperty.value(); 155 156 // Get the list value 157 list = (List<Object>)getProperties().get(defaultPropertyName); 158 159 // Coerce the element to the list item type 160 if (!Map.class.isAssignableFrom(type)) { 161 Type listType = getValueAdapter().getGenericType(defaultPropertyName); 162 element = BeanAdapter.coerce(element, BeanAdapter.getListItemType(listType)); 163 } 164 } 165 166 list.add(element); 167 } 168 169 public void set(Object value) throws LoadException { 170 if (this.value == null) { 171 throw new LoadException("Cannot set value on this element."); 172 } 173 174 // Apply value to this element's properties 175 Class<?> type = this.value.getClass(); 176 DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class); 177 if (defaultProperty == null) { 178 throw new LoadException("Element does not define a default property."); 179 } 180 181 getProperties().put(defaultProperty.value(), value); 182 } 183 184 public void updateValue(Object value) { 185 this.value = value; 186 valueAdapter = null; 187 } 188 189 public boolean isTyped() { 190 return !(value instanceof Map<?, ?>); 191 } 192 193 public BeanAdapter getValueAdapter() { 194 if (valueAdapter == null) { 195 valueAdapter = new BeanAdapter(value); 196 } 197 198 return valueAdapter; 199 } 200 201 @SuppressWarnings("unchecked") 202 public Map<String, Object> getProperties() { 203 return (isTyped()) ? getValueAdapter() : (Map<String, Object>)value; 204 } 205 206 public void processStartElement() throws IOException { 207 for (int i = 0, n = xmlStreamReader.getAttributeCount(); i < n; i++) { 208 String prefix = xmlStreamReader.getAttributePrefix(i); 209 String localName = xmlStreamReader.getAttributeLocalName(i); 210 String value = xmlStreamReader.getAttributeValue(i); 211 212 if (loadListener != null 213 && prefix != null 214 && prefix.equals(FX_NAMESPACE_PREFIX)) { 215 loadListener.readInternalAttribute(prefix + ":" + localName, value); 216 } 217 218 processAttribute(prefix, localName, value); 219 } 220 } 221 222 public void processEndElement() throws IOException { 223 // No-op 224 } 225 226 public void processCharacters() throws IOException { 227 throw new LoadException("Unexpected characters in input stream."); 228 } 229 230 public void processInstancePropertyAttributes() throws IOException { 231 if (instancePropertyAttributes.size() > 0) { 232 for (Attribute attribute : instancePropertyAttributes) { 233 processPropertyAttribute(attribute); 234 } 235 } 236 } 237 238 public void processAttribute(String prefix, String localName, String value) 239 throws IOException{ 240 if (prefix == null) { 241 // Add the attribute to the appropriate list 242 if (localName.startsWith(EVENT_HANDLER_PREFIX)) { 243 if (loadListener != null) { 244 loadListener.readEventHandlerAttribute(localName, value); 245 } 246 247 eventHandlerAttributes.add(new Attribute(localName, null, value)); 248 } else { 249 int i = localName.lastIndexOf('.'); 250 251 if (i == -1) { 252 // The attribute represents an instance property 253 if (loadListener != null) { 254 loadListener.readPropertyAttribute(localName, null, value); 255 } 256 257 instancePropertyAttributes.add(new Attribute(localName, null, value)); 258 } else { 259 // The attribute represents a static property 260 String name = localName.substring(i + 1); 261 Class<?> sourceType = getType(localName.substring(0, i)); 262 263 if (sourceType != null) { 264 if (loadListener != null) { 265 loadListener.readPropertyAttribute(name, sourceType, value); 266 } 267 268 staticPropertyAttributes.add(new Attribute(name, sourceType, value)); 269 } else if (staticLoad) { 270 if (loadListener != null) { 271 loadListener.readUnknownStaticPropertyAttribute(localName, value); 272 } 273 } else { 274 throw new LoadException(localName + " is not a valid attribute."); 275 } 276 } 277 278 } 279 } else { 280 throw new LoadException(prefix + ":" + localName 281 + " is not a valid attribute."); 282 } 283 } 284 285 @SuppressWarnings("unchecked") 286 public void processPropertyAttribute(Attribute attribute) throws IOException { 287 String value = attribute.value; 288 if (isBindingExpression(value)) { 289 // Resolve the expression 290 Expression expression; 291 292 if (attribute.sourceType != null) { 293 throw new LoadException("Cannot bind to static property."); 294 } 295 296 if (!isTyped()) { 297 throw new LoadException("Cannot bind to untyped object."); 298 } 299 300 // TODO We may want to identify binding properties in processAttribute() 301 // and apply them after build() has been called 302 if (this.value instanceof Builder) { 303 throw new LoadException("Cannot bind to builder property."); 304 } 305 306 value = value.substring(BINDING_EXPRESSION_PREFIX.length(), 307 value.length() - 1); 308 expression = Expression.valueOf(value); 309 310 // Create the binding 311 BeanAdapter targetAdapter = new BeanAdapter(this.value); 312 ObservableValue<Object> propertyModel = targetAdapter.getPropertyModel(attribute.name); 313 Class<?> type = targetAdapter.getType(attribute.name); 314 315 if (propertyModel instanceof Property<?>) { 316 ((Property<Object>)propertyModel).bind(new ExpressionValue(namespace, expression, type)); 317 } 318 } else if (isBidirectionalBindingExpression(value)) { 319 throw 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 new LoadException("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 new LoadException("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 try { 392 return (aValue.charAt(0) == '/') ? 393 classLoader.getResource(aValue.substring(1)).toString() : 394 new URL(FXMLLoader.this.location, aValue).toString(); 395 } catch (MalformedURLException e) { 396 System.err.println(FXMLLoader.this.location + "/" + aValue); 397 } 398 } 399 } else if (aValue.startsWith(RESOURCE_KEY_PREFIX)) { 400 aValue = aValue.substring(RESOURCE_KEY_PREFIX.length()); 401 if (aValue.length() == 0) { 402 throw new LoadException("Missing resource key."); 403 } 404 if (aValue.startsWith(RESOURCE_KEY_PREFIX)) { 405 // The prefix was escaped 406 warnDeprecatedEscapeSequence(RESOURCE_KEY_PREFIX); 407 return aValue; 408 } else { 409 // Resolve the resource value 410 if (resources == null) { 411 throw new LoadException("No resources specified."); 412 } 413 if (!resources.containsKey(aValue)) { 414 throw new LoadException("Resource \"" + aValue + "\" not found."); 415 } 416 417 return resources.getString(aValue); 418 } 419 } else if (aValue.startsWith(EXPRESSION_PREFIX)) { 420 aValue = aValue.substring(EXPRESSION_PREFIX.length()); 421 if (aValue.length() == 0) { 422 throw new LoadException("Missing expression."); 423 } 424 if (aValue.startsWith(EXPRESSION_PREFIX)) { 425 // The prefix was escaped 426 warnDeprecatedEscapeSequence(EXPRESSION_PREFIX); 427 return aValue; 428 } else if (aValue.equals(NULL_KEYWORD)) { 429 // The attribute value is null 430 return null; 431 } 432 return Expression.get(namespace, KeyPath.parse(aValue)); 433 } 434 return aValue; 435 } 436 437 /** 438 * Creates an array of given type and populates it with values from 439 * a string where tokens are separated by ARRAY_COMPONENT_DELIMITER. 440 * If token is prefixed with RELATIVE_PATH_PREFIX a value added to 441 * the array becomes relative to document location. 442 */ 443 private Object populateArrayFromString( 444 Class<?>type, 445 String stringValue) throws LoadException { 446 447 Object propertyValue = null; 448 // Split the string and set the values as an array 449 Class<?> componentType = type.getComponentType(); 450 451 if (stringValue.length() > 0) { 452 String[] values = stringValue.split(ARRAY_COMPONENT_DELIMITER); 453 propertyValue = Array.newInstance(componentType, values.length); 454 for (int i = 0; i < values.length; i++) { 455 Array.set(propertyValue, i, 456 BeanAdapter.coerce(resolvePrefixedValue(values[i].trim()), 457 type.getComponentType())); 458 } 459 } else { 460 propertyValue = Array.newInstance(componentType, 0); 461 } 462 return propertyValue; 463 } 464 465 /** 466 * Populates list with values from a string where tokens are separated 467 * by ARRAY_COMPONENT_DELIMITER. If token is prefixed with RELATIVE_PATH_PREFIX 468 * a value added to the list becomes relative to document location. 469 */ 470 private void populateListFromString( 471 BeanAdapter valueAdapter, 472 String listPropertyName, 473 String stringValue) throws LoadException { 474 // Split the string and add the values to the list 475 List<Object> list = (List<Object>)valueAdapter.get(listPropertyName); 476 Type listType = valueAdapter.getGenericType(listPropertyName); 477 Type itemType = (Class<?>)BeanAdapter.getGenericListItemType(listType); 478 479 if (itemType instanceof ParameterizedType) { 480 itemType = ((ParameterizedType)itemType).getRawType(); 481 } 482 483 if (stringValue.length() > 0) { 484 String[] values = stringValue.split(ARRAY_COMPONENT_DELIMITER); 485 486 for (String aValue: values) { 487 aValue = aValue.trim(); 488 list.add( 489 BeanAdapter.coerce(resolvePrefixedValue(aValue), 490 (Class<?>)itemType)); 491 } 492 } 493 } 494 495 public void warnDeprecatedEscapeSequence(String prefix) { 496 System.err.println(prefix + prefix + " is a deprecated escape sequence. " 497 + "Please use \\" + prefix + " instead."); 498 } 499 500 public void applyProperty(String name, Class<?> sourceType, Object value) { 501 if (sourceType == null) { 502 getProperties().put(name, value); 503 } else { 504 BeanAdapter.put(this.value, sourceType, name, value); 505 } 506 } 507 508 public void processEventHandlerAttributes() throws LoadException { 509 if (eventHandlerAttributes.size() > 0 && !staticLoad) { 510 for (Attribute attribute : eventHandlerAttributes) { 511 EventHandler<? extends Event> eventHandler = null; 512 513 String attrValue = attribute.value; 514 515 if (attrValue.startsWith(CONTROLLER_METHOD_PREFIX)) { 516 attrValue = attrValue.substring(CONTROLLER_METHOD_PREFIX.length()); 517 518 if (!attrValue.startsWith(CONTROLLER_METHOD_PREFIX)) { 519 if (attrValue.length() == 0) { 520 throw new LoadException("Missing controller method."); 521 } 522 523 if (controller == null) { 524 throw new LoadException("No controller specified."); 525 } 526 527 Method method = getControllerMethods().get(attrValue); 528 529 if (method == null) { 530 throw new LoadException("Controller method \"" + attrValue + "\" not found."); 531 } 532 533 eventHandler = new ControllerMethodEventHandler(controller, method); 534 } 535 536 } else if (attrValue.startsWith(EXPRESSION_PREFIX)) { 537 attrValue = attrValue.substring(EXPRESSION_PREFIX.length()); 538 539 if (attrValue.length() == 0) { 540 throw new LoadException("Missing expression reference."); 541 } 542 543 Object expression = Expression.get(namespace, KeyPath.parse(attrValue)); 544 if (expression instanceof EventHandler) { 545 eventHandler = (EventHandler<? extends Event>) expression; 546 } 547 548 } 549 550 if (eventHandler == null) { 551 if (attrValue.length() == 0 || scriptEngine == null) { 552 throw new LoadException("Error resolving " + attribute.name + "='" + attribute.value 553 + "', either the event handler is not in the Namespace or there is an error in the script."); 554 } 555 556 eventHandler = new ScriptEventHandler(attrValue, scriptEngine); 557 } 558 559 // Add the handler 560 if (eventHandler != null){ 561 addEventHandler(attribute, eventHandler); 562 } 563 } 564 } 565 } 566 567 @SuppressWarnings("unchecked") 568 private void addEventHandler(Attribute attribute, EventHandler<? extends Event> eventHandler) 569 throws LoadException { 570 if (attribute.name.endsWith(CHANGE_EVENT_HANDLER_SUFFIX)) { 571 int i = EVENT_HANDLER_PREFIX.length(); 572 int j = attribute.name.length() - CHANGE_EVENT_HANDLER_SUFFIX.length(); 573 574 if (i == j) { 575 if (value instanceof ObservableList<?>) { 576 ObservableList<Object> list = (ObservableList<Object>)value; 577 list.addListener(new ObservableListChangeAdapter(list, 578 (EventHandler<ObservableListChangeEvent<?>>)eventHandler)); 579 } else if (value instanceof ObservableMap<?, ?>) { 580 ObservableMap<Object, Object> map = (ObservableMap<Object, Object>)value; 581 map.addListener(new ObservableMapChangeAdapter(map, 582 (EventHandler<ObservableMapChangeEvent<?, ?>>)eventHandler)); 583 } else { 584 throw new LoadException("Invalid event source."); 585 } 586 } else { 587 String key = Character.toLowerCase(attribute.name.charAt(i)) 588 + attribute.name.substring(i + 1, j); 589 590 ObservableValue<Object> propertyModel = getValueAdapter().getPropertyModel(key); 591 if (propertyModel == null) { 592 throw new LoadException(value.getClass().getName() + " does not define" 593 + " a property model for \"" + key + "\"."); 594 } 595 596 propertyModel.addListener(new PropertyChangeAdapter(value, 597 (EventHandler<PropertyChangeEvent<?>>)eventHandler)); 598 } 599 } else { 600 getValueAdapter().put(attribute.name, eventHandler); 601 } 602 } 603 } 604 605 // Element representing a value 606 private abstract class ValueElement extends Element { 607 public String fx_id = null; 608 609 @Override 610 public void processStartElement() throws IOException { 611 super.processStartElement(); 612 613 updateValue(constructValue()); 614 615 if (value instanceof Builder<?>) { 616 processInstancePropertyAttributes(); 617 } else { 618 processValue(); 619 } 620 } 621 622 @Override 623 @SuppressWarnings("unchecked") 624 public void processEndElement() throws IOException { 625 super.processEndElement(); 626 627 // Build the value, if necessary 628 if (value instanceof Builder<?>) { 629 Builder<Object> builder = (Builder<Object>)value; 630 updateValue(builder.build()); 631 632 processValue(); 633 } else { 634 processInstancePropertyAttributes(); 635 } 636 637 processEventHandlerAttributes(); 638 639 // Process static property attributes 640 if (staticPropertyAttributes.size() > 0) { 641 for (Attribute attribute : staticPropertyAttributes) { 642 processPropertyAttribute(attribute); 643 } 644 } 645 646 // Process static property elements 647 if (staticPropertyElements.size() > 0) { 648 for (PropertyElement element : staticPropertyElements) { 649 BeanAdapter.put(value, element.sourceType, element.name, element.value); 650 } 651 } 652 653 if (parent != null) { 654 if (parent.isCollection()) { 655 parent.add(value); 656 } else { 657 parent.set(value); 658 } 659 } 660 } 661 662 private Object getListValue(Element parent, String listPropertyName, Object value) { 663 // If possible, coerce the value to the list item type 664 if (parent.isTyped()) { 665 Type listType = parent.getValueAdapter().getGenericType(listPropertyName); 666 667 if (listType != null) { 668 Type itemType = BeanAdapter.getGenericListItemType(listType); 669 670 if (itemType instanceof ParameterizedType) { 671 itemType = ((ParameterizedType)itemType).getRawType(); 672 } 673 674 value = BeanAdapter.coerce(value, (Class<?>)itemType); 675 } 676 } 677 678 return value; 679 } 680 681 private void processValue() throws LoadException { 682 // If this is the root element, update the value 683 if (parent == null) { 684 root = value; 685 686 // checking version of fx namespace - throw exception if not supported 687 String fxNSURI = xmlStreamReader.getNamespaceContext().getNamespaceURI("fx"); 688 if (fxNSURI != null) { 689 String fxVersion = fxNSURI.substring(fxNSURI.lastIndexOf("/") + 1); 690 if (compareJFXVersions(FX_NAMESPACE_VERSION, fxVersion) < 0) { 691 throw new LoadException("Loading FXML document of version " + 692 fxVersion + " by JavaFX runtime supporting version " + FX_NAMESPACE_VERSION); 693 } 694 } 695 696 // checking the version JavaFX API - print warning if not supported 697 String defaultNSURI = xmlStreamReader.getNamespaceContext().getNamespaceURI(""); 698 if (defaultNSURI != null) { 699 String nsVersion = defaultNSURI.substring(defaultNSURI.lastIndexOf("/") + 1); 700 if (compareJFXVersions(JAVAFX_VERSION, nsVersion) < 0) { 701 Logging.getJavaFXLogger().warning("Loading FXML document with JavaFX API of version " + 702 nsVersion + " by JavaFX runtime of version " + JAVAFX_VERSION); 703 } 704 } 705 } 706 707 // Add the value to the namespace 708 if (fx_id != null) { 709 namespace.put(fx_id, value); 710 711 // If the value defines an ID property, set it 712 IDProperty idProperty = value.getClass().getAnnotation(IDProperty.class); 713 714 if (idProperty != null) { 715 Map<String, Object> properties = getProperties(); 716 // set fx:id property value to Node.id only if Node.id was not 717 // already set when processing start element attributes 718 if (properties.get(idProperty.value()) == null) { 719 properties.put(idProperty.value(), fx_id); 720 } 721 } 722 723 // Set the controller field value 724 if (controller != null) { 725 Field field = getControllerFields().get(fx_id); 726 727 if (field != null) { 728 try { 729 field.set(controller, value); 730 } catch (IllegalAccessException exception) { 731 throw new RuntimeException(exception); 732 } 733 } 734 } 735 } 736 } 737 738 @Override 739 @SuppressWarnings("unchecked") 740 public void processCharacters() throws LoadException { 741 Class<?> type = value.getClass(); 742 DefaultProperty defaultProperty = type.getAnnotation(DefaultProperty.class); 743 744 // If the default property is a read-only list, add the value to it; 745 // otherwise, set the value as the default property 746 if (defaultProperty != null) { 747 String text = xmlStreamReader.getText(); 748 text = extraneousWhitespacePattern.matcher(text).replaceAll(" "); 749 750 String defaultPropertyName = defaultProperty.value(); 751 BeanAdapter valueAdapter = getValueAdapter(); 752 753 if (valueAdapter.isReadOnly(defaultPropertyName) 754 && List.class.isAssignableFrom(valueAdapter.getType(defaultPropertyName))) { 755 List<Object> list = (List<Object>)valueAdapter.get(defaultPropertyName); 756 list.add(getListValue(this, defaultPropertyName, text)); 757 } else { 758 valueAdapter.put(defaultPropertyName, text.trim()); 759 } 760 } else { 761 throw new LoadException(type.getName() + " does not have a default property."); 762 } 763 } 764 765 @Override 766 public void processAttribute(String prefix, String localName, String value) 767 throws IOException{ 768 if (prefix != null 769 && prefix.equals(FX_NAMESPACE_PREFIX)) { 770 if (localName.equals(FX_ID_ATTRIBUTE)) { 771 // Verify that ID is a valid identifier 772 if (value.equals(NULL_KEYWORD)) { 773 throw new LoadException("Invalid identifier."); 774 } 775 776 for (int i = 0, n = value.length(); i < n; i++) { 777 if (!Character.isJavaIdentifierPart(value.charAt(i))) { 778 throw new LoadException("Invalid identifier."); 779 } 780 } 781 782 fx_id = value; 783 784 } else if (localName.equals(FX_CONTROLLER_ATTRIBUTE)) { 785 if (current.parent != null) { 786 throw new LoadException(FX_NAMESPACE_PREFIX + ":" + FX_CONTROLLER_ATTRIBUTE 787 + " can only be applied to root element."); 788 } 789 790 if (controller != null) { 791 throw new LoadException("Controller value already specified."); 792 } 793 794 if (!staticLoad) { 795 Class<?> type; 796 try { 797 type = classLoader.loadClass(value); 798 } catch (ClassNotFoundException exception) { 799 throw new LoadException(exception); 800 } 801 802 try { 803 if (controllerFactory == null) { 804 setController(ReflectUtil.newInstance(type)); 805 } else { 806 setController(controllerFactory.call(type)); 807 } 808 } catch (InstantiationException exception) { 809 throw new LoadException(exception); 810 } catch (IllegalAccessException exception) { 811 throw new LoadException(exception); 812 } 813 } 814 } else { 815 throw new LoadException("Invalid attribute."); 816 } 817 } else { 818 super.processAttribute(prefix, localName, value); 819 } 820 } 821 822 public abstract Object constructValue() throws IOException; 823 } 824 825 // Element representing a class instance 826 private class InstanceDeclarationElement extends ValueElement { 827 public Class<?> type; 828 829 public String constant = null; 830 public String factory = null; 831 832 public InstanceDeclarationElement(Class<?> type) throws LoadException { 833 this.type = type; 834 } 835 836 @Override 837 public void processAttribute(String prefix, String localName, String value) 838 throws IOException { 839 if (prefix != null 840 && prefix.equals(FX_NAMESPACE_PREFIX)) { 841 if (localName.equals(FX_VALUE_ATTRIBUTE)) { 842 this.value = value; 843 } else if (localName.equals(FX_CONSTANT_ATTRIBUTE)) { 844 constant = value; 845 } else if (localName.equals(FX_FACTORY_ATTRIBUTE)) { 846 factory = value; 847 } else { 848 super.processAttribute(prefix, localName, value); 849 } 850 } else { 851 super.processAttribute(prefix, localName, value); 852 } 853 } 854 855 @Override 856 public Object constructValue() throws IOException { 857 Object value; 858 if (this.value != null) { 859 value = BeanAdapter.coerce(this.value, type); 860 } else if (constant != null) { 861 value = BeanAdapter.getConstantValue(type, constant); 862 } else if (factory != null) { 863 Method factoryMethod; 864 try { 865 factoryMethod = MethodUtil.getMethod(type, factory, new Class[] {}); 866 } catch (NoSuchMethodException exception) { 867 throw new LoadException(exception); 868 } 869 870 try { 871 value = MethodUtil.invoke(factoryMethod, null, new Object [] {}); 872 } catch (IllegalAccessException exception) { 873 throw new LoadException(exception); 874 } catch (InvocationTargetException exception) { 875 throw new LoadException(exception); 876 } 877 } else { 878 value = (builderFactory == null) ? null : builderFactory.getBuilder(type); 879 880 if (value == null) { 881 try { 882 value = ReflectUtil.newInstance(type); 883 } catch (InstantiationException exception) { 884 throw new LoadException(exception); 885 } catch (IllegalAccessException exception) { 886 throw new LoadException(exception); 887 } 888 } 889 } 890 891 return value; 892 } 893 } 894 895 // Element representing an unknown type 896 private class UnknownTypeElement extends ValueElement { 897 // Map type representing an unknown value 898 @DefaultProperty("items") 899 public class UnknownValueMap extends AbstractMap<String, Object> { 900 private ArrayList<?> items = new ArrayList<Object>(); 901 private HashMap<String, Object> values = new HashMap<String, Object>(); 902 903 @Override 904 public Object get(Object key) { 905 if (key == null) { 906 throw new NullPointerException(); 907 } 908 909 return (key.equals(getClass().getAnnotation(DefaultProperty.class).value())) ? 910 items : values.get(key); 911 } 912 913 @Override 914 public Object put(String key, Object value) { 915 if (key == null) { 916 throw new NullPointerException(); 917 } 918 919 if (key.equals(getClass().getAnnotation(DefaultProperty.class).value())) { 920 throw new IllegalArgumentException(); 921 } 922 923 return values.put(key, value); 924 } 925 926 @Override 927 public Set<Entry<String, Object>> entrySet() { 928 return Collections.emptySet(); 929 } 930 } 931 932 @Override 933 public void processEndElement() throws IOException { 934 // No-op 935 } 936 937 @Override 938 public Object constructValue() throws LoadException { 939 return new UnknownValueMap(); 940 } 941 } 942 943 // Element representing an include 944 private class IncludeElement extends ValueElement { 945 public String source = null; 946 public ResourceBundle resources = FXMLLoader.this.resources; 947 public Charset charset = FXMLLoader.this.charset; 948 949 @Override 950 public void processAttribute(String prefix, String localName, String value) 951 throws IOException { 952 if (prefix == null) { 953 if (localName.equals(INCLUDE_SOURCE_ATTRIBUTE)) { 954 if (loadListener != null) { 955 loadListener.readInternalAttribute(localName, value); 956 } 957 958 source = value; 959 } else if (localName.equals(INCLUDE_RESOURCES_ATTRIBUTE)) { 960 if (loadListener != null) { 961 loadListener.readInternalAttribute(localName, value); 962 } 963 964 resources = ResourceBundle.getBundle(value, Locale.getDefault(), 965 FXMLLoader.this.resources.getClass().getClassLoader()); 966 } else if (localName.equals(INCLUDE_CHARSET_ATTRIBUTE)) { 967 if (loadListener != null) { 968 loadListener.readInternalAttribute(localName, value); 969 } 970 971 charset = Charset.forName(value); 972 } else { 973 super.processAttribute(prefix, localName, value); 974 } 975 } else { 976 super.processAttribute(prefix, localName, value); 977 } 978 } 979 980 @Override 981 public Object constructValue() throws IOException { 982 if (source == null) { 983 throw new LoadException(INCLUDE_SOURCE_ATTRIBUTE + " is required."); 984 } 985 986 URL location; 987 if (source.charAt(0) == '/') { 988 location = classLoader.getResource(source.substring(1)); 989 } else { 990 if (FXMLLoader.this.location == null) { 991 throw new LoadException("Base location is undefined."); 992 } 993 994 location = new URL(FXMLLoader.this.location, source); 995 } 996 997 FXMLLoader fxmlLoader = new FXMLLoader(location, resources, 998 builderFactory, controllerFactory, charset, 999 loaders); 1000 fxmlLoader.parentLoader = FXMLLoader.this; 1001 1002 if (isCyclic(FXMLLoader.this, fxmlLoader)) { 1003 throw new IOException( 1004 String.format( 1005 "Including \"%s\" in \"%s\" created cyclic reference.", 1006 fxmlLoader.location.toExternalForm(), 1007 FXMLLoader.this.location.toExternalForm())); 1008 } 1009 fxmlLoader.setClassLoader(classLoader); 1010 fxmlLoader.setStaticLoad(staticLoad); 1011 1012 Object value = fxmlLoader.load(); 1013 1014 if (fx_id != null) { 1015 String id = this.fx_id + CONTROLLER_SUFFIX; 1016 Object controller = fxmlLoader.getController(); 1017 1018 namespace.put(id, controller); 1019 1020 if (FXMLLoader.this.controller != null) { 1021 Field field = getControllerFields().get(id); 1022 1023 if (field != null) { 1024 try { 1025 field.set(FXMLLoader.this.controller, controller); 1026 } catch (IllegalAccessException exception) { 1027 throw new LoadException(exception); 1028 } 1029 } 1030 } 1031 } 1032 1033 return value; 1034 } 1035 } 1036 1037 // Element representing a reference 1038 private class ReferenceElement extends ValueElement { 1039 public String source = null; 1040 1041 @Override 1042 public void processAttribute(String prefix, String localName, String value) 1043 throws IOException { 1044 if (prefix == null) { 1045 if (localName.equals(REFERENCE_SOURCE_ATTRIBUTE)) { 1046 if (loadListener != null) { 1047 loadListener.readInternalAttribute(localName, value); 1048 } 1049 1050 source = value; 1051 } else { 1052 super.processAttribute(prefix, localName, value); 1053 } 1054 } else { 1055 super.processAttribute(prefix, localName, value); 1056 } 1057 } 1058 1059 @Override 1060 public Object constructValue() throws LoadException { 1061 if (source == null) { 1062 throw new LoadException(REFERENCE_SOURCE_ATTRIBUTE + " is required."); 1063 } 1064 1065 KeyPath path = KeyPath.parse(source); 1066 if (!Expression.isDefined(namespace, path)) { 1067 throw new LoadException("Value \"" + source + "\" does not exist."); 1068 } 1069 1070 return Expression.get(namespace, path); 1071 } 1072 } 1073 1074 // Element representing a copy 1075 private class CopyElement extends ValueElement { 1076 public String source = null; 1077 1078 @Override 1079 public void processAttribute(String prefix, String localName, String value) 1080 throws IOException { 1081 if (prefix == null) { 1082 if (localName.equals(COPY_SOURCE_ATTRIBUTE)) { 1083 if (loadListener != null) { 1084 loadListener.readInternalAttribute(localName, value); 1085 } 1086 1087 source = value; 1088 } else { 1089 super.processAttribute(prefix, localName, value); 1090 } 1091 } else { 1092 super.processAttribute(prefix, localName, value); 1093 } 1094 } 1095 1096 @Override 1097 public Object constructValue() throws LoadException { 1098 if (source == null) { 1099 throw new LoadException(COPY_SOURCE_ATTRIBUTE + " is required."); 1100 } 1101 1102 KeyPath path = KeyPath.parse(source); 1103 if (!Expression.isDefined(namespace, path)) { 1104 throw new LoadException("Value \"" + source + "\" does not exist."); 1105 } 1106 1107 Object sourceValue = Expression.get(namespace, path); 1108 Class<?> sourceValueType = sourceValue.getClass(); 1109 1110 Constructor<?> constructor = null; 1111 try { 1112 constructor = ConstructorUtil.getConstructor(sourceValueType, new Class[] { sourceValueType }); 1113 } catch (NoSuchMethodException exception) { 1114 // No-op 1115 } 1116 1117 Object value; 1118 if (constructor != null) { 1119 try { 1120 ReflectUtil.checkPackageAccess(sourceValueType); 1121 value = constructor.newInstance(sourceValue); 1122 } catch (InstantiationException exception) { 1123 throw new LoadException(exception); 1124 } catch (IllegalAccessException exception) { 1125 throw new LoadException(exception); 1126 } catch (InvocationTargetException exception) { 1127 throw new LoadException(exception); 1128 } 1129 } else { 1130 throw new LoadException("Can't copy value " + sourceValue + "."); 1131 } 1132 1133 return value; 1134 } 1135 } 1136 1137 // Element representing a predefined root value 1138 private class RootElement extends ValueElement { 1139 public String type = null; 1140 1141 @Override 1142 public void processAttribute(String prefix, String localName, String value) 1143 throws IOException { 1144 if (prefix == null) { 1145 if (localName.equals(ROOT_TYPE_ATTRIBUTE)) { 1146 if (loadListener != null) { 1147 loadListener.readInternalAttribute(localName, value); 1148 } 1149 1150 type = value; 1151 } else { 1152 super.processAttribute(prefix, localName, value); 1153 } 1154 } else { 1155 super.processAttribute(prefix, localName, value); 1156 } 1157 } 1158 1159 @Override 1160 public Object constructValue() throws LoadException { 1161 if (type == null) { 1162 throw new LoadException(ROOT_TYPE_ATTRIBUTE + " is required."); 1163 } 1164 1165 Class<?> type = getType(this.type); 1166 1167 if (type == null) { 1168 throw new LoadException(this.type + " is not a valid type."); 1169 } 1170 1171 Object value; 1172 if (root == null) { 1173 throw new LoadException("Root hasn't been set. Use method setRoot() before load."); 1174 } else { 1175 if (!type.isAssignableFrom(root.getClass())) { 1176 throw new LoadException("Root is not an instance of " 1177 + type.getName() + "."); 1178 } 1179 1180 value = root; 1181 } 1182 1183 return value; 1184 } 1185 } 1186 1187 // Element representing a property 1188 private class PropertyElement extends Element { 1189 public final String name; 1190 public final Class<?> sourceType; 1191 public final boolean readOnly; 1192 1193 public PropertyElement(String name, Class<?> sourceType) throws LoadException { 1194 if (parent == null) { 1195 throw new LoadException("Invalid root element."); 1196 } 1197 1198 if (parent.value == null) { 1199 throw new LoadException("Parent element does not support property elements."); 1200 } 1201 1202 this.name = name; 1203 this.sourceType = sourceType; 1204 1205 if (sourceType == null) { 1206 // The element represents an instance property 1207 if (name.startsWith(EVENT_HANDLER_PREFIX)) { 1208 throw new LoadException("\"" + name + "\" is not a valid element name."); 1209 } 1210 1211 Map<String, Object> parentProperties = parent.getProperties(); 1212 1213 if (parent.isTyped()) { 1214 readOnly = parent.getValueAdapter().isReadOnly(name); 1215 } else { 1216 // If the map already defines a value for the property, assume 1217 // that it is read-only 1218 readOnly = parentProperties.containsKey(name); 1219 } 1220 1221 if (readOnly) { 1222 Object value = parentProperties.get(name); 1223 if (value == null) { 1224 throw new LoadException("Invalid property."); 1225 } 1226 1227 updateValue(value); 1228 } 1229 } else { 1230 // The element represents a static property 1231 readOnly = false; 1232 } 1233 } 1234 1235 @Override 1236 public boolean isCollection() { 1237 return (readOnly) ? super.isCollection() : false; 1238 } 1239 1240 @Override 1241 public void add(Object element) throws LoadException { 1242 // Coerce the element to the list item type 1243 if (parent.isTyped()) { 1244 Type listType = parent.getValueAdapter().getGenericType(name); 1245 element = BeanAdapter.coerce(element, BeanAdapter.getListItemType(listType)); 1246 } 1247 1248 // Add the item to the list 1249 super.add(element); 1250 } 1251 1252 @Override 1253 public void set(Object value) throws LoadException { 1254 // Update the value 1255 updateValue(value); 1256 1257 if (sourceType == null) { 1258 // Apply value to parent element's properties 1259 parent.getProperties().put(name, value); 1260 } else { 1261 if (parent.value instanceof Builder) { 1262 // Defer evaluation of the property 1263 parent.staticPropertyElements.add(this); 1264 } else { 1265 // Apply the static property value 1266 BeanAdapter.put(parent.value, sourceType, name, value); 1267 } 1268 } 1269 } 1270 1271 @Override 1272 public void processAttribute(String prefix, String localName, String value) 1273 throws IOException { 1274 if (!readOnly) { 1275 throw new LoadException("Attributes are not supported for writable property elements."); 1276 } 1277 1278 super.processAttribute(prefix, localName, value); 1279 } 1280 1281 @Override 1282 public void processEndElement() throws IOException { 1283 super.processEndElement(); 1284 1285 if (readOnly) { 1286 processInstancePropertyAttributes(); 1287 processEventHandlerAttributes(); 1288 } 1289 } 1290 1291 @Override 1292 public void processCharacters() throws IOException { 1293 if (!readOnly) { 1294 String text = xmlStreamReader.getText(); 1295 text = extraneousWhitespacePattern.matcher(text).replaceAll(" "); 1296 1297 set(text.trim()); 1298 } else { 1299 super.processCharacters(); 1300 } 1301 } 1302 } 1303 1304 // Element representing an unknown static property 1305 private class UnknownStaticPropertyElement extends Element { 1306 public UnknownStaticPropertyElement() throws LoadException { 1307 if (parent == null) { 1308 throw new LoadException("Invalid root element."); 1309 } 1310 1311 if (parent.value == null) { 1312 throw new LoadException("Parent element does not support property elements."); 1313 } 1314 } 1315 1316 @Override 1317 public boolean isCollection() { 1318 return false; 1319 } 1320 1321 @Override 1322 public void set(Object value) { 1323 updateValue(value); 1324 } 1325 1326 @Override 1327 public void processCharacters() throws IOException { 1328 String text = xmlStreamReader.getText(); 1329 text = extraneousWhitespacePattern.matcher(text).replaceAll(" "); 1330 1331 updateValue(text.trim()); 1332 } 1333 } 1334 1335 // Element representing a script block 1336 private class ScriptElement extends Element { 1337 public String source = null; 1338 public Charset charset = FXMLLoader.this.charset; 1339 1340 @Override 1341 public boolean isCollection() { 1342 return false; 1343 } 1344 1345 @Override 1346 public void processStartElement() throws IOException { 1347 super.processStartElement(); 1348 1349 if (source != null && !staticLoad) { 1350 int i = source.lastIndexOf("."); 1351 if (i == -1) { 1352 throw new LoadException("Cannot determine type of script \"" 1353 + source + "\"."); 1354 } 1355 1356 String extension = source.substring(i + 1); 1357 ScriptEngine scriptEngine; 1358 ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); 1359 try { 1360 Thread.currentThread().setContextClassLoader(classLoader); 1361 ScriptEngineManager scriptEngineManager = getScriptEngineManager(); 1362 scriptEngine = scriptEngineManager.getEngineByExtension(extension); 1363 } finally { 1364 Thread.currentThread().setContextClassLoader(oldLoader); 1365 } 1366 1367 if (scriptEngine == null) { 1368 throw new LoadException("Unable to locate scripting engine for" 1369 + " extension " + extension + "."); 1370 } 1371 1372 scriptEngine.setBindings(scriptEngineManager.getBindings(), ScriptContext.ENGINE_SCOPE); 1373 1374 try { 1375 URL location; 1376 if (source.charAt(0) == '/') { 1377 location = classLoader.getResource(source.substring(1)); 1378 } else { 1379 if (FXMLLoader.this.location == null) { 1380 throw new LoadException("Base location is undefined."); 1381 } 1382 1383 location = new URL(FXMLLoader.this.location, source); 1384 } 1385 1386 InputStreamReader scriptReader = null; 1387 try { 1388 scriptReader = new InputStreamReader(location.openStream(), charset); 1389 scriptEngine.eval(scriptReader); 1390 } catch(ScriptException exception) { 1391 exception.printStackTrace(); 1392 } finally { 1393 if (scriptReader != null) { 1394 scriptReader.close(); 1395 } 1396 } 1397 } catch (IOException exception) { 1398 throw new LoadException(exception); 1399 } 1400 } 1401 } 1402 1403 @Override 1404 public void processEndElement() throws IOException { 1405 super.processEndElement(); 1406 1407 if (value != null && !staticLoad) { 1408 // Evaluate the script 1409 try { 1410 scriptEngine.eval((String)value); 1411 } catch (ScriptException exception) { 1412 System.err.println(exception.getMessage()); 1413 } 1414 } 1415 } 1416 1417 @Override 1418 public void processCharacters() throws LoadException { 1419 if (source != null) { 1420 throw new LoadException("Script source already specified."); 1421 } 1422 1423 if (scriptEngine == null && !staticLoad) { 1424 throw new LoadException("Page language not specified."); 1425 } 1426 1427 updateValue(xmlStreamReader.getText()); 1428 } 1429 1430 @Override 1431 public void processAttribute(String prefix, String localName, String value) 1432 throws IOException { 1433 if (prefix == null 1434 && localName.equals(SCRIPT_SOURCE_ATTRIBUTE)) { 1435 if (loadListener != null) { 1436 loadListener.readInternalAttribute(localName, value); 1437 } 1438 1439 source = value; 1440 } else if (localName.equals(SCRIPT_CHARSET_ATTRIBUTE)) { 1441 if (loadListener != null) { 1442 loadListener.readInternalAttribute(localName, value); 1443 } 1444 1445 charset = Charset.forName(value); 1446 } else { 1447 throw new LoadException(prefix == null ? localName : prefix + ":" + localName 1448 + " is not a valid attribute."); 1449 } 1450 } 1451 } 1452 1453 // Element representing a define block 1454 private class DefineElement extends Element { 1455 @Override 1456 public boolean isCollection() { 1457 return true; 1458 } 1459 1460 @Override 1461 public void add(Object element) { 1462 // No-op 1463 } 1464 1465 @Override 1466 public void processAttribute(String prefix, String localName, String value) 1467 throws LoadException{ 1468 throw new LoadException("Element does not support attributes."); 1469 } 1470 } 1471 1472 // Class representing an attribute of an element 1473 private static class Attribute { 1474 public final String name; 1475 public final Class<?> sourceType; 1476 public final String value; 1477 1478 public Attribute(String name, Class<?> sourceType, String value) { 1479 this.name = name; 1480 this.sourceType = sourceType; 1481 this.value = value; 1482 } 1483 } 1484 1485 // Event handler that delegates to a method defined by the controller object 1486 private static class ControllerMethodEventHandler implements EventHandler<Event> { 1487 public final Object controller; 1488 public final Method method; 1489 public final boolean typed; 1490 1491 public ControllerMethodEventHandler(Object controller, Method method) { 1492 this.controller = controller; 1493 this.method = method; 1494 this.typed = (method.getParameterTypes().length == 1); 1495 } 1496 1497 @Override 1498 public void handle(Event event) { 1499 try { 1500 if (typed) { 1501 MethodUtil.invoke(method, controller, new Object[] { event }); 1502 } else { 1503 MethodUtil.invoke(method, controller, new Object[] {}); 1504 } 1505 } catch (InvocationTargetException exception) { 1506 throw new RuntimeException(exception); 1507 } catch (IllegalAccessException exception) { 1508 throw new RuntimeException(exception); 1509 } 1510 } 1511 } 1512 1513 // Event handler implemented in script code 1514 private static class ScriptEventHandler implements EventHandler<Event> { 1515 public final String script; 1516 public final ScriptEngine scriptEngine; 1517 1518 public ScriptEventHandler(String script, ScriptEngine scriptEngine) { 1519 this.script = script; 1520 this.scriptEngine = scriptEngine; 1521 } 1522 1523 @Override 1524 public void handle(Event event) { 1525 // Don't pollute the page namespace with values defined in the script 1526 Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE); 1527 Bindings localBindings = scriptEngine.createBindings(); 1528 localBindings.put(EVENT_KEY, event); 1529 scriptEngine.setBindings(localBindings, ScriptContext.ENGINE_SCOPE); 1530 1531 // Execute the script 1532 try { 1533 scriptEngine.eval(script); 1534 } catch (ScriptException exception){ 1535 throw new RuntimeException(exception); 1536 } 1537 1538 // Restore the original bindings 1539 scriptEngine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE); 1540 } 1541 } 1542 1543 // Observable list change listener 1544 private static class ObservableListChangeAdapter implements ListChangeListener<Object> { 1545 public final ObservableList<Object> source; 1546 public final EventHandler<ObservableListChangeEvent<?>> handler; 1547 1548 public ObservableListChangeAdapter(ObservableList<Object> source, 1549 EventHandler<ObservableListChangeEvent<?>> handler) { 1550 this.source = source; 1551 this.handler = handler; 1552 } 1553 1554 @Override 1555 @SuppressWarnings("unchecked") 1556 public void onChanged(Change<? extends Object> change) { 1557 while (change.next()) { 1558 EventType<ObservableListChangeEvent<?>> eventType; 1559 List<Object> removed = (List<Object>)change.getRemoved(); 1560 1561 if (change.wasPermutated()) { 1562 eventType = ObservableListChangeEvent.UPDATE; 1563 removed = null; 1564 } else if (change.wasAdded() && change.wasRemoved()) { 1565 eventType = ObservableListChangeEvent.UPDATE; 1566 } else if (change.wasAdded()) { 1567 eventType = ObservableListChangeEvent.ADD; 1568 } else if (change.wasRemoved()) { 1569 eventType = ObservableListChangeEvent.REMOVE; 1570 } else { 1571 throw new UnsupportedOperationException(); 1572 } 1573 1574 handler.handle(new ObservableListChangeEvent<Object>(source, 1575 eventType, change.getFrom(), change.getTo(), 1576 removed)); 1577 } 1578 } 1579 } 1580 1581 // Observable map change listener 1582 private static class ObservableMapChangeAdapter implements MapChangeListener<Object, Object> { 1583 public final ObservableMap<Object, Object> source; 1584 public final EventHandler<ObservableMapChangeEvent<?, ?>> handler; 1585 1586 public ObservableMapChangeAdapter(ObservableMap<Object, Object> source, 1587 EventHandler<ObservableMapChangeEvent<?, ?>> handler) { 1588 this.source = source; 1589 this.handler = handler; 1590 } 1591 1592 @Override 1593 public void onChanged(Change<? extends Object, ? extends Object> change) { 1594 EventType<ObservableMapChangeEvent<?, ?>> eventType; 1595 if (change.wasAdded() && change.wasRemoved()) { 1596 eventType = ObservableMapChangeEvent.UPDATE; 1597 } else if (change.wasAdded()) { 1598 eventType = ObservableMapChangeEvent.ADD; 1599 } else if (change.wasRemoved()) { 1600 eventType = ObservableMapChangeEvent.REMOVE; 1601 } else { 1602 throw new UnsupportedOperationException(); 1603 } 1604 1605 handler.handle(new ObservableMapChangeEvent<Object, Object>(source, 1606 eventType, change.getKey(), change.getValueRemoved())); 1607 } 1608 } 1609 1610 // Property model change listener 1611 private static class PropertyChangeAdapter implements ChangeListener<Object> { 1612 public final Object source; 1613 public final EventHandler<PropertyChangeEvent<?>> handler; 1614 1615 public PropertyChangeAdapter(Object source, EventHandler<PropertyChangeEvent<?>> handler) { 1616 if (source == null) { 1617 throw new NullPointerException(); 1618 } 1619 1620 if (handler == null) { 1621 throw new NullPointerException(); 1622 } 1623 1624 this.source = source; 1625 this.handler = handler; 1626 } 1627 1628 @Override 1629 public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) { 1630 handler.handle(new PropertyChangeEvent<Object>(source, oldValue)); 1631 } 1632 } 1633 1634 protected URL location; 1635 protected ResourceBundle resources; 1636 1637 private ObservableMap<String, Object> namespace = FXCollections.observableHashMap(); 1638 1639 protected Object root = null; 1640 protected Object controller = null; 1641 1642 private BuilderFactory builderFactory; 1643 private Callback<Class<?>, Object> controllerFactory; 1644 private Charset charset; 1645 1646 private LinkedList<FXMLLoader> loaders; 1647 1648 private ClassLoader classLoader = defaultClassLoader; 1649 private boolean staticLoad = false; 1650 private LoadListener loadListener = null; 1651 1652 private FXMLLoader parentLoader; 1653 1654 private XMLStreamReader xmlStreamReader = null; 1655 private Element current = null; 1656 1657 private ScriptEngine scriptEngine = null; 1658 1659 private boolean template = false; 1660 1661 private LinkedList<String> packages = new LinkedList<String>(); 1662 private HashMap<String, Class<?>> classes = new HashMap<String, Class<?>>(); 1663 1664 private HashMap<String, Field> controllerFields = null; 1665 private HashMap<String, Method> controllerMethods = null; 1666 1667 private ScriptEngineManager scriptEngineManager = null; 1668 1669 private static ClassLoader defaultClassLoader; 1670 1671 private static final Pattern extraneousWhitespacePattern = Pattern.compile("\\s+"); 1672 1673 public static final String DEFAULT_CHARSET_NAME = "UTF-8"; 1674 1675 public static final String LANGUAGE_PROCESSING_INSTRUCTION = "language"; 1676 public static final String IMPORT_PROCESSING_INSTRUCTION = "import"; 1677 1678 public static final String FX_NAMESPACE_PREFIX = "fx"; 1679 public static final String FX_CONTROLLER_ATTRIBUTE = "controller"; 1680 public static final String FX_ID_ATTRIBUTE = "id"; 1681 public static final String FX_VALUE_ATTRIBUTE = "value"; 1682 public static final String FX_CONSTANT_ATTRIBUTE = "constant"; 1683 public static final String FX_FACTORY_ATTRIBUTE = "factory"; 1684 1685 public static final String INCLUDE_TAG = "include"; 1686 public static final String INCLUDE_SOURCE_ATTRIBUTE = "source"; 1687 public static final String INCLUDE_RESOURCES_ATTRIBUTE = "resources"; 1688 public static final String INCLUDE_CHARSET_ATTRIBUTE = "charset"; 1689 1690 public static final String SCRIPT_TAG = "script"; 1691 public static final String SCRIPT_SOURCE_ATTRIBUTE = "source"; 1692 public static final String SCRIPT_CHARSET_ATTRIBUTE = "charset"; 1693 1694 public static final String DEFINE_TAG = "define"; 1695 1696 public static final String REFERENCE_TAG = "reference"; 1697 public static final String REFERENCE_SOURCE_ATTRIBUTE = "source"; 1698 1699 public static final String ROOT_TAG = "root"; 1700 public static final String ROOT_TYPE_ATTRIBUTE = "type"; 1701 1702 public static final String COPY_TAG = "copy"; 1703 public static final String COPY_SOURCE_ATTRIBUTE = "source"; 1704 1705 public static final String EVENT_HANDLER_PREFIX = "on"; 1706 public static final String EVENT_KEY = "event"; 1707 public static final String CHANGE_EVENT_HANDLER_SUFFIX = "Change"; 1708 1709 public static final String NULL_KEYWORD = "null"; 1710 1711 public static final String ESCAPE_PREFIX = "\\"; 1712 public static final String RELATIVE_PATH_PREFIX = "@"; 1713 public static final String RESOURCE_KEY_PREFIX = "%"; 1714 public static final String EXPRESSION_PREFIX = "$"; 1715 public static final String BINDING_EXPRESSION_PREFIX = "${"; 1716 public static final String BINDING_EXPRESSION_SUFFIX = "}"; 1717 1718 public static final String BI_DIRECTIONAL_BINDING_PREFIX = "#{"; 1719 public static final String BI_DIRECTIONAL_BINDING_SUFFIX = "}"; 1720 1721 public static final String ARRAY_COMPONENT_DELIMITER = ","; 1722 1723 public static final String LOCATION_KEY = "location"; 1724 public static final String RESOURCES_KEY = "resources"; 1725 1726 public static final String CONTROLLER_METHOD_PREFIX = "#"; 1727 public static final String CONTROLLER_KEYWORD = "controller"; 1728 public static final String CONTROLLER_SUFFIX = "Controller"; 1729 1730 public static final String INITIALIZE_METHOD_NAME = "initialize"; 1731 1732 public static final String JAVAFX_VERSION; 1733 1734 public static final String FX_NAMESPACE_VERSION = "1"; 1735 1736 static { 1737 defaultClassLoader = Thread.currentThread().getContextClassLoader(); 1738 1739 if (defaultClassLoader == null) { 1740 throw new NullPointerException(); 1741 } 1742 1743 JAVAFX_VERSION = AccessController.doPrivileged(new PrivilegedAction<String>() { 1744 @Override 1745 public String run() { 1746 return System.getProperty("javafx.version"); 1747 } 1748 }); 1749 } 1750 1751 /** 1752 * Creates a new FXMLLoader instance. 1753 */ 1754 public FXMLLoader() { 1755 this((URL)null); 1756 } 1757 1758 /** 1759 * Creates a new FXMLLoader instance. 1760 * 1761 * @param location 1762 */ 1763 public FXMLLoader(URL location) { 1764 this(location, null); 1765 } 1766 1767 /** 1768 * Creates a new FXMLLoader instance. 1769 * 1770 * @param location 1771 * @param resources 1772 */ 1773 public FXMLLoader(URL location, ResourceBundle resources) { 1774 this(location, resources, new JavaFXBuilderFactory()); 1775 } 1776 1777 /** 1778 * Creates a new FXMLLoader instance. 1779 * 1780 * @param location 1781 * @param resources 1782 * @param builderFactory 1783 */ 1784 public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory) { 1785 this(location, resources, builderFactory, null); 1786 } 1787 1788 /** 1789 * Creates a new FXMLLoader instance. 1790 * 1791 * @param location 1792 * @param resources 1793 * @param builderFactory 1794 * @param controllerFactory 1795 */ 1796 public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory, 1797 Callback<Class<?>, Object> controllerFactory) { 1798 this(location, resources, builderFactory, controllerFactory, Charset.forName(DEFAULT_CHARSET_NAME)); 1799 } 1800 1801 /** 1802 * Creates a new FXMLLoader instance. 1803 * 1804 * @param charset 1805 */ 1806 public FXMLLoader(Charset charset) { 1807 this(null, null, null, null, charset); 1808 } 1809 1810 /** 1811 * Creates a new FXMLLoader instance. 1812 * 1813 * @param location 1814 * @param resources 1815 * @param builderFactory 1816 * @param controllerFactory 1817 * @param charset 1818 */ 1819 public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory, 1820 Callback<Class<?>, Object> controllerFactory, Charset charset) { 1821 this(location, resources, builderFactory, controllerFactory, charset, 1822 new LinkedList<FXMLLoader>()); 1823 } 1824 1825 /** 1826 * Creates a new FXMLLoader instance. 1827 * 1828 * @param location 1829 * @param resources 1830 * @param builderFactory 1831 * @param controllerFactory 1832 * @param charset 1833 * @param loaders 1834 */ 1835 public FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory, 1836 Callback<Class<?>, Object> controllerFactory, Charset charset, 1837 LinkedList<FXMLLoader> loaders) { 1838 setLocation(location); 1839 setResources(resources); 1840 setBuilderFactory(builderFactory); 1841 setControllerFactory(controllerFactory); 1842 setCharset(charset); 1843 1844 this.loaders = loaders; 1845 } 1846 1847 /** 1848 * Returns the location used to resolve relative path attribute values. 1849 */ 1850 public URL getLocation() { 1851 return location; 1852 } 1853 1854 /** 1855 * Sets the location used to resolve relative path attribute values. 1856 * 1857 * @param location 1858 */ 1859 public void setLocation(URL location) { 1860 this.location = location; 1861 } 1862 1863 /** 1864 * Returns the resources used to resolve resource key attribute values. 1865 */ 1866 public ResourceBundle getResources() { 1867 return resources; 1868 } 1869 1870 /** 1871 * Sets the resources used to resolve resource key attribute values. 1872 * 1873 * @param resources 1874 */ 1875 public void setResources(ResourceBundle resources) { 1876 this.resources = resources; 1877 } 1878 1879 /** 1880 * Returns the namespace used by this loader. 1881 */ 1882 public ObservableMap<String, Object> getNamespace() { 1883 return namespace; 1884 } 1885 1886 /** 1887 * Returns the root of the object hierarchy. 1888 */ 1889 @SuppressWarnings("unchecked") 1890 public <T> T getRoot() { 1891 return (T)root; 1892 } 1893 1894 /** 1895 * Sets the root of the object hierarchy. The value passed to this method 1896 * is used as the value of the <tt><fx:root></tt> tag. This method 1897 * must be called prior to loading the document when using 1898 * <tt><fx:root></tt>. 1899 * 1900 * @param root 1901 * The root of the object hierarchy. 1902 */ 1903 public void setRoot(Object root) { 1904 this.root = root; 1905 } 1906 1907 @Override 1908 public boolean equals(Object obj) { 1909 if (obj instanceof FXMLLoader) { 1910 FXMLLoader loader = (FXMLLoader)obj; 1911 return loader.location.toExternalForm().equals( 1912 location.toExternalForm()); 1913 } 1914 return false; 1915 } 1916 1917 private boolean isCyclic( 1918 FXMLLoader currentLoader, 1919 FXMLLoader node) { 1920 if (currentLoader == null) { 1921 return false; 1922 } 1923 if (currentLoader.equals(node)) { 1924 return true; 1925 } 1926 return isCyclic(currentLoader.parentLoader, node); 1927 } 1928 1929 /** 1930 * Returns the controller associated with the root object. 1931 */ 1932 @SuppressWarnings("unchecked") 1933 public <T> T getController() { 1934 return (T)controller; 1935 } 1936 1937 /** 1938 * Sets the controller associated with the root object. The value passed to 1939 * this method is used as the value of the <tt>fx:controller</tt> attribute. 1940 * This method must be called prior to loading the document when using 1941 * controller event handlers when an <tt>fx:controller</tt> attribute is not 1942 * specified in the document. 1943 * 1944 * @param controller 1945 * The controller to associate with the root object. 1946 */ 1947 public void setController(Object controller) { 1948 this.controller = controller; 1949 1950 if (controller == null) { 1951 namespace.remove(CONTROLLER_KEYWORD); 1952 } else { 1953 namespace.put(CONTROLLER_KEYWORD, controller); 1954 } 1955 1956 controllerFields = null; 1957 controllerMethods = null; 1958 } 1959 1960 /** 1961 * Returns the template flag. 1962 */ 1963 public boolean isTemplate() { 1964 return template; 1965 } 1966 1967 /** 1968 * Sets the template flag. Setting this value to <tt>true</tt> can improve 1969 * performance when using a single loader instance to reload the same FXML 1970 * document multiple times. See the documentation for the {@link #load()} 1971 * method for more information. 1972 * 1973 * @param template 1974 * The template flag. 1975 */ 1976 public void setTemplate(boolean template) { 1977 this.template = template; 1978 } 1979 1980 /** 1981 * Returns the builder factory used by this loader. 1982 */ 1983 public BuilderFactory getBuilderFactory() { 1984 return builderFactory; 1985 } 1986 1987 /** 1988 * Sets the builder factory used by this loader. 1989 * 1990 * @param builderFactory 1991 */ 1992 public void setBuilderFactory(BuilderFactory builderFactory) { 1993 this.builderFactory = builderFactory; 1994 } 1995 1996 /** 1997 * Returns the controller factory used by this serializer. 1998 */ 1999 public Callback<Class<?>, Object> getControllerFactory() { 2000 return controllerFactory; 2001 } 2002 2003 /** 2004 * Sets the controller factory used by this serializer. 2005 * 2006 * @param controllerFactory 2007 */ 2008 public void setControllerFactory(Callback<Class<?>, Object> controllerFactory) { 2009 this.controllerFactory = controllerFactory; 2010 } 2011 2012 /** 2013 * Returns the character set used by this loader. 2014 */ 2015 public Charset getCharset() { 2016 return charset; 2017 } 2018 2019 /** 2020 * Sets the charset used by this loader. 2021 * 2022 * @param charset 2023 */ 2024 public void setCharset(Charset charset) { 2025 if (charset == null) { 2026 throw new NullPointerException("charset is null."); 2027 } 2028 2029 this.charset = charset; 2030 } 2031 2032 /** 2033 * Returns the classloader used by this serializer. 2034 */ 2035 public ClassLoader getClassLoader() { 2036 return classLoader; 2037 } 2038 2039 /** 2040 * Sets the classloader used by this serializer and clears any existing 2041 * imports (see {@link #setTemplate(boolean)}). 2042 * 2043 * @param classLoader 2044 */ 2045 public void setClassLoader(ClassLoader classLoader) { 2046 if (classLoader == null) { 2047 throw new IllegalArgumentException(); 2048 } 2049 2050 this.classLoader = classLoader; 2051 2052 clearImports(); 2053 } 2054 2055 /** 2056 * Returns the static load flag. 2057 * 2058 * @treatAsPrivate 2059 * @deprecated 2060 */ 2061 public boolean isStaticLoad() { 2062 // SB-dependency: RT-21226 has been filed to track this 2063 return staticLoad; 2064 } 2065 2066 /** 2067 * Sets the static load flag. 2068 * 2069 * @param staticLoad 2070 * 2071 * @treatAsPrivate 2072 * @deprecated 2073 */ 2074 public void setStaticLoad(boolean staticLoad) { 2075 // SB-dependency: RT-21226 has been filed to track this 2076 this.staticLoad = staticLoad; 2077 } 2078 2079 /** 2080 * Returns this loader's load listener. 2081 * 2082 * @treatAsPrivate 2083 * @deprecated 2084 */ 2085 public LoadListener getLoadListener() { 2086 // SB-dependency: RT-21228 has been filed to track this 2087 return loadListener; 2088 } 2089 2090 /** 2091 * Sets this loader's load listener. 2092 * 2093 * @param loadListener 2094 * 2095 * @treatAsPrivate 2096 * @deprecated 2097 */ 2098 public void setLoadListener(LoadListener loadListener) { 2099 // SB-dependency: RT-21228 has been filed to track this 2100 this.loadListener = loadListener; 2101 } 2102 2103 /** 2104 * Loads an object hierarchy from a FXML document. The location from which 2105 * the document will be loaded must have been set by a prior call to 2106 * {@link #setLocation(URL)}. 2107 * <p> 2108 * When the "template" flag is set to <tt>false</tt> (the default), this 2109 * method will clear the imports before loading the document's content. 2110 * When "template" is <tt>true</tt>, the imports will not be cleared, and 2111 * the root value will be set to <tt>null</tt> before the content is 2112 * loaded. This helps improve performance on subsequent loads by 2113 * eliminating the overhead of loading the classes referred to by the 2114 * document. 2115 * 2116 * @return 2117 * The loaded object hierarchy. 2118 */ 2119 public Object load() throws IOException { 2120 if (location == null) { 2121 throw new IllegalStateException("Location is not set."); 2122 } 2123 2124 InputStream inputStream = null; 2125 Object value; 2126 try { 2127 inputStream = location.openStream(); 2128 value = load(inputStream); 2129 } catch (IOException exception) { 2130 logException(exception); 2131 throw exception; 2132 } catch (RuntimeException exception) { 2133 logException(exception); 2134 throw exception; 2135 } finally { 2136 if (inputStream != null) { 2137 inputStream.close(); 2138 } 2139 } 2140 2141 return value; 2142 } 2143 2144 /** 2145 * Loads an object hierarchy from a FXML document. 2146 * 2147 * @param inputStream 2148 * An input stream containing the FXML data to load. 2149 * 2150 * @return 2151 * The loaded object hierarchy. 2152 */ 2153 @SuppressWarnings("dep-ann") 2154 public Object load(InputStream inputStream) throws IOException { 2155 if (inputStream == null) { 2156 throw new NullPointerException("inputStream is null."); 2157 } 2158 2159 if (template) { 2160 setRoot(null); 2161 } else { 2162 clearImports(); 2163 } 2164 2165 // Initialize the namespace 2166 namespace.put(LOCATION_KEY, location); 2167 namespace.put(RESOURCES_KEY, resources); 2168 2169 // Clear the script engine 2170 scriptEngine = null; 2171 2172 // Create the parser 2173 try { 2174 XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); 2175 xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true); 2176 2177 // Some stream readers incorrectly report an empty string as the prefix 2178 // for the default namespace; correct this as needed 2179 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset); 2180 xmlStreamReader = new StreamReaderDelegate(xmlInputFactory.createXMLStreamReader(inputStreamReader)) { 2181 @Override 2182 public String getPrefix() { 2183 String prefix = super.getPrefix(); 2184 2185 if (prefix != null 2186 && prefix.length() == 0) { 2187 prefix = null; 2188 } 2189 2190 return prefix; 2191 } 2192 2193 @Override 2194 public String getAttributePrefix(int index) { 2195 String attributePrefix = super.getAttributePrefix(index); 2196 2197 if (attributePrefix != null 2198 && attributePrefix.length() == 0) { 2199 attributePrefix = null; 2200 } 2201 2202 return attributePrefix; 2203 } 2204 }; 2205 } catch (XMLStreamException exception) { 2206 throw new LoadException(exception); 2207 } 2208 2209 // Push this loader onto the stack 2210 loaders.push(this); 2211 2212 // Parse the XML stream 2213 try { 2214 while (xmlStreamReader.hasNext()) { 2215 int event = xmlStreamReader.next(); 2216 2217 switch (event) { 2218 case XMLStreamConstants.PROCESSING_INSTRUCTION: { 2219 processProcessingInstruction(); 2220 break; 2221 } 2222 2223 case XMLStreamConstants.COMMENT: { 2224 processComment(); 2225 break; 2226 } 2227 2228 case XMLStreamConstants.START_ELEMENT: { 2229 processStartElement(); 2230 break; 2231 } 2232 2233 case XMLStreamConstants.END_ELEMENT: { 2234 processEndElement(); 2235 break; 2236 } 2237 2238 case XMLStreamConstants.CHARACTERS: { 2239 processCharacters(); 2240 break; 2241 } 2242 } 2243 } 2244 } catch (XMLStreamException exception) { 2245 throw new LoadException(exception); 2246 } 2247 2248 if (controller != null) { 2249 if (controller instanceof Initializable) { 2250 ((Initializable)controller).initialize(location, resources); 2251 } else { 2252 // Inject controller fields 2253 HashMap<String, Field> controllerFields = getControllerFields(); 2254 2255 Field locationField = controllerFields.get(LOCATION_KEY); 2256 if (locationField != null) { 2257 try { 2258 locationField.set(controller, location); 2259 } catch (IllegalAccessException exception) { 2260 // TODO Throw when Initializable is deprecated/removed 2261 // throw new LoadException(exception); 2262 } 2263 } 2264 2265 Field resourcesField = controllerFields.get(RESOURCES_KEY); 2266 if (resourcesField != null) { 2267 try { 2268 resourcesField.set(controller, resources); 2269 } catch (IllegalAccessException exception) { 2270 // TODO Throw when Initializable is deprecated/removed 2271 // throw new LoadException(exception); 2272 } 2273 } 2274 2275 // Initialize the controller 2276 Method initializeMethod = getControllerMethods().get(INITIALIZE_METHOD_NAME); 2277 2278 if (initializeMethod != null) { 2279 try { 2280 MethodUtil.invoke(initializeMethod, controller, new Object [] {}); 2281 } catch (IllegalAccessException exception) { 2282 // TODO Throw when Initializable is deprecated/removed 2283 // throw new LoadException(exception); 2284 } catch (InvocationTargetException exception) { 2285 throw new LoadException(exception); 2286 } 2287 } 2288 } 2289 } 2290 2291 // Pop this loader off of the stack 2292 loaders.pop(); 2293 2294 // Clear the parser 2295 xmlStreamReader = null; 2296 2297 return root; 2298 } 2299 2300 private void clearImports() { 2301 packages.clear(); 2302 classes.clear(); 2303 } 2304 2305 private void logException(Exception exception) { 2306 String message = exception.getMessage(); 2307 if (message == null) { 2308 message = exception.getClass().getName(); 2309 } 2310 2311 StringBuilder messageBuilder = new StringBuilder(message); 2312 messageBuilder.append("\n"); 2313 2314 for (FXMLLoader loader : loaders) { 2315 messageBuilder.append(loader.location.getPath()); 2316 2317 if (loader.current != null) { 2318 messageBuilder.append(":"); 2319 messageBuilder.append(loader.current.lineNumber); 2320 } 2321 2322 messageBuilder.append("\n"); 2323 } 2324 2325 StackTraceElement[] stackTrace = exception.getStackTrace(); 2326 if (stackTrace != null) { 2327 for (int i = 0; i < stackTrace.length; i++) { 2328 messageBuilder.append(" at "); 2329 messageBuilder.append(stackTrace[i].toString()); 2330 messageBuilder.append("\n"); 2331 } 2332 } 2333 2334 System.err.println(messageBuilder.toString()); 2335 } 2336 2337 /** 2338 * Returns the current line number. 2339 * 2340 * @treatAsPrivate 2341 * @deprecated 2342 */ 2343 public int getLineNumber() { 2344 return xmlStreamReader.getLocation().getLineNumber(); 2345 } 2346 2347 /** 2348 * Returns the current parse trace. 2349 * 2350 * @treatAsPrivate 2351 * @deprecated 2352 */ 2353 public ParseTraceElement[] getParseTrace() { 2354 ParseTraceElement[] parseTrace = new ParseTraceElement[loaders.size()]; 2355 2356 int i = 0; 2357 for (FXMLLoader loader : loaders) { 2358 parseTrace[i++] = new ParseTraceElement(loader.location, (loader.current != null) ? 2359 loader.current.lineNumber : -1); 2360 } 2361 2362 return parseTrace; 2363 } 2364 2365 private void processProcessingInstruction() throws LoadException { 2366 String piTarget = xmlStreamReader.getPITarget().trim(); 2367 2368 if (piTarget.equals(LANGUAGE_PROCESSING_INSTRUCTION)) { 2369 processLanguage(); 2370 } else if (piTarget.equals(IMPORT_PROCESSING_INSTRUCTION)) { 2371 processImport(); 2372 } 2373 } 2374 2375 private void processLanguage() throws LoadException { 2376 if (scriptEngine != null) { 2377 throw new LoadException("Page language already set."); 2378 } 2379 2380 String language = xmlStreamReader.getPIData(); 2381 2382 if (loadListener != null) { 2383 loadListener.readLanguageProcessingInstruction(language); 2384 } 2385 2386 if (!staticLoad) { 2387 ScriptEngineManager scriptEngineManager = getScriptEngineManager(); 2388 scriptEngine = scriptEngineManager.getEngineByName(language); 2389 scriptEngine.setBindings(scriptEngineManager.getBindings(), ScriptContext.ENGINE_SCOPE); 2390 } 2391 } 2392 2393 private void processImport() throws LoadException { 2394 String target = xmlStreamReader.getPIData().trim(); 2395 2396 if (loadListener != null) { 2397 loadListener.readImportProcessingInstruction(target); 2398 } 2399 2400 if (target.endsWith(".*")) { 2401 importPackage(target.substring(0, target.length() - 2)); 2402 } else { 2403 importClass(target); 2404 } 2405 } 2406 2407 private void processComment() throws LoadException { 2408 if (loadListener != null) { 2409 loadListener.readComment(xmlStreamReader.getText()); 2410 } 2411 } 2412 2413 private void processStartElement() throws IOException { 2414 // Create the element 2415 createElement(); 2416 2417 // Process the start tag 2418 current.processStartElement(); 2419 2420 // Set the root value 2421 if (root == null) { 2422 root = current.value; 2423 } 2424 } 2425 2426 private void createElement() throws IOException { 2427 String prefix = xmlStreamReader.getPrefix(); 2428 String localName = xmlStreamReader.getLocalName(); 2429 2430 if (prefix == null) { 2431 int i = localName.lastIndexOf('.'); 2432 2433 if (Character.isLowerCase(localName.charAt(i + 1))) { 2434 String name = localName.substring(i + 1); 2435 2436 if (i == -1) { 2437 // This is an instance property 2438 if (loadListener != null) { 2439 loadListener.beginPropertyElement(name, null); 2440 } 2441 2442 current = new PropertyElement(name, null); 2443 } else { 2444 // This is a static property 2445 Class<?> sourceType = getType(localName.substring(0, i)); 2446 2447 if (sourceType != null) { 2448 if (loadListener != null) { 2449 loadListener.beginPropertyElement(name, sourceType); 2450 } 2451 2452 current = new PropertyElement(name, sourceType); 2453 } else if (staticLoad) { 2454 // The source type was not recognized 2455 if (loadListener != null) { 2456 loadListener.beginUnknownStaticPropertyElement(localName); 2457 } 2458 2459 current = new UnknownStaticPropertyElement(); 2460 } else { 2461 throw new LoadException(localName + " is not a valid property."); 2462 } 2463 } 2464 } else { 2465 if (current == null && root != null) { 2466 throw new LoadException("Root value already specified."); 2467 } 2468 2469 Class<?> type = getType(localName); 2470 2471 if (type != null) { 2472 if (loadListener != null) { 2473 loadListener.beginInstanceDeclarationElement(type); 2474 } 2475 2476 current = new InstanceDeclarationElement(type); 2477 } else if (staticLoad) { 2478 // The type was not recognized 2479 if (loadListener != null) { 2480 loadListener.beginUnknownTypeElement(localName); 2481 } 2482 2483 current = new UnknownTypeElement(); 2484 } else { 2485 throw new LoadException(localName + " is not a valid type."); 2486 } 2487 } 2488 } else if (prefix.equals(FX_NAMESPACE_PREFIX)) { 2489 if (localName.equals(INCLUDE_TAG)) { 2490 if (loadListener != null) { 2491 loadListener.beginIncludeElement(); 2492 } 2493 2494 current = new IncludeElement(); 2495 } else if (localName.equals(REFERENCE_TAG)) { 2496 if (loadListener != null) { 2497 loadListener.beginReferenceElement(); 2498 } 2499 2500 current = new ReferenceElement(); 2501 } else if (localName.equals(COPY_TAG)) { 2502 if (loadListener != null) { 2503 loadListener.beginCopyElement(); 2504 } 2505 2506 current = new CopyElement(); 2507 } else if (localName.equals(ROOT_TAG)) { 2508 if (loadListener != null) { 2509 loadListener.beginRootElement(); 2510 } 2511 2512 current = new RootElement(); 2513 } else if (localName.equals(SCRIPT_TAG)) { 2514 if (loadListener != null) { 2515 loadListener.beginScriptElement(); 2516 } 2517 2518 current = new ScriptElement(); 2519 } else if (localName.equals(DEFINE_TAG)) { 2520 if (loadListener != null) { 2521 loadListener.beginDefineElement(); 2522 } 2523 2524 current = new DefineElement(); 2525 } else { 2526 throw new LoadException(prefix + ":" + localName + " is not a valid element."); 2527 } 2528 } else { 2529 throw new LoadException("Unexpected namespace prefix: " + prefix + "."); 2530 } 2531 } 2532 2533 private void processEndElement() throws IOException { 2534 current.processEndElement(); 2535 2536 if (loadListener != null) { 2537 loadListener.endElement(current.value); 2538 } 2539 2540 // Move up the stack 2541 current = current.parent; 2542 } 2543 2544 private void processCharacters() throws IOException { 2545 // Process the characters 2546 if (!xmlStreamReader.isWhiteSpace()) { 2547 current.processCharacters(); 2548 } 2549 } 2550 2551 private void importPackage(String name) throws LoadException { 2552 packages.add(name); 2553 } 2554 2555 private void importClass(String name) throws LoadException { 2556 try { 2557 loadType(name, true); 2558 } catch (ClassNotFoundException exception) { 2559 throw new LoadException(exception); 2560 } 2561 } 2562 2563 private Class<?> getType(String name) throws LoadException { 2564 Class<?> type = null; 2565 2566 if (Character.isLowerCase(name.charAt(0))) { 2567 // This is a fully-qualified class name 2568 try { 2569 type = loadType(name, false); 2570 } catch (ClassNotFoundException exception) { 2571 // No-op 2572 } 2573 } else { 2574 // This is an unqualified class name 2575 type = classes.get(name); 2576 2577 if (type == null) { 2578 // The class has not been loaded yet; look it up 2579 for (String packageName : packages) { 2580 try { 2581 type = loadTypeForPackage(packageName, name); 2582 } catch (ClassNotFoundException exception) { 2583 // No-op 2584 } 2585 2586 if (type != null) { 2587 break; 2588 } 2589 } 2590 2591 if (type != null) { 2592 classes.put(name, type); 2593 } 2594 } 2595 } 2596 2597 return type; 2598 } 2599 2600 private Class<?> loadType(String name, boolean cache) throws ClassNotFoundException { 2601 int i = name.indexOf('.'); 2602 int n = name.length(); 2603 while (i != -1 2604 && i < n 2605 && Character.isLowerCase(name.charAt(i + 1))) { 2606 i = name.indexOf('.', i + 1); 2607 } 2608 2609 if (i == -1 || i == n) { 2610 throw new ClassNotFoundException(); 2611 } 2612 2613 String packageName = name.substring(0, i); 2614 String className = name.substring(i + 1); 2615 2616 Class<?> type = loadTypeForPackage(packageName, className); 2617 2618 if (cache) { 2619 classes.put(className, type); 2620 } 2621 2622 return type; 2623 } 2624 2625 // TODO Rename to loadType() when deprecated static version is removed 2626 private Class<?> loadTypeForPackage(String packageName, String className) throws ClassNotFoundException { 2627 return classLoader.loadClass(packageName + "." + className.replace('.', '$')); 2628 } 2629 2630 private HashMap<String, Field> getControllerFields() throws LoadException { 2631 if (controllerFields == null) { 2632 controllerFields = new HashMap<String, Field>(); 2633 2634 Class<?> controllerType = controller.getClass(); 2635 Class<?> type = controllerType; 2636 2637 while (type != Object.class) { 2638 Field[] fields = FieldUtil.getDeclaredFields(type); 2639 2640 for (int i = 0; i < fields.length; i++) { 2641 Field field = fields[i]; 2642 int modifiers = field.getModifiers(); 2643 2644 // Only add fields that are visible to this controller type 2645 if (type == controllerType 2646 || (modifiers & Modifier.PRIVATE) == 0) { 2647 // Ensure that the field is accessible 2648 if ((modifiers & Modifier.PUBLIC) == 0 2649 && field.getAnnotation(FXML.class) != null) { 2650 try { 2651 field.setAccessible(true); 2652 } catch (SecurityException exception) { 2653 throw new LoadException(exception); 2654 } 2655 } 2656 2657 controllerFields.put(field.getName(), field); 2658 } 2659 } 2660 2661 type = type.getSuperclass(); 2662 } 2663 } 2664 2665 return controllerFields; 2666 } 2667 2668 private HashMap<String, Method> getControllerMethods() throws LoadException { 2669 if (controllerMethods == null) { 2670 controllerMethods = new HashMap<String, Method>(); 2671 2672 Class<?> controllerType = controller.getClass(); 2673 Class<?> type = controllerType; 2674 2675 while (type != Object.class) { 2676 ReflectUtil.checkPackageAccess(type); 2677 Method[] methods = type.getDeclaredMethods(); 2678 2679 for (int i = 0; i < methods.length; i++) { 2680 Method method = methods[i]; 2681 int modifiers = method.getModifiers(); 2682 2683 // Only add methods that are visible to this controller type 2684 if (type == controllerType 2685 || (modifiers & Modifier.PRIVATE) == 0) { 2686 // Ensure that the method is accessible 2687 if ((modifiers & Modifier.PUBLIC) == 0 2688 && method.getAnnotation(FXML.class) != null) { 2689 try { 2690 method.setAccessible(true); 2691 } catch (SecurityException exception) { 2692 throw new LoadException(exception); 2693 } 2694 } 2695 2696 // Add this method to the map if: 2697 // a) it is the initialize() method, or 2698 // b) it takes a single event argument, or 2699 // c) it takes no arguments and a handler with this 2700 // name has not already been defined 2701 String methodName = method.getName(); 2702 Class<?>[] parameterTypes = method.getParameterTypes(); 2703 2704 if (methodName.equals(INITIALIZE_METHOD_NAME)) { 2705 if (parameterTypes.length == 0) { 2706 controllerMethods.put(method.getName(), method); 2707 } 2708 } else if ((parameterTypes.length == 1 && Event.class.isAssignableFrom(parameterTypes[0])) 2709 || (parameterTypes.length == 0 && !controllerMethods.containsKey(methodName))) { 2710 controllerMethods.put(method.getName(), method); 2711 } 2712 } 2713 } 2714 2715 type = type.getSuperclass(); 2716 } 2717 } 2718 2719 return controllerMethods; 2720 } 2721 2722 private ScriptEngineManager getScriptEngineManager() { 2723 if (scriptEngineManager == null) { 2724 scriptEngineManager = new javax.script.ScriptEngineManager(); 2725 scriptEngineManager.setBindings(new SimpleBindings(namespace)); 2726 } 2727 2728 return scriptEngineManager; 2729 } 2730 2731 /** 2732 * Loads a type using the default class loader. 2733 * 2734 * @param packageName 2735 * @param className 2736 * 2737 * @deprecated 2738 * This method now delegates to {@link #getDefaultClassLoader()}. 2739 */ 2740 public static Class<?> loadType(String packageName, String className) throws ClassNotFoundException { 2741 return loadType(packageName + "." + className.replace('.', '$')); 2742 } 2743 2744 /** 2745 * Loads a type using the default class loader. 2746 * 2747 * @param className 2748 * 2749 * @deprecated 2750 * This method now delegates to {@link #getDefaultClassLoader()}. 2751 */ 2752 public static Class<?> loadType(String className) throws ClassNotFoundException { 2753 ReflectUtil.checkPackageAccess(className); 2754 return Class.forName(className, true, defaultClassLoader); 2755 } 2756 2757 /** 2758 * Returns the default class loader. 2759 */ 2760 public static ClassLoader getDefaultClassLoader() { 2761 return defaultClassLoader; 2762 } 2763 2764 /** 2765 * Sets the default class loader. 2766 * 2767 * @param defaultClassLoader 2768 * The default class loader to use when loading classes. 2769 */ 2770 public static void setDefaultClassLoader(ClassLoader defaultClassLoader) { 2771 if (defaultClassLoader == null) { 2772 throw new NullPointerException(); 2773 } 2774 2775 FXMLLoader.defaultClassLoader = defaultClassLoader; 2776 } 2777 2778 /** 2779 * Loads an object hierarchy from a FXML document. 2780 * 2781 * @param location 2782 */ 2783 @SuppressWarnings("unchecked") 2784 public static <T> T load(URL location) throws IOException { 2785 return (T)load(location, null); 2786 } 2787 2788 /** 2789 * Loads an object hierarchy from a FXML document. 2790 * 2791 * @param location 2792 * @param resources 2793 */ 2794 @SuppressWarnings("unchecked") 2795 public static <T> T load(URL location, ResourceBundle resources) throws IOException { 2796 return (T)load(location, resources, new JavaFXBuilderFactory()); 2797 } 2798 2799 /** 2800 * Loads an object hierarchy from a FXML document. 2801 * 2802 * @param location 2803 * @param resources 2804 * @param builderFactory 2805 */ 2806 @SuppressWarnings("unchecked") 2807 public static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory) 2808 throws IOException { 2809 return (T)load(location, resources, builderFactory, null); 2810 } 2811 2812 /** 2813 * Loads an object hierarchy from a FXML document. 2814 * 2815 * @param location 2816 * @param resources 2817 * @param builderFactory 2818 * @param controllerFactory 2819 */ 2820 @SuppressWarnings("unchecked") 2821 public static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, 2822 Callback<Class<?>, Object> controllerFactory) throws IOException { 2823 return (T)load(location, resources, builderFactory, controllerFactory, Charset.forName(DEFAULT_CHARSET_NAME)); 2824 } 2825 2826 /** 2827 * Loads an object hierarchy from a FXML document. 2828 * 2829 * @param location 2830 * @param resources 2831 * @param builderFactory 2832 * @param controllerFactory 2833 * @param charset 2834 */ 2835 @SuppressWarnings("unchecked") 2836 public static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, 2837 Callback<Class<?>, Object> controllerFactory, Charset charset) throws IOException { 2838 if (location == null) { 2839 throw new NullPointerException("Location is required."); 2840 } 2841 2842 FXMLLoader fxmlLoader = new FXMLLoader(location, resources, builderFactory, controllerFactory, charset); 2843 2844 return (T)fxmlLoader.load(); 2845 } 2846 2847 /** 2848 * Utility method for comparing two JavaFX version strings (such as 2.2.5, 8.0.0-ea) 2849 * @param rtVer String representation of JavaFX runtime version, including - or _ appendix 2850 * @param nsVer String representation of JavaFX version to compare against runtime version 2851 * @return number < 0 if runtime version is lower, 0 when both versions are the same, 2852 * number > 0 if runtime is higher version 2853 */ 2854 static int compareJFXVersions(String rtVer, String nsVer) { 2855 2856 int retVal = 0; 2857 2858 if (rtVer == null || "".equals(rtVer) || 2859 nsVer == null || "".equals(nsVer)) { 2860 return retVal; 2861 } 2862 2863 if (rtVer.equals(nsVer)) { 2864 return retVal; 2865 } 2866 2867 // version string can contain '-' 2868 int dashIndex = rtVer.indexOf("-"); 2869 if (dashIndex > 0) { 2870 rtVer = rtVer.substring(0, dashIndex); 2871 } 2872 2873 // or "_" 2874 int underIndex = rtVer.indexOf("_"); 2875 if (underIndex > 0) { 2876 rtVer = rtVer.substring(0, underIndex); 2877 } 2878 2879 // do not try to compare if the string is not valid version format 2880 if (!Pattern.matches("^(\\d+)(\\.\\d+)*$", rtVer) || 2881 !Pattern.matches("^(\\d+)(\\.\\d+)*$", nsVer)) { 2882 return retVal; 2883 } 2884 2885 StringTokenizer nsVerTokenizer = new StringTokenizer(nsVer, "."); 2886 StringTokenizer rtVerTokenizer = new StringTokenizer(rtVer, "."); 2887 int nsDigit = 0, rtDigit = 0; 2888 boolean rtVerEnd = false; 2889 2890 while (nsVerTokenizer.hasMoreTokens() && retVal == 0) { 2891 nsDigit = Integer.parseInt(nsVerTokenizer.nextToken()); 2892 if (rtVerTokenizer.hasMoreTokens()) { 2893 rtDigit = Integer.parseInt(rtVerTokenizer.nextToken()); 2894 retVal = rtDigit - nsDigit; 2895 } else { 2896 rtVerEnd = true; 2897 break; 2898 } 2899 } 2900 2901 if (rtVerTokenizer.hasMoreTokens() && retVal == 0) { 2902 rtDigit = Integer.parseInt(rtVerTokenizer.nextToken()); 2903 if (rtDigit > 0) { 2904 retVal = 1; 2905 } 2906 } 2907 2908 if (rtVerEnd) { 2909 if (nsDigit > 0) { 2910 retVal = -1; 2911 } else { 2912 while (nsVerTokenizer.hasMoreTokens()) { 2913 nsDigit = Integer.parseInt(nsVerTokenizer.nextToken()); 2914 if (nsDigit > 0) { 2915 retVal = -1; 2916 break; 2917 } 2918 } 2919 } 2920 } 2921 2922 return retVal; 2923 } 2924 2925}