/*
* Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javafx.fxml;
import com.sun.javafx.fxml.
BeanAdapter;
import com.sun.javafx.fxml.builder.
JavaFXFontBuilder;
import com.sun.javafx.fxml.builder.
JavaFXImageBuilder;
import com.sun.javafx.fxml.builder.
JavaFXSceneBuilder;
import com.sun.javafx.fxml.builder.
ProxyBuilder;
import com.sun.javafx.fxml.builder.
TriangleMeshBuilder;
import com.sun.javafx.fxml.builder.
URLBuilder;
import java.lang.annotation.
Annotation;
import java.lang.reflect.
Array;
import java.lang.reflect.
Constructor;
import java.lang.reflect.
InvocationTargetException;
import java.lang.reflect.
Method;
import java.lang.reflect.
Modifier;
import java.net.
URL;
import java.util.
AbstractMap;
import java.util.
ArrayList;
import java.util.
Arrays;
import java.util.
Collection;
import java.util.
HashMap;
import java.util.
HashSet;
import java.util.
Iterator;
import java.util.
List;
import java.util.
Map;
import java.util.
Set;
import java.util.logging.
Level;
import java.util.logging.
Logger;
import javafx.application.
ConditionalFeature;
import javafx.application.
Platform;
import javafx.beans.
NamedArg;
import javafx.collections.
FXCollections;
import javafx.collections.
ObservableList;
import javafx.collections.
ObservableMap;
import javafx.scene.
Node;
import javafx.scene.
Scene;
import javafx.scene.image.
Image;
import javafx.scene.shape.
TriangleMesh;
import javafx.scene.text.
Font;
import javafx.util.
Builder;
import javafx.util.
BuilderFactory;
import sun.reflect.misc.
ConstructorUtil;
import sun.reflect.misc.
MethodUtil;
/**
* JavaFX builder factory.
* @since JavaFX 2.0
*/
public final class
JavaFXBuilderFactory implements
BuilderFactory {
private final
JavaFXBuilder NO_BUILDER = new
JavaFXBuilder();
private final
Map<
Class<?>,
JavaFXBuilder>
builders = new
HashMap<
Class<?>,
JavaFXBuilder>();
private final
ClassLoader classLoader;
private final boolean
alwaysUseBuilders;
private final boolean
webSupported;
/**
* Default constructor.
*/
public
JavaFXBuilderFactory() {
this(
FXMLLoader.
getDefaultClassLoader(), false);
}
/**
* @treatAsPrivate
* This constructor is for internal use only.
*
* @deprecated
*/
public
JavaFXBuilderFactory(boolean
alwaysUseBuilders) {
// SB-dependency: RT-21230 has been filed to track this
this(
FXMLLoader.
getDefaultClassLoader(),
alwaysUseBuilders);
}
/**
* Constructor that takes a class loader.
*
* @param classLoader
* @since JavaFX 2.1
*/
public
JavaFXBuilderFactory(
ClassLoader classLoader) {
this(
classLoader, false);
}
/**
* @treatAsPrivate
* This constructor is for internal use only.
*
* @deprecated
* @since JavaFX 2.1
*/
public
JavaFXBuilderFactory(
ClassLoader classLoader, boolean
alwaysUseBuilders) {
// SB-dependency: RT-21230 has been filed to track this
if (
classLoader == null) {
throw new
NullPointerException();
}
this.
classLoader =
classLoader;
this.
alwaysUseBuilders =
alwaysUseBuilders;
this.
webSupported =
Platform.
isSupported(
ConditionalFeature.
WEB);
}
@
Override
public
Builder<?>
getBuilder(
Class<?>
type) {
Builder<?>
builder;
if (
type ==
Scene.class) {
builder = new
JavaFXSceneBuilder();
} else if (
type ==
Font.class) {
builder = new
JavaFXFontBuilder();
} else if (
type ==
Image.class) {
builder = new
JavaFXImageBuilder();
} else if (
type ==
URL.class) {
builder = new
URLBuilder(
classLoader);
} else if (
type ==
TriangleMesh.class) {
builder = new
TriangleMeshBuilder();
} else if (
scanForConstructorAnnotations(
type)) {
builder = new
ProxyBuilder(
type);
} else {
Builder<
Object>
objectBuilder = null;
JavaFXBuilder typeBuilder =
builders.
get(
type);
if (
typeBuilder !=
NO_BUILDER) {
if (
typeBuilder == null) {
// We want to retun a builder here
// only for those classes that reqire it.
// For now we assume that an object that has a default
// constructor does not require a builder. This is the case
// for most platform classes, except those handled above.
// We may need to add other exceptions to the rule if the need
// arises...
//
boolean
hasDefaultConstructor;
try {
ConstructorUtil.
getConstructor(
type, new
Class[] {});
// found!
// forces the factory to return a builder if there is one.
// TODO: delete the line below when we are sure that both
// builders and default constructors are working!
if (
alwaysUseBuilders) throw new
Exception();
hasDefaultConstructor = true;
} catch (
Exception x) {
hasDefaultConstructor = false;
}
// Force the loader to use a builder for WebView even though
// it defines a default constructor
if (!
hasDefaultConstructor || (
webSupported &&
type.
getName().
equals("javafx.scene.web.WebView"))) {
try {
typeBuilder =
createTypeBuilder(
type);
} catch (
ClassNotFoundException ex) {
// no builder... will fail later when the FXMLLoader
// will try to instantiate the bean...
}
}
builders.
put(
type,
typeBuilder == null ?
NO_BUILDER :
typeBuilder);
}
if (
typeBuilder != null) {
objectBuilder =
typeBuilder.
createBuilder();
}
}
builder =
objectBuilder;
}
return
builder;
}
JavaFXBuilder createTypeBuilder(
Class<?>
type) throws
ClassNotFoundException {
JavaFXBuilder typeBuilder = null;
Class<?>
builderClass =
classLoader.
loadClass(
type.
getName() + "Builder");
try {
typeBuilder = new
JavaFXBuilder(
builderClass);
} catch (
Exception ex) {
//TODO should be reported
Logger.
getLogger(
JavaFXBuilderFactory.class.
getName()).
log(
Level.
WARNING, "Failed to instantiate JavaFXBuilder for " +
builderClass,
ex);
}
if (!
alwaysUseBuilders) {
Logger.
getLogger(
JavaFXBuilderFactory.class.
getName()).
log(
Level.
FINER, "class {0} requires a builder.",
type);
}
return
typeBuilder;
}
private boolean
scanForConstructorAnnotations(
Class<?>
type) {
Constructor constructors[] =
ConstructorUtil.
getConstructors(
type);
for (
Constructor constructor :
constructors) {
Annotation[][]
paramAnnotations =
constructor.
getParameterAnnotations();
for (int
i = 0;
i <
constructor.
getParameterTypes().length;
i++) {
for (
Annotation annotation :
paramAnnotations[
i]) {
if (
annotation instanceof
NamedArg) {
return true;
}
}
}
}
return false;
}
}
/**
* JavaFX builder.
*/
final class
JavaFXBuilder {
private static final
Object[]
NO_ARGS = {};
private static final
Class<?>[]
NO_SIG = {};
private final
Class<?>
builderClass;
private final
Method createMethod;
private final
Method buildMethod;
private final
Map<
String,
Method>
methods = new
HashMap<
String,
Method>();
private final
Map<
String,
Method>
getters = new
HashMap<
String,
Method>();
private final
Map<
String,
Method>
setters = new
HashMap<
String,
Method>();
final class
ObjectBuilder extends
AbstractMap<
String,
Object> implements
Builder<
Object> {
private final
Map<
String,
Object>
containers = new
HashMap<
String,
Object>();
private
Object builder = null;
private
Map<
Object,
Object>
properties;
private
ObjectBuilder() {
try {
builder =
MethodUtil.
invoke(
createMethod, null,
NO_ARGS);
} catch (
Exception e) {
//TODO
throw new
RuntimeException("Creation of the builder " +
builderClass.
getName() + " failed.",
e);
}
}
@
Override
public
Object build() {
for (
Iterator<
Entry<
String,
Object>>
iter =
containers.
entrySet().
iterator();
iter.
hasNext(); ) {
Entry<
String,
Object>
entry =
iter.
next();
put(
entry.
getKey(),
entry.
getValue());
}
Object res;
try {
res =
MethodUtil.
invoke(
buildMethod,
builder,
NO_ARGS);
// TODO:
// temporary special case for Node properties until
// platform builders are fixed
if (
properties != null &&
res instanceof
Node) {
((
Map<
Object,
Object>)((
Node)
res).
getProperties()).
putAll(
properties);
}
} catch (
InvocationTargetException exception) {
throw new
RuntimeException(
exception);
} catch (
IllegalAccessException exception) {
throw new
RuntimeException(
exception);
} finally {
builder = null;
}
return
res;
}
@
Override
public int
size() {
throw new
UnsupportedOperationException();
}
@
Override
public boolean
isEmpty() {
throw new
UnsupportedOperationException();
}
@
Override
public boolean
containsKey(
Object key) {
return (
getTemporaryContainer(
key.
toString()) != null);
}
@
Override
public boolean
containsValue(
Object value) {
throw new
UnsupportedOperationException();
}
@
Override
public
Object get(
Object key) {
return
getTemporaryContainer(
key.
toString());
}
@
Override
@
SuppressWarnings("unchecked")
public
Object put(
String key,
Object value) {
// TODO:
// temporary hack: builders don't have a method for properties...
if (
Node.class.
isAssignableFrom(
getTargetClass()) && "properties".
equals(
key)) {
properties = (
Map<
Object,
Object>)
value;
return null;
}
try {
Method m =
methods.
get(
key);
if (
m == null) {
m =
findMethod(
key);
methods.
put(
key,
m);
}
try {
final
Class<?>
type =
m.
getParameterTypes()[0];
// If the type is an Array, and our value is a list,
// we simply convert the list into an array. Otherwise,
// we treat the value as a string and split it into a
// list using the array component delimiter.
if (
type.
isArray()) {
final
List<?>
list;
if (
value instanceof
List) {
list = (
List<?>)
value;
} else {
list =
Arrays.
asList(
value.
toString().
split(
FXMLLoader.
ARRAY_COMPONENT_DELIMITER));
}
final
Class<?>
componentType =
type.
getComponentType();
Object array =
Array.
newInstance(
componentType,
list.
size());
for (int
i=0;
i<
list.
size();
i++) {
Array.
set(
array,
i,
BeanAdapter.
coerce(
list.
get(
i),
componentType));
}
value =
array;
}
MethodUtil.
invoke(
m,
builder, new
Object[] {
BeanAdapter.
coerce(
value,
type) });
} catch (
Exception e) {
Logger.
getLogger(
JavaFXBuilder.class.
getName()).
log(
Level.
WARNING,
"Method " +
m.
getName() + " failed",
e);
}
//TODO Is it OK to return null here?
return null;
} catch (
Exception e) {
//TODO Should be reported
Logger.
getLogger(
JavaFXBuilder.class.
getName()).
log(
Level.
WARNING,
"Failed to set "+
getTargetClass()+"."+
key+" using "+
builderClass,
e);
return null;
}
}
// Should do this in BeanAdapter?
// This is used to support read-only collection property.
// This method must return a Collection of the appropriate type
// if 1. the property is read-only, and 2. the property is a collection.
// It must return null otherwise.
Object getReadOnlyProperty(
String propName) {
if (
setters.
get(
propName) != null) return null;
Method getter =
getters.
get(
propName);
if (
getter == null) {
Method setter = null;
Class<?>
target =
getTargetClass();
String suffix =
Character.
toUpperCase(
propName.
charAt(0)) +
propName.
substring(1);
try {
getter =
MethodUtil.
getMethod(
target, "get"+
suffix,
NO_SIG);
setter =
MethodUtil.
getMethod(
target, "set"+
suffix, new
Class[] {
getter.
getReturnType() });
} catch (
Exception x) {
}
if (
getter != null) {
getters.
put(
propName,
getter);
setters.
put(
propName,
setter);
}
if (
setter != null) return null;
}
Class<?>
type;
if (
getter == null) {
// if we have found no getter it might be a constructor property
// try to get the type from the builder method.
final
Method m =
findMethod(
propName);
if (
m == null) {
return null;
}
type =
m.
getParameterTypes()[0];
if (
type.
isArray())
type =
List.class;
} else {
type =
getter.
getReturnType();
}
if (
ObservableMap.class.
isAssignableFrom(
type)) {
return
FXCollections.
observableMap(new
HashMap<
Object,
Object>());
} else if (
Map.class.
isAssignableFrom(
type)) {
return new
HashMap<
Object,
Object>();
} else if (
ObservableList.class.
isAssignableFrom(
type)) {
return
FXCollections.
observableArrayList();
} else if (
List.class.
isAssignableFrom(
type)) {
return new
ArrayList<
Object>();
} else if (
Set.class.
isAssignableFrom(
type)) {
return new
HashSet<
Object>();
}
return null;
}
/**
* This is used to support read-only collection property.
* This method must return a Collection of the appropriate type
* if 1. the property is read-only, and 2. the property is a collection.
* It must return null otherwise.
**/
public
Object getTemporaryContainer(
String propName) {
Object o =
containers.
get(
propName);
if (
o == null) {
o =
getReadOnlyProperty(
propName);
if (
o != null) {
containers.
put(
propName,
o);
}
}
return
o;
}
@
Override
public
Object remove(
Object key) {
throw new
UnsupportedOperationException();
}
@
Override
public void
putAll(
Map<? extends
String, ? extends
Object>
m) {
throw new
UnsupportedOperationException();
}
@
Override
public void
clear() {
throw new
UnsupportedOperationException();
}
@
Override
public
Set<
String>
keySet() {
throw new
UnsupportedOperationException();
}
@
Override
public
Collection<
Object>
values() {
throw new
UnsupportedOperationException();
}
@
Override
public
Set<
Entry<
String,
Object>>
entrySet() {
throw new
UnsupportedOperationException();
}
}
JavaFXBuilder() {
builderClass = null;
createMethod = null;
buildMethod = null;
}
JavaFXBuilder(
Class<?>
builderClass) throws
NoSuchMethodException,
InstantiationException,
IllegalAccessException {
this.
builderClass =
builderClass;
createMethod =
MethodUtil.
getMethod(
builderClass, "create",
NO_SIG);
buildMethod =
MethodUtil.
getMethod(
builderClass, "build",
NO_SIG);
assert
Modifier.
isStatic(
createMethod.
getModifiers());
assert !
Modifier.
isStatic(
buildMethod.
getModifiers());
}
Builder<
Object>
createBuilder() {
return new
ObjectBuilder();
}
private
Method findMethod(
String name) {
if (
name.
length() > 1
&&
Character.
isUpperCase(
name.
charAt(1))) {
name =
Character.
toUpperCase(
name.
charAt(0)) +
name.
substring(1);
}
for (
Method m :
MethodUtil.
getMethods(
builderClass)) {
if (
m.
getName().
equals(
name)) {
return
m;
}
}
throw new
IllegalArgumentException("Method " +
name + " could not be found at class " +
builderClass.
getName());
}
/**
* The type constructed by this builder.
* @return The type constructed by this builder.
*/
public
Class<?>
getTargetClass() {
return
buildMethod.
getReturnType();
}
}