/*******************************************************************************
* 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.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gdt.eclipse.designer.uibinder.Activator;
import com.google.gdt.eclipse.designer.uibinder.model.widgets.WidgetInfo;
import com.google.gdt.eclipse.designer.uibinder.parser.UiBinderContext;
import org.eclipse.wb.core.model.ObjectInfo;
import org.eclipse.wb.core.model.broadcast.ObjectInfoChildrenTree;
import org.eclipse.wb.internal.core.model.presentation.DefaultObjectPresentation;
import org.eclipse.wb.internal.core.model.presentation.IObjectPresentation;
import org.eclipse.wb.internal.core.model.property.ComplexProperty;
import org.eclipse.wb.internal.core.model.property.Property;
import org.eclipse.wb.internal.core.model.property.category.PropertyCategory;
import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor;
import org.eclipse.wb.internal.core.utils.jdt.core.CodeUtils;
import org.eclipse.wb.internal.core.utils.reflect.ClassMap;
import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils;
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.association.Association;
import org.eclipse.wb.internal.core.xml.model.association.DirectAssociation;
import org.eclipse.wb.internal.core.xml.model.broadcast.XmlObjectAddProperties;
import org.eclipse.wb.internal.core.xml.model.description.DescriptionPropertiesHelper;
import org.eclipse.wb.internal.core.xml.model.description.GenericPropertyDescription;
import org.eclipse.wb.internal.core.xml.model.property.GenericPropertyImpl;
import org.eclipse.wb.internal.core.xml.model.property.accessor.ExpressionAccessor;
import org.eclipse.wb.internal.core.xml.model.property.converter.ExpressionConverter;
import org.eclipse.wb.internal.core.xml.model.utils.ElementTarget;
import org.eclipse.wb.internal.core.xml.model.utils.XmlObjectUtils;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.swt.graphics.Image;
import org.apache.commons.lang.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Support for @UiChild positions.
*
* @author scheglov_ke
* @coverage GWT.UiBinder.model
*/
public final class UiChildSupport {
private static final String UI_CHILD = "com.google.gwt.uibinder.client.UiChild";
private final UiBinderContext m_context;
private final ClassMap<List<Description>> m_descriptions = ClassMap.create();
private final Map<WidgetInfo, Map<String, Position>> m_positions =
new MapMaker().weakKeys().makeMap();
private final Map<XmlObjectInfo, Property> m_properties = new MapMaker().weakKeys().makeMap();
////////////////////////////////////////////////////////////////////////////
//
// Constructor
//
////////////////////////////////////////////////////////////////////////////
public UiChildSupport(UiBinderContext context) {
m_context = context;
m_context.getBroadcastSupport().addListener(null, new ObjectInfoChildrenTree() {
public void invoke(ObjectInfo parent, List<ObjectInfo> children) throws Exception {
processTreeChildren(parent, children);
}
});
m_context.getBroadcastSupport().addListener(null, new XmlObjectAddProperties() {
public void invoke(XmlObjectInfo object, List<Property> properties) throws Exception {
Property property = getProperty(object);
if (property != null) {
properties.add(property);
}
}
});
}
////////////////////////////////////////////////////////////////////////////
//
// Implementation
//
////////////////////////////////////////////////////////////////////////////
private void processTreeChildren(ObjectInfo parent, List<ObjectInfo> children) throws Exception {
// exclude @UiChild widgets from parent
if (parent instanceof WidgetInfo) {
WidgetInfo widget = (WidgetInfo) parent;
// may be support for @UiChild is disabled
if (XmlObjectUtils.hasTrueParameter(widget, "UiChild.disabled")) {
return;
}
// include Position objects
{
Map<String, Position> tagToPosition = getPositions(widget);
for (Position position : tagToPosition.values()) {
children.add(position);
position.setParent(parent);
}
}
// exclude widgets
for (WidgetInfo child : widget.getChildren(WidgetInfo.class)) {
Position position = getPosition(widget, child);
if (position != null) {
children.remove(child);
}
}
}
}
/**
* @return the {@link Position} to which given child is bound, may be <code>null</code>.
*/
private Position getPosition(WidgetInfo widget, WidgetInfo child) throws Exception {
Map<String, Position> tagToPosition = getPositions(widget);
DocumentElement childElementParent = child.getElement().getParent();
String tag = childElementParent.getTagLocal();
return tagToPosition.get(tag);
}
/**
* @return the {@link Position} to which given object is bound as {@link WidgetInfo}, may be
* <code>null</code> if not {@link WidgetInfo} or not bound.
*/
private Position getPosition(XmlObjectInfo object) throws Exception {
if (object instanceof WidgetInfo && object.getParent() instanceof WidgetInfo) {
return getPosition((WidgetInfo) object.getParent(), (WidgetInfo) object);
}
return null;
}
/**
* Prepare information for {@link WidgetInfo}.
*/
private Map<String, Position> getPositions(WidgetInfo widget) throws Exception {
Map<String, Position> tagToPosition = m_positions.get(widget);
if (tagToPosition == null) {
// prepare tags to hide
Set<String> hideTags = Sets.newHashSet();
{
String hideString = XmlObjectUtils.getParameter(widget, "UiChild.hide");
if (hideString != null) {
String[] hideSplit = StringUtils.split(hideString);
hideTags = ImmutableSet.copyOf(hideSplit);
}
}
// remember positions for Widget
tagToPosition = Maps.newLinkedHashMap();
m_positions.put(widget, tagToPosition);
// fill positions
for (Description description : getSortedDescriptions(widget)) {
String tag = description.getTag();
if (!hideTags.contains(tag)) {
Position position = new Position(widget, description);
tagToPosition.put(tag, position);
}
}
}
return tagToPosition;
}
////////////////////////////////////////////////////////////////////////////
//
// "UiChild" property
//
////////////////////////////////////////////////////////////////////////////
/**
* @return the complex {@link Property} for parent {@link Position}.
*/
private Property getProperty(XmlObjectInfo object) throws Exception {
Property property = m_properties.get(object);
if (property == null) {
property = createProperty(object);
if (property != null) {
m_properties.put(object, property);
}
}
return property;
}
/**
* Creates {@link Property} for {@link #getProperty(XmlObjectInfo)}, may be <code>null</code>.
*/
private Property createProperty(XmlObjectInfo object) throws Exception {
// prepare @UiChild method
Method method;
{
Position position = getPosition(object);
if (position == null) {
return null;
}
method = position.getMethod();
}
// if no other parameters than Widget, then no property
if (method.getParameterTypes().length < 2) {
return null;
}
// prepare sub-properties
List<Property> subPropertiesList = Lists.newArrayList();
String[] parameterNames = getMethodParameterNames(method);
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 1; i < parameterTypes.length; i++) {
Property property = createProperty(object, parameterNames[i], parameterTypes[i]);
if (property != null) {
subPropertiesList.add(property);
}
}
// if no sub-properties, then no property
if (subPropertiesList.isEmpty()) {
return null;
}
// prepare @UiChild complex property
ComplexProperty methodProperty = new ComplexProperty("UiChild", "(Properties)");
methodProperty.setCategory(PropertyCategory.system(3));
methodProperty.setModified(true);
methodProperty.setTooltip("Properties for @UiChild arguments.");
// set sub-properties
methodProperty.setProperties(subPropertiesList);
return methodProperty;
}
/**
* @return the {@link Property} for single @UiChild parameter.
*/
private static Property createProperty(XmlObjectInfo object, String name, Class<?> type)
throws Exception {
ExpressionConverter converter = DescriptionPropertiesHelper.getConverterForType(type);
PropertyEditor editor = DescriptionPropertiesHelper.getEditorForType(type);
if (converter == null || editor == null) {
return null;
}
ExpressionAccessor accessor = new ExpressionAccessor(name) {
@Override
protected DocumentElement getExistingElement(XmlObjectInfo object) {
return object.getElement().getParent();
}
@Override
protected DocumentElement getElement(XmlObjectInfo object) {
return getExistingElement(object);
}
@Override
public Object getValue(XmlObjectInfo object) throws Exception {
UiBinderContext context = (UiBinderContext) object.getContext();
return context.getAttributeValue(getElement(object), m_attribute);
}
};
GenericPropertyDescription description =
new GenericPropertyDescription(name, name, type, accessor);
description.setConverter(converter);
description.setEditor(editor);
return new GenericPropertyImpl(object, description);
}
/**
* @return the names of {@link Method} parameters, not <code>null</code>.
*/
private String[] getMethodParameterNames(Method reflectionMethod) throws Exception {
IJavaProject javaProject = m_context.getJavaProject();
IMethod javaMethod =
CodeUtils.findMethod(
javaProject,
reflectionMethod.getDeclaringClass().getName(),
ReflectionUtils.getMethodSignature(reflectionMethod));
return javaMethod.getParameterNames();
}
////////////////////////////////////////////////////////////////////////////
//
// Description
//
////////////////////////////////////////////////////////////////////////////
/**
* @return the sorted {@link Description}s for methods of given {@link WidgetInfo}.
*/
private List<Description> getSortedDescriptions(WidgetInfo widget) throws Exception {
// prepare order of tags
final List<String> tagOrder;
{
String hideString = XmlObjectUtils.getParameter(widget, "UiChild.order");
if (hideString != null) {
String[] hideSplit = StringUtils.split(hideString);
tagOrder = ImmutableList.copyOf(hideSplit);
} else {
tagOrder = ImmutableList.of();
}
}
// sort descriptions
List<Description> descriptions = getDescriptions(widget);
Collections.sort(descriptions, new Comparator<Description>() {
public int compare(Description o1, Description o2) {
String tag1 = o1.getTag();
String tag2 = o2.getTag();
int index1 = tagOrder.indexOf(tag1);
int index2 = tagOrder.indexOf(tag2);
if (index1 != -1 && index2 != -1) {
return index1 - index2;
}
if (index1 != -1) {
return -1;
}
if (index2 != -1) {
return 1;
}
return tag1.compareTo(tag2);
}
});
return descriptions;
}
/**
* @return the {@link Description}s for methods of given {@link WidgetInfo}.
*/
private List<Description> getDescriptions(WidgetInfo widget) throws Exception {
Class<?> componentClass = widget.getDescription().getComponentClass();
List<Description> descriptions = m_descriptions.get(componentClass);
if (descriptions == null) {
descriptions = Lists.newArrayList();
for (Method method : componentClass.getMethods()) {
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
if (ReflectionUtils.isSuccessorOf(annotation, UI_CHILD)) {
descriptions.add(new Description(method, annotation));
}
}
}
m_descriptions.put(componentClass, descriptions);
}
return descriptions;
}
/**
* Description for single @UiChild annotated method.
*/
private static final class Description {
private final Method m_method;
private final String m_tag;
private final int m_limit;
private final Class<?> m_widgetClass;
////////////////////////////////////////////////////////////////////////////
//
// Constructor
//
////////////////////////////////////////////////////////////////////////////
public Description(Method method, Annotation annotation) throws Exception {
m_method = method;
m_tag = getTag(method, annotation);
m_limit = getLimit(annotation);
m_widgetClass = method.getParameterTypes()[0];
}
////////////////////////////////////////////////////////////////////////////
//
// Private access
//
////////////////////////////////////////////////////////////////////////////
private static String getTag(Method method, Annotation annotation) throws Exception {
String tag = (String) ReflectionUtils.invokeMethod(annotation, "tagname()");
if (StringUtils.isEmpty(tag)) {
tag = StringUtils.removeStart(method.getName(), "add").toLowerCase();
}
return tag;
}
private static Integer getLimit(Annotation annotation) throws Exception {
int limit = (Integer) ReflectionUtils.invokeMethod(annotation, "limit()");
if (limit == -1) {
limit = Integer.MAX_VALUE;
}
return limit;
}
////////////////////////////////////////////////////////////////////////////
//
// Access
//
////////////////////////////////////////////////////////////////////////////
public Method getMethod() {
return m_method;
}
public String getTag() {
return m_tag;
}
public int getLimit() {
return m_limit;
}
public Class<?> getWidgetClass() {
return m_widgetClass;
}
}
////////////////////////////////////////////////////////////////////////////
//
// Position
//
////////////////////////////////////////////////////////////////////////////
/**
* Model for @UiChild element.
*/
public final class Position extends ObjectInfo {
private final WidgetInfo m_widget;
private final Description m_description;
////////////////////////////////////////////////////////////////////////////
//
// Constructor
//
////////////////////////////////////////////////////////////////////////////
public Position(WidgetInfo widget, Description description) throws Exception {
m_widget = widget;
m_description = description;
}
////////////////////////////////////////////////////////////////////////////
//
// Access
//
////////////////////////////////////////////////////////////////////////////
public Method getMethod() {
return m_description.getMethod();
}
public String getTag() {
return m_description.getTag();
}
public Class<?> getWidgetClass() {
return m_description.getWidgetClass();
}
public boolean canAddChild() {
return getWidgets().size() < m_description.getLimit();
}
private List<WidgetInfo> getWidgets() {
List<WidgetInfo> widgets = Lists.newArrayList();
for (WidgetInfo child : m_widget.getChildren(WidgetInfo.class)) {
DocumentElement childElementParent = child.getElement().getParent();
String tag = childElementParent.getTagLocal();
String positionTag = m_description.getTag();
if (tag.equals(positionTag)) {
widgets.add(child);
}
}
return widgets;
}
////////////////////////////////////////////////////////////////////////////
//
// Presentation
//
////////////////////////////////////////////////////////////////////////////
@Override
public IObjectPresentation getPresentation() {
return new DefaultObjectPresentation(this) {
public String getText() throws Exception {
return m_description.getTag();
}
@Override
public Image getIcon() throws Exception {
return Activator.getImage("info/UiChild.png");
}
@Override
public List<ObjectInfo> getChildrenTree() throws Exception {
List<WidgetInfo> widgets = getWidgets();
return Lists.<ObjectInfo>newArrayList(widgets);
}
};
}
////////////////////////////////////////////////////////////////////////////
//
// Commands
//
////////////////////////////////////////////////////////////////////////////
public void command_CREATE(WidgetInfo widget, WidgetInfo reference) throws Exception {
XmlObjectUtils.add(widget, getAssociation(), m_widget, reference);
}
public void command_MOVE(WidgetInfo widget, WidgetInfo reference) throws Exception {
XmlObjectUtils.move(widget, getAssociation(), m_widget, reference);
}
private Association getAssociation() {
return new DirectAssociation() {
@Override
public void add(XmlObjectInfo object, ElementTarget target) throws Exception {
target = prepareTarget(target);
super.add(object, target);
}
@Override
//@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "BC_UNCONFIRMED_CAST")
public void move(XmlObjectInfo object,
ElementTarget target,
XmlObjectInfo oldParent,
XmlObjectInfo newParent) throws Exception {
WidgetInfo widget = (WidgetInfo) object;
// reorder, use same "position" element
{
Position position = getPosition(m_widget, widget);
if (position == Position.this) {
DocumentElement targetElement = target.getElement();
int targetIndex = target.getIndex();
targetElement.moveChild(object.getElement().getParent(), targetIndex);
return;
}
}
// create new "position" element
target = prepareTarget(target);
super.move(object, target, oldParent, newParent);
}
private ElementTarget prepareTarget(ElementTarget target) {
// prepare "position" element
String tag = m_widget.getElement().getTagNS() + m_description.getTag();
DocumentElement positionElement = new DocumentElement(tag);
// add "position" element
DocumentElement targetElement = target.getElement();
int targetIndex = target.getIndex();
targetElement.addChild(positionElement, targetIndex);
// prepare new target
return new ElementTarget(positionElement, 0);
}
};
}
}
}