/*******************************************************************************
* 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.model.util;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gdt.eclipse.designer.GwtToolkitDescription;
import com.google.gdt.eclipse.designer.model.widgets.support.IDevModeBridge;
import com.google.gdt.eclipse.designer.uibinder.IExceptionConstants;
import com.google.gdt.eclipse.designer.uibinder.model.widgets.UIObjectInfo;
import com.google.gdt.eclipse.designer.uibinder.parser.UiBinderContext;
import com.google.gdt.eclipse.designer.uibinder.parser.UiBinderParser;
import com.google.gdt.eclipse.designer.util.Utils;
import org.eclipse.wb.core.model.ObjectInfo;
import org.eclipse.wb.core.model.broadcast.ObjectInfoDelete;
import org.eclipse.wb.core.model.broadcast.ObjectInfoPresentationDecorateText;
import org.eclipse.wb.internal.core.DesignerPlugin;
import org.eclipse.wb.internal.core.model.ObjectInfoVisitor;
import org.eclipse.wb.internal.core.model.description.CreationDescription;
import org.eclipse.wb.internal.core.model.description.CreationDescription.TypeParameterDescription;
import org.eclipse.wb.internal.core.model.description.helpers.ComponentDescriptionHelper;
import org.eclipse.wb.internal.core.model.variable.NamesManager;
import org.eclipse.wb.internal.core.model.variable.NamesManager.ComponentNameDescription;
import org.eclipse.wb.internal.core.utils.ast.AstEditor;
import org.eclipse.wb.internal.core.utils.ast.AstNodeUtils;
import org.eclipse.wb.internal.core.utils.ast.BodyDeclarationTarget;
import org.eclipse.wb.internal.core.utils.ast.DomGenerics;
import org.eclipse.wb.internal.core.utils.exception.DesignerException;
import org.eclipse.wb.internal.core.utils.execution.ExecutionUtils;
import org.eclipse.wb.internal.core.utils.execution.RunnableEx;
import org.eclipse.wb.internal.core.utils.execution.RunnableObjectEx;
import org.eclipse.wb.internal.core.utils.jdt.core.CodeUtils;
import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils;
import org.eclipse.wb.internal.core.utils.state.EditorState;
import org.eclipse.wb.internal.core.utils.xml.DocumentElement;
import org.eclipse.wb.internal.core.xml.model.XmlObjectInfo;
import org.eclipse.wb.internal.core.xml.model.broadcast.XmlObjectAdd;
import org.eclipse.wb.internal.core.xml.model.description.ComponentDescription;
import org.eclipse.wb.internal.core.xml.model.utils.XmlObjectUtils;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.ui.refactoring.RenameSupport;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
/**
* Support for "ui:field" attribute and "@UiField" in Java.
*
* @author scheglov_ke
* @coverage GWT.UiBinder.model
*/
public final class NameSupport {
////////////////////////////////////////////////////////////////////////////
//
// Static utils
//
////////////////////////////////////////////////////////////////////////////
/**
* Sometimes widget has no text, so it is hard to identify in components tree. But if it has name,
* would be nice to show it.
*/
public static void decoratePresentationWithName(XmlObjectInfo root) {
root.addBroadcastListener(new ObjectInfoPresentationDecorateText() {
public void invoke(ObjectInfo object, String[] text) throws Exception {
if (object instanceof XmlObjectInfo) {
XmlObjectInfo xObject = (XmlObjectInfo) object;
String name = getName(xObject);
if (name != null) {
text[0] = text[0] + " - " + name;
}
}
}
});
}
/**
* Deletes artifacts of widget from Java source.
*/
public static void removeName_onDelete(XmlObjectInfo rootObject) {
rootObject.addBroadcastListener(new ObjectInfoDelete() {
@Override
public void before(ObjectInfo parent, ObjectInfo child) throws Exception {
if (child instanceof XmlObjectInfo) {
XmlObjectInfo object = (XmlObjectInfo) child;
removeName(object);
}
}
});
}
/**
* If widget marked with <code>"UiBinder.createFieldProvided"</code> then during creation ensure
* <code>@UiField(provided=true)</code> using default Java creation.
*/
public static void ensureFieldProvided_onCreate(XmlObjectInfo rootObject) {
rootObject.addBroadcastListener(new XmlObjectAdd() {
@Override
public void after(ObjectInfo parent, XmlObjectInfo child) throws Exception {
if (XmlObjectUtils.hasTrueParameter(child, "UiBinder.createFieldProvided")) {
NameSupport nameSupport = new NameSupport(child);
nameSupport.createFieldProvided();
}
}
});
}
/**
* @return the existing name of the widget, or <code>null</code>.
*/
public static String getName(XmlObjectInfo object) {
if (XmlObjectUtils.isImplicit(object)) {
return null;
}
return new NameSupport(object).getName();
}
/**
* @return the existing or new name of the widget, can not be <code>null</code>.
*/
public static String ensureName(XmlObjectInfo object) throws Exception {
if (XmlObjectUtils.isImplicit(object)) {
throw new IllegalArgumentException();
}
final NameSupport nameSupport = new NameSupport(object);
return ExecutionUtils.runObject(object, new RunnableObjectEx<String>() {
public String runObject() throws Exception {
return nameSupport.ensureName();
}
});
}
/**
* Removes "@UiField" of the widget.
*/
public static void removeName(XmlObjectInfo object) throws Exception {
if (XmlObjectUtils.isImplicit(object)) {
return;
}
final NameSupport nameSupport = new NameSupport(object);
ExecutionUtils.run(object, new RunnableEx() {
public void run() throws Exception {
nameSupport.removeName();
}
});
}
/**
* Sets new name for "@UiField" of the widget.
*/
public static void setName(XmlObjectInfo object, final String name) throws Exception {
if (XmlObjectUtils.isImplicit(object)) {
throw new IllegalArgumentException();
}
m_renaming = true;
try {
final NameSupport nameSupport = new NameSupport(object);
ExecutionUtils.run(object, new RunnableEx() {
public void run() throws Exception {
nameSupport.setName(name);
}
});
} finally {
m_renaming = false;
}
}
/**
* @return the error message if given name is not valid, or <code>null</code> if this name can be
* used.
*/
public static String validateName(XmlObjectInfo object, String name) throws Exception {
return new NameSupport(object).validateName(name);
}
/**
* @return the {@link XmlObjectInfo} with the given name, may be <code>null</code>.
*/
public static XmlObjectInfo getObject(XmlObjectInfo root, final String name) {
final XmlObjectInfo result[] = {null};
root.accept(new ObjectInfoVisitor() {
@Override
public void endVisit(ObjectInfo object) throws Exception {
if (object instanceof XmlObjectInfo) {
XmlObjectInfo xmlObject = (XmlObjectInfo) object;
if (ObjectUtils.equals(getName(xmlObject), name)) {
result[0] = xmlObject;
}
}
}
});
return result[0];
}
////////////////////////////////////////////////////////////////////////////
//
// Instance fields
//
////////////////////////////////////////////////////////////////////////////
private final XmlObjectInfo m_object;
private final UiBinderContext m_context;
////////////////////////////////////////////////////////////////////////////
//
// Constructor
//
////////////////////////////////////////////////////////////////////////////
private NameSupport(XmlObjectInfo object) {
m_object = object;
m_context = (UiBinderContext) m_object.getContext();
}
////////////////////////////////////////////////////////////////////////////
//
// Name
//
////////////////////////////////////////////////////////////////////////////
/**
* @return the existing name of the widget, or <code>null</code>.
*/
public String getName() {
String nameAttribute = getNameAttribute();
return m_object.getAttribute(nameAttribute);
}
/**
* @return the existing or new name of the widget, can not be <code>null</code>.
*/
public String ensureName() throws Exception {
String name = getName();
if (name == null) {
name = generateName();
setName(name);
}
return name;
}
/**
* Removes "@UiField" of the widget.
*/
private void removeName() throws Exception {
String name = getName();
if (name != null) {
// remove attribute
{
String nameAttribute = getNameAttribute();
m_object.getElement().setAttribute(nameAttribute, null);
}
// update Java
{
AstEditor editor = getEditor();
TypeDeclaration typeDeclaration = editor.getPrimaryType();
// remove handlers
for (MethodDeclaration methodDeclaration : typeDeclaration.getMethods()) {
if (EventHandlerProperty.isObjectHandler(methodDeclaration, name)) {
editor.removeBodyDeclaration(methodDeclaration);
}
}
// remove field
VariableDeclaration variable = getBinderField(typeDeclaration, name);
FieldDeclaration field = AstNodeUtils.getEnclosingFieldDeclaration(variable);
editor.removeBodyDeclaration(field);
}
}
}
/**
* Sets new name for "@UiField" of the widget.
*/
private void setName(String name) throws Exception {
AstEditor editor = getEditor();
TypeDeclaration typeDeclaration = editor.getPrimaryType();
// prepare "old" state
String oldName = getName();
VariableDeclaration oldVariable = getBinderField(typeDeclaration, oldName);
// set "ui:field" attribute
{
String nameAttribute = getNameAttribute();
m_object.setAttribute(nameAttribute, name);
}
// update Java
if (oldVariable != null) {
IType modelType = m_context.getFormType();
IField modelField = modelType.getField(oldName);
RenameSupport renameSupport =
RenameSupport.create(modelField, name, RenameSupport.UPDATE_REFERENCES);
renameSupport.perform(DesignerPlugin.getShell(), DesignerPlugin.getActiveWorkbenchWindow());
return;
} else {
Class<?> componentClass = m_object.getDescription().getComponentClass();
String source =
"@com.google.gwt.uibinder.client.UiField "
+ ReflectionUtils.getCanonicalName(componentClass)
+ " "
+ name
+ ";";
BodyDeclarationTarget target = getNewFieldTarget(typeDeclaration);
editor.addFieldDeclaration(source, target);
}
}
/**
* @return the error message if given name is not valid, or <code>null</code> if this name can be
* used.
*/
private String validateName(String name) throws Exception {
// check that identifier is valid
{
IJavaProject javaProject = m_object.getContext().getJavaProject();
String sourceLevel = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
String complianceLevel = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
IStatus status = JavaConventions.validateFieldName(name, sourceLevel, complianceLevel);
if (status.matches(IStatus.ERROR)) {
return status.getMessage();
}
}
// check that name is unique
{
Set<String> existingNames = getExistingNames();
if (existingNames.contains(name)) {
return "Field '" + name + "' already exists.";
}
}
// OK
return null;
}
/**
* Creates <code>@UiField(provided=true)</code> using default Java creation.
*/
private void createFieldProvided() throws Exception {
// may be no support for @UiField
if (!UiBinderParser.hasUiFieldUiFactorySupport(m_context.getJavaProject())) {
throw new DesignerException(IExceptionConstants.UI_FIELD_FACTORY_FEATURE);
}
// prepare AST
AstEditor editor = getEditor();
// configure EditorState
EditorState.get(editor).initialize(
GwtToolkitDescription.INSTANCE.getId(),
m_object.getContext().getClassLoader());
// prepare name
String name = generateName();
// set "ui:field" attribute
{
String nameAttribute = getNameAttribute();
m_object.setAttribute(nameAttribute, name);
}
// prepare CreationDescription for Java
Class<?> componentClass = m_object.getDescription().getComponentClass();
org.eclipse.wb.internal.core.model.description.ComponentDescription javaDescription =
ComponentDescriptionHelper.getDescription(editor, componentClass);
CreationDescription creationDescription = javaDescription.getCreation(null);
// update Java
String fieldTypeName = ReflectionUtils.getCanonicalName(componentClass);
String creationSource = creationDescription.getSource();
// apply generics
{
Set<Entry<String, TypeParameterDescription>> entrySet =
creationDescription.getTypeParameters().entrySet();
if (!entrySet.isEmpty()) {
fieldTypeName += "<";
for (Entry<String, TypeParameterDescription> entry : entrySet) {
// use type bounds, until we have dialog to ask type arguments
String typeArgument = entry.getValue().getTypeName();
// field type
if (!fieldTypeName.endsWith("<")) {
fieldTypeName += ", ";
}
fieldTypeName += typeArgument;
// creation source
creationSource =
StringUtils.replace(creationSource, "%" + entry.getKey() + "%", typeArgument);
}
fieldTypeName += ">";
}
}
addUiFieldJava(componentClass, fieldTypeName, name, creationSource);
}
/**
* Adds @UiField with given type, name and initializer.
*/
public void addUiFieldJava(Class<?> fieldClass,
String fieldTypeName,
String name,
String initializer) throws Exception {
AstEditor editor = getEditor();
TypeDeclaration typeDeclaration = editor.getPrimaryType();
// prepare source lines
List<String> lines;
{
lines = Lists.newArrayList();
String source = "@com.google.gwt.uibinder.client.UiField(provided=true) ";
source += fieldTypeName + " " + name + " = " + initializer + ";";
Collections.addAll(lines, StringUtils.split(source, '\n'));
}
// add field
BodyDeclarationTarget target = getNewFieldTarget(typeDeclaration);
editor.addFieldDeclaration(lines, target);
// add new JField into "form" JType
addFormJField(fieldClass, name);
}
/**
* Adds <code>JField</code> with given name and @UiField annotation into "form" <code>JType</code>
* .
*/
private void addFormJField(Class<?> componentClass, String name) throws Exception {
IDevModeBridge bridge = m_context.getState().getDevModeBridge();
ClassLoader devClassLoader = bridge.getDevClassLoader();
// prepare "form" JType
Object formType = bridge.findJType(m_context.getFormType().getFullyQualifiedName());
// prepare @UiField annotation instance
Class<?> uiFieldAnnotationClass =
devClassLoader.loadClass("com.google.gwt.uibinder.client.UiField");
java.lang.annotation.Annotation annotation =
(java.lang.annotation.Annotation) Proxy.newProxyInstance(
uiFieldAnnotationClass.getClassLoader(),
new Class[]{uiFieldAnnotationClass},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
return Boolean.TRUE;
}
});
// add new JField
Object newField;
{
Constructor<?> fieldConstructor;
Class<?> fieldClass;
if (m_context.getState().getVersion().isHigherOrSame(Utils.GWT_2_2)) {
fieldClass = devClassLoader.loadClass("com.google.gwt.dev.javac.typemodel.JField");
fieldConstructor =
ReflectionUtils.getConstructorBySignature(
fieldClass,
"<init>(com.google.gwt.dev.javac.typemodel.JClassType,java.lang.String,java.util.Map)");
} else {
fieldClass = devClassLoader.loadClass("com.google.gwt.core.ext.typeinfo.JField");
fieldConstructor =
ReflectionUtils.getConstructorBySignature(
fieldClass,
"<init>(com.google.gwt.core.ext.typeinfo.JClassType,java.lang.String,java.util.Map)");
}
newField =
fieldConstructor.newInstance(
formType,
name,
ImmutableMap.of(uiFieldAnnotationClass, annotation));
}
// set "widget" JType for JField
Object widgetType = bridge.findJType(ReflectionUtils.getCanonicalName(componentClass));
ReflectionUtils.invokeMethod(
newField,
"setType(com.google.gwt.core.ext.typeinfo.JType)",
widgetType);
}
////////////////////////////////////////////////////////////////////////////
//
// Temporary state
//
////////////////////////////////////////////////////////////////////////////
/**
* When we change name ourself, using "UiField" property, we don't want to change
* <code>*.ui.xml</code> file, because this causes reparsing and all related problems. So, we
* disable template changing temporary.
*/
private static boolean m_renaming;
/**
* @return the <code>true</code> if <code>*.ui.xml</code> is changing by {@link NameSupport} now.
*/
public static boolean isRenaming() {
return m_renaming;
}
////////////////////////////////////////////////////////////////////////////
//
// Field utils
//
////////////////////////////////////////////////////////////////////////////
/**
* @return the {@link AstEditor} for companion Java unit.
*/
private AstEditor getEditor() throws Exception {
return m_context.getFormEditor();
}
/**
* @return the {@link VariableDeclaration} of "@UiField" with given name, may be <code>null</code>
* .
*/
public static VariableDeclaration getBinderField(TypeDeclaration typeDeclaration,
final String name) {
final VariableDeclaration[] result = {null};
typeDeclaration.accept(new ASTVisitor() {
@Override
public void endVisit(FieldDeclaration node) {
if (isBinderField(node)) {
for (VariableDeclaration fragment : DomGenerics.fragments(node)) {
if (fragment.getName().getIdentifier().equals(name)) {
result[0] = fragment;
}
}
}
}
});
return result[0];
}
/**
* @return the {@link VariableDeclaration}s of with "@UiField(provided)".
*/
public static List<FieldDeclaration> getUiFields(TypeDeclaration typeDeclaration) {
final List<FieldDeclaration> fields = Lists.newArrayList();
typeDeclaration.accept(new ASTVisitor() {
@Override
public void endVisit(FieldDeclaration node) {
if (isBinderField(node)) {
fields.add(node);
}
}
});
return fields;
}
/**
* Adds @UiField with given type, name and initializer.
*/
public static String addUiFieldJava(UIObjectInfo contextObject,
Class<?> fieldClass,
String fieldTypeName,
String baseName,
String initializer) throws Exception {
NameSupport nameSupport = new NameSupport(contextObject);
String name = nameSupport.generateName(baseName);
nameSupport.addUiFieldJava(fieldClass, fieldTypeName, name, initializer);
return name;
}
/**
* @return the {@link BodyDeclarationTarget} to add new "@UiField".
*/
private static BodyDeclarationTarget getNewFieldTarget(TypeDeclaration typeDeclaration) {
final FieldDeclaration[] lastUiField = {null};
final FieldDeclaration[] binderCreate = {null};
typeDeclaration.accept(new ASTVisitor() {
@Override
public void endVisit(FieldDeclaration node) {
if (isBinderField(node)) {
lastUiField[0] = node;
}
if (isBinderCreate(node)) {
binderCreate[0] = node;
}
}
});
// after last @UiField
if (lastUiField[0] != null) {
return new BodyDeclarationTarget(lastUiField[0], false);
}
// after GWT.create(Binder.class)
if (binderCreate[0] != null) {
return new BodyDeclarationTarget(binderCreate[0], false);
}
// at type end
return new BodyDeclarationTarget(typeDeclaration, false);
}
/**
* @return <code>true</code> if given {@link FieldDeclaration} is "@UiField".
*/
public static boolean isBinderField(FieldDeclaration fieldDeclaration) {
for (IExtendedModifier modifier : DomGenerics.modifiers(fieldDeclaration)) {
if (modifier instanceof Annotation) {
Annotation annotation = (Annotation) modifier;
if (isBinderAnnotation(annotation)) {
return true;
}
}
}
return false;
}
/**
* @return <code>true</code> if given {@link Annotation} is "@UiField".
*/
public static boolean isBinderAnnotation(Annotation annotation) {
return AstNodeUtils.isSuccessorOf(annotation, "com.google.gwt.uibinder.client.UiField");
}
/**
* @return <code>true</code> if has initializer <code>GWT.create(UiBinder+.class)</code>.
*/
public static boolean isBinderCreate(FieldDeclaration fieldDeclaration) {
List<VariableDeclarationFragment> fragments = DomGenerics.fragments(fieldDeclaration);
for (VariableDeclaration fragment : fragments) {
if (fragment.getInitializer() instanceof MethodInvocation) {
MethodInvocation invocation = (MethodInvocation) fragment.getInitializer();
if (invocation.getName().getIdentifier().equals("create")
&& AstNodeUtils.isSuccessorOf(
invocation.getExpression(),
"com.google.gwt.core.client.GWT")) {
List<Expression> arguments = DomGenerics.arguments(invocation);
if (arguments.size() == 1 && arguments.get(0) instanceof TypeLiteral) {
TypeLiteral typeLiteral = (TypeLiteral) arguments.get(0);
if (AstNodeUtils.isSuccessorOf(
typeLiteral.getType(),
"com.google.gwt.uibinder.client.UiBinder")) {
return true;
}
}
}
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////
//
// Name utils
//
////////////////////////////////////////////////////////////////////////////
/**
* @return the full name (including namespace) for "field" attribute.
*/
private String getNameAttribute() {
DocumentElement rootElement = m_object.getElement().getRoot();
return rootElement.getTagNS() + "field";
}
/**
* @return the generated unique name basing on settings and info in *.wbp-component.xml
* description.
*/
private String generateName() throws Exception {
String baseName = getBaseName();
return generateName(baseName);
}
/**
* @return the generated unique name based on the given.
*/
private String generateName(String baseName) throws Exception {
final Set<String> identifiers = getExistingNames();
String uniqueName = CodeUtils.generateUniqueName(baseName, new Predicate<String>() {
public boolean apply(String name) {
return !identifiers.contains(name);
}
});
return uniqueName;
}
/**
* Traverses the entire hierarchy and gathers set of existing names.
*/
private Set<String> getExistingNames() throws Exception {
final Set<String> resultSet = Sets.newTreeSet();
// add ui:field
m_object.getRootXML().accept(new ObjectInfoVisitor() {
@Override
public void endVisit(ObjectInfo object) throws Exception {
if (object instanceof XmlObjectInfo) {
XmlObjectInfo xmlObject = (XmlObjectInfo) object;
if (!XmlObjectUtils.isImplicit(xmlObject)) {
String name = getName(xmlObject);
if (name != null) {
resultSet.add(name);
}
}
}
}
});
// add TypeDeclaration fields
{
AstEditor editor = getEditor();
TypeDeclaration typeDeclaration = editor.getPrimaryType();
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
for (VariableDeclarationFragment fragment : DomGenerics.fragments(fieldDeclaration)) {
String name = fragment.getName().getIdentifier();
resultSet.add(name);
}
}
}
// done
return resultSet;
}
/**
* @return the base variable name for given {@link XmlObjectInfo}.
*/
private String getBaseName() {
ComponentDescription description = m_object.getDescription();
String componentClassName = description.getComponentClass().getName();
// check type specific information
{
ComponentNameDescription nameDescription =
NamesManager.getNameDescription(description.getToolkit(), componentClassName);
if (nameDescription != null) {
return nameDescription.getName();
}
}
// check component parameter
{
String name = XmlObjectUtils.getParameter(m_object, NamesManager.NAME_PARAMETER);
if (!StringUtils.isEmpty(name)) {
return name;
}
}
// use default name
return NamesManager.getDefaultName(componentClassName);
}
}