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