/*******************************************************************************
* Copyright 2011 Google Inc. All Rights Reserved.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.google.gdt.eclipse.designer.uibinder.parser;
import com.google.common.collect.Maps;
import com.google.gdt.eclipse.designer.uibinder.IExceptionConstants;
import com.google.gdt.eclipse.designer.uibinder.model.util.EventHandlersSupport;
import com.google.gdt.eclipse.designer.uibinder.model.util.NameSupport;
import com.google.gdt.eclipse.designer.uibinder.model.util.StylePropertySupport;
import com.google.gdt.eclipse.designer.uibinder.model.util.UiBinderStaticFieldSupport;
import com.google.gdt.eclipse.designer.uibinder.model.util.UiChildSupport;
import com.google.gdt.eclipse.designer.uibinder.model.util.UiConstructorSupport;
import com.google.gdt.eclipse.designer.uibinder.model.widgets.IsWidgetInfo;
import com.google.gdt.eclipse.designer.util.Utils;
import org.eclipse.wb.core.model.broadcast.ObjectEventListener;
import org.eclipse.wb.core.model.broadcast.ObjectInfoTreeComplete;
import org.eclipse.wb.internal.core.model.util.ScriptUtils;
import org.eclipse.wb.internal.core.utils.exception.DesignerException;
import org.eclipse.wb.internal.core.utils.execution.RunnableEx;
import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils;
import org.eclipse.wb.internal.core.utils.xml.DocumentElement;
import org.eclipse.wb.internal.core.utils.xml.DocumentModelVisitor;
import org.eclipse.wb.internal.core.xml.model.XmlObjectInfo;
import org.eclipse.wb.internal.core.xml.model.creation.ElementCreationSupport;
import org.eclipse.wb.internal.core.xml.model.description.ComponentDescription;
import org.eclipse.wb.internal.core.xml.model.description.ComponentDescriptionHelper;
import org.eclipse.wb.internal.core.xml.model.utils.GlobalStateXml;
import org.eclipse.wb.internal.core.xml.model.utils.XmlObjectUtils;
import org.eclipse.jdt.core.IJavaProject;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
/**
* Parser for GWT UiBinder.
*
* @author scheglov_ke
* @coverage GWT.UiBinder.parser
*/
public final class UiBinderParser {
private final UiBinderContext m_context;
private final Map<String, DocumentElement> m_pathToElementMap = Maps.newLinkedHashMap();
private final Map<String, XmlObjectInfo> m_pathToModelMap = Maps.newLinkedHashMap();
private XmlObjectInfo m_rootModel;
////////////////////////////////////////////////////////////////////////////
//
// Constructor
//
////////////////////////////////////////////////////////////////////////////
public UiBinderParser(UiBinderContext context) throws Exception {
m_context = context;
}
////////////////////////////////////////////////////////////////////////////
//
// Parse
//
////////////////////////////////////////////////////////////////////////////
/**
* Parse <code>*.ui.xml</code> files and return root {@link XmlObjectInfo}.
*/
public XmlObjectInfo parse() throws Exception {
// validate GWT version
{
IJavaProject javaProject = m_context.getJavaProject();
// has UiBinder support at all
if (!hasUiBinderSupport(javaProject)) {
throw new DesignerException(IExceptionConstants.WRONG_VERSION);
}
// has support for @UiField(provided) and @UiFactory
if (!hasUiFieldUiFactorySupport(javaProject)) {
String formSource = m_context.getFormType().getSource();
if (formSource.contains("@UiField(provided")) {
throw new DesignerException(IExceptionConstants.UI_FIELD_FACTORY_FEATURE);
}
if (formSource.contains("@UiFactory")) {
throw new DesignerException(IExceptionConstants.UI_FIELD_FACTORY_FEATURE);
}
}
}
// prepare for parsing
GlobalStateXml.setEditorContext(m_context);
m_context.initialize();
// parse
m_context.runDesignTime(new RunnableEx() {
public void run() throws Exception {
try {
parse0();
} catch (Throwable e) {
m_context.getBroadcastSupport().getListener(ObjectEventListener.class).dispose();
ReflectionUtils.propagate(e);
}
}
});
return m_rootModel;
}
/**
* Implementation of {@link #parse()}.
*/
private void parse0() throws Exception {
m_context.setParsing(true);
m_context.notifyAboutToParse();
fillMap_pathToElement();
// load Binder
Object createdBinder;
{
ClassLoader classLoader = m_context.getClassLoader();
String binderClassName = m_context.getBinderClassName();
Class<?> binderClass = classLoader.loadClass(binderClassName);
Class<?> classGWT = classLoader.loadClass("com.google.gwt.core.client.GWT");
createdBinder =
ReflectionUtils.invokeMethod(classGWT, "create(java.lang.Class)", binderClass);
createModels(createdBinder);
}
// render Widget(s)
ReflectionUtils.invokeMethod(createdBinder, "createAndBindUi(java.lang.Object)", (Object) null);
buildHierarchy();
// done
m_context.setParsing(false);
new UiConstructorSupport(m_context);
new UiChildSupport(m_context);
NameSupport.decoratePresentationWithName(m_rootModel);
XmlObjectUtils.callRootProcessors(m_rootModel);
XmlObjectUtils.registerTagResolvers(m_rootModel);
new UiBinderStaticFieldSupport(m_rootModel);
NameSupport.removeName_onDelete(m_rootModel);
NameSupport.ensureFieldProvided_onCreate(m_rootModel);
new EventHandlersSupport(m_rootModel);
new StylePropertySupport(m_context);
GlobalStateXml.activate(m_rootModel);
m_rootModel.getBroadcast(ObjectInfoTreeComplete.class).invoke();
m_rootModel.refresh_dispose();
}
/**
* Handle object before setting attributes, so get default property values.
*/
private void createModels(Object binder) throws Exception {
ClassLoader classLoader = binder.getClass().getClassLoader();
String handlerClassName = binder.getClass().getName() + "$DTObjectHandler";
Class<?> handlerClass = classLoader.loadClass(handlerClassName);
Object handler =
Proxy.newProxyInstance(classLoader, new Class[]{handlerClass}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("handle")) {
String path = (String) args[0];
Object object = args[1];
createModel(path, object);
}
if (method.getName().equals("provideFactory")) {
Class<?> factoryType = (Class<?>) args[0];
String methodName = (String) args[1];
Object[] factoryArgs = (Object[]) args[2];
return createProvidedFactory(m_context, factoryType, methodName, factoryArgs);
}
if (method.getName().equals("provideField")) {
Class<?> fieldType = (Class<?>) args[0];
String fieldName = (String) args[1];
return createProvidedField(m_context, fieldType, fieldName);
}
return null;
}
});
ReflectionUtils.setField(binder, "dtObjectHandler", handler);
}
private void createModel(String path, Object object) throws Exception {
DocumentElement xmlElement = m_pathToElementMap.get(path);
XmlObjectInfo objectInfo =
XmlObjectUtils.createObject(
m_context,
object.getClass(),
new ElementCreationSupport(xmlElement));
GlobalStateXml.activate(objectInfo);
objectInfo.setObject(object);
m_pathToModelMap.put(path, objectInfo);
}
private void buildHierarchy() throws Exception {
for (Map.Entry<String, XmlObjectInfo> entry : m_pathToModelMap.entrySet()) {
String path = entry.getKey();
XmlObjectInfo object = entry.getValue();
if (object instanceof IsWidgetInfo) {
object = ((IsWidgetInfo) object).getWrapped();
}
XmlObjectInfo parent = findParent(path);
if (parent != null) {
parent.addChild(object);
} else {
m_rootModel = object;
}
}
}
/**
* @return the parent for given "child" path, may be <code>null</code> if root.
*/
private XmlObjectInfo findParent(String childPath) {
String parentPath = StringUtils.substringBeforeLast(childPath, "/");
for (Map.Entry<String, XmlObjectInfo> entry : m_pathToModelMap.entrySet()) {
String path = entry.getKey();
XmlObjectInfo parent = entry.getValue();
if (path.equals(parentPath)) {
return parent;
}
}
if (childPath.contains("/")) {
return findParent(parentPath);
}
return null;
}
/**
* Visits all {@link DocumentElement}s and remembers all of them with path.
*/
private void fillMap_pathToElement() {
m_context.getRootElement().accept(new DocumentModelVisitor() {
@Override
public void endVisit(DocumentElement element) {
m_pathToElementMap.put(getPath(element), element);
}
});
}
/**
* @return the path of our {@link DocumentElement}.
*/
public static String getPath(DocumentElement element) {
DocumentElement parent = element.getParent();
if (parent == null) {
return "0";
} else {
int index = parent.indexOf(element);
return getPath(parent) + "/" + index;
}
}
////////////////////////////////////////////////////////////////////////////
//
// Utils
//
////////////////////////////////////////////////////////////////////////////
/**
* @return the instance of given {@link Class} created using factory method.
*/
static Object createProvidedFactory(UiBinderContext context,
Class<?> factoryType,
String methodName,
Object[] args) throws Exception {
try {
return createObjectInstance(context, methodName, factoryType, args);
} catch (Throwable e) {
throw new DesignerException(IExceptionConstants.UI_FACTORY_EXCEPTION,
e,
factoryType.getName(),
methodName);
}
}
/**
* @return the instance of given {@link Class}.
*/
static Object createProvidedField(UiBinderContext context, Class<?> fieldType, String fieldName)
throws Exception {
try {
return createObjectInstance(context, fieldName, fieldType, ArrayUtils.EMPTY_OBJECT_ARRAY);
} catch (Throwable e) {
throw new DesignerException(IExceptionConstants.UI_FIELD_EXCEPTION,
e,
fieldType.getName(),
fieldName);
}
}
/**
* @return the instance of given {@link Class}, created using the most specific method.
*/
private static Object createObjectInstance(UiBinderContext context,
String objectName,
Class<?> clazz,
Object[] args) throws Exception {
// try CreateObjectInstance broadcast
{
Object result[] = {null};
context.getBroadcastSupport().getListener(CreateObjectInstance.class).invoke(
objectName,
clazz,
args,
result);
if (result[0] != null) {
return result[0];
}
}
// try "UiBinder.createInstance" script
{
ComponentDescription description = ComponentDescriptionHelper.getDescription(context, clazz);
// prepare script
String script;
{
String scriptObject[] = {null};
context.getBroadcastSupport().getListener(CreateObjectScript.class).invoke(
objectName,
clazz,
args,
scriptObject);
script = scriptObject[0];
}
if (script == null) {
script = description.getParameter("UiBinder.createInstance");
}
// try to use script
if (script != null) {
ClassLoader classLoader = context.getClassLoader();
Map<String, Object> variables = Maps.newTreeMap();
variables.put("wbpClassLoader", UiBinderParser.class.getClassLoader());
variables.put("classLoader", classLoader);
variables.put("componentClass", clazz);
variables.put("objectName", objectName);
variables.put("modelClass", description.getModelClass());
variables.put("modelClassLoader", description.getModelClass().getClassLoader());
variables.put("args", args);
Object result = ScriptUtils.evaluate(classLoader, script, variables);
return result;
}
}
// default constructor
{
Constructor<?> constructor = ReflectionUtils.getConstructorBySignature(clazz, "<init>()");
if (constructor != null) {
return constructor.newInstance();
}
}
// shortest constructor
{
Constructor<?> constructor = ReflectionUtils.getShortestConstructor(clazz);
Class<?>[] parameterTypes = constructor.getParameterTypes();
Object[] argumentValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
argumentValues[i] = getDefaultValue(parameterType);
}
return constructor.newInstance(argumentValues);
}
}
/**
* @return the argument value for parameter of given type.
*/
private static Object getDefaultValue(Class<?> parameterType) throws Exception {
// try INSTANCE field
{
Field instanceField = ReflectionUtils.getFieldByName(parameterType, "INSTANCE");
if (instanceField != null
&& ReflectionUtils.isStatic(instanceField)
&& parameterType.isAssignableFrom(instanceField.getType())) {
return instanceField.get(null);
}
}
// use default value
return ReflectionUtils.getDefaultValue(parameterType);
}
////////////////////////////////////////////////////////////////////////////
//
// Versions
//
////////////////////////////////////////////////////////////////////////////
/**
* @return <code>true</code> if given {@link IJavaProject} uses at least GWT 2.1, which contains
* design-time tweaks for using UiBinder in GWT Designer.
*/
public static boolean hasUiBinderSupport(IJavaProject javaProject) {
return Utils.getVersion(javaProject).isHigherOrSame(Utils.GWT_2_1);
}
/**
* @return <code>true</code> if given {@link IJavaProject} uses at least GWT 2.1.1, which contains
* design-time tweaks for using UiBinder's <code>@UiField(provided)</code> and
* <code>@UiFactory</code> in GWT Designer.
*/
public static boolean hasUiFieldUiFactorySupport(IJavaProject javaProject) {
return Utils.getVersion(javaProject).isHigherOrSame(Utils.GWT_2_1_1);
}
}