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