org.eclipse.wb.android.internal.parser.AndroidDescriptionRulesProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.wb.android.internal.parser.AndroidDescriptionRulesProvider.java

Source

/*******************************************************************************
 * Copyright (c) 2011 Alexander Mitin (Alexander.Mitin@gmail.com)
 * 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
 *
 * Contributors:
 *    Alexander Mitin (Alexander.Mitin@gmail.com) - initial API and implementation
 *******************************************************************************/
package org.eclipse.wb.android.internal.parser;

import com.google.common.collect.Sets;

import org.eclipse.wb.android.internal.model.property.IPropertiesConstants;
import org.eclipse.wb.android.internal.model.property.accessor.AndroidExpressionAccessor;
import org.eclipse.wb.android.internal.model.property.accessor.FieldJavaValueAccessor;
import org.eclipse.wb.android.internal.model.property.accessor.IJavaValueAccessor;
import org.eclipse.wb.android.internal.model.property.accessor.MethodJavaValueAccessor;
import org.eclipse.wb.android.internal.model.property.converter.AndroidConverters;
import org.eclipse.wb.android.internal.model.property.editor.ColorPropertyEditor;
import org.eclipse.wb.android.internal.model.property.editor.DefaultEnumPropertyEditor;
import org.eclipse.wb.android.internal.model.property.editor.DefaultFlagsPropertyEditor;
import org.eclipse.wb.android.internal.model.property.editor.ReferencePropertyEditor;
import org.eclipse.wb.android.internal.model.property.editor.delegating.DelegatingPropertyEditor;
import org.eclipse.wb.android.internal.support.AndroidBridge;
import org.eclipse.wb.android.internal.support.AndroidUtils;
import org.eclipse.wb.internal.core.model.property.editor.BooleanPropertyEditor;
import org.eclipse.wb.internal.core.model.property.editor.FloatPropertyEditor;
import org.eclipse.wb.internal.core.model.property.editor.IntegerPropertyEditor;
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.ReflectionUtils;
import org.eclipse.wb.internal.core.xml.model.EditorContext;
import org.eclipse.wb.internal.core.xml.model.description.ComponentDescription;
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.description.IDescriptionRulesProvider;
import org.eclipse.wb.internal.core.xml.model.property.accessor.ExpressionAccessor;
import org.eclipse.wb.internal.core.xml.model.property.accessor.FieldExpressionAccessor;
import org.eclipse.wb.internal.core.xml.model.property.accessor.MethodExpressionAccessor;
import org.eclipse.wb.internal.core.xml.model.property.converter.ExpressionConverter;
import org.eclipse.wb.internal.core.xml.model.utils.NamespacesHelper;

import com.android.ide.common.api.IAttributeInfo;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.layout.LayoutConstants;
import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.common.resources.platform.DeclareStyleableInfo;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.lang.StringUtils;
import org.xml.sax.Attributes;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Adds a rule for working with Android xml attributes as properties.
 * 
 * @author mitin_aa
 * @coverage android.parser
 */
@SuppressWarnings("restriction")
public class AndroidDescriptionRulesProvider implements IDescriptionRulesProvider {
    public void addRules(Digester digester, EditorContext context, Class<?> componentClass) {
        if (ReflectionUtils.isSuccessorOf(componentClass, "android.view.View")
                || ReflectionUtils.isSuccessorOf(componentClass, "android.view.ViewGroup$LayoutParams")) {
            addAndroidPropertiesRule(digester, context);
        }
    }

    private void addAndroidPropertiesRule(final Digester digester, EditorContext ctx) {
        // hook on "standard-bean-properties"
        String pattern = "component/standard-bean-properties";
        digester.addRule(pattern, new AndroidPropertiesRule((AndroidEditorContext) ctx));
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // AndroidPropertiesRule
    //
    ////////////////////////////////////////////////////////////////////////////
    private static final class AndroidPropertiesRule extends Rule {
        private final AndroidEditorContext m_context;
        private final AndroidBridge m_androidBridge;

        ////////////////////////////////////////////////////////////////////////////
        //
        // Constructor
        //
        ////////////////////////////////////////////////////////////////////////////
        private AndroidPropertiesRule(AndroidEditorContext context) {
            m_context = context;
            m_androidBridge = m_context.getAndroidBridge();
        }

        ////////////////////////////////////////////////////////////////////////////
        //
        // Rule
        //
        ////////////////////////////////////////////////////////////////////////////
        @Override
        public void begin(String namespace, String name, Attributes attributes) throws Exception {
            ComponentDescription componentDescription = (ComponentDescription) digester.peek();
            addProperties(componentDescription);
        }

        ////////////////////////////////////////////////////////////////////////////
        //
        // Properties
        //
        ////////////////////////////////////////////////////////////////////////////
        private void addProperties(ComponentDescription componentDescription) throws Exception {
            Class<?> componentClass = componentDescription.getComponentClass();
            // create set
            Set<NamespacedAttribute> attrsSet = Sets.newHashSet();
            // collect
            collectAttributes(componentClass, attrsSet);
            for (Iterator<NamespacedAttribute> I = attrsSet.iterator(); I.hasNext();) {
                NamespacedAttribute attribute = I.next();
                createPropertyForAttribute(componentDescription, attribute,
                        m_androidBridge.getEnumFlagValues(componentClass, attribute.attribute.getName()));
            }
        }

        /**
         * Invokes {@link #collectAttributes0} and adds 'style' attribute if needed.
         */
        private void collectAttributes(Class<?> componentClass, Set<NamespacedAttribute> attrsSet)
                throws Exception {
            collectAttributes0(componentClass, attrsSet);
            if (!AndroidUtils.isLayoutParamClass(componentClass)) {
                // All views and groups have an implicit "style" attribute which is a reference.
                AttributeInfo styleInfo = new AttributeInfo("style", new Format[] { Format.REFERENCE });
                styleInfo.setJavaDoc("A reference to a custom style");
                attrsSet.add(NamespacedAttribute.getNamespaced("", styleInfo));
            }
        }

        /**
         * Recursive. Collects xml attributes.
         */
        private void collectAttributes0(Class<?> componentClass, Set<NamespacedAttribute> attrsSet)
                throws Exception {
            if (componentClass.getSuperclass() != null) {
                collectAttributes0(componentClass.getSuperclass(), attrsSet);
            }
            DeclareStyleableInfo style = m_androidBridge.getStyleable(componentClass,
                    getStyleableKey(componentClass));
            if (style != null) {
                AttributeInfo[] attributes = style.getAttributes();
                CollectionUtils.addAll(attrsSet,
                        NamespacedAttribute.getNamespaced(
                                AndroidUtils.getNamespace(componentClass, m_context.getJavaProject().getProject()),
                                attributes));
            }
        }

        /**
         * Creates single attribute-based property for given attribute info. Tries to match java-based
         * properties to attribute-based.
         */
        private void createPropertyForAttribute(ComponentDescription componentDescription,
                NamespacedAttribute attribute, Map<String, Integer> enumFlagValues) throws Exception {
            IAttributeInfo attributeInfo = attribute.attribute;
            String namespaceUri = attribute.namespace;
            String localAttributeName = attributeInfo.getName();
            // prepare property parts
            String id = StringUtils.isEmpty(namespaceUri) ? localAttributeName
                    : NamespacesHelper.getName(m_context.getRootElement(), namespaceUri) + ":" + localAttributeName;
            // prepare java-based value accessor
            IJavaValueAccessor javaValueAccessor = createJavaValueAccessor(componentDescription,
                    localAttributeName);
            // create the accessor
            Format[] formats = attributeInfo.getFormats();
            ExpressionAccessor accessor = new AndroidExpressionAccessor(localAttributeName, namespaceUri,
                    attributeInfo.getJavaDoc(), javaValueAccessor,
                    AndroidConverters.getObjectConverter(formats, enumFlagValues));
            ExpressionConverter converter = AndroidConverters.getExpressionConverter(formats, enumFlagValues);
            // editor
            PropertyEditor editor = getPropertyEditor(attributeInfo, enumFlagValues);
            // create property descriptor
            GenericPropertyDescription propertyDescriptor = new GenericPropertyDescription(id, localAttributeName,
                    null, accessor);
            // remove 'layout_' prefix
            if (localAttributeName.startsWith(LayoutConstants.ATTR_LAYOUT_PREFIX)) {
                propertyDescriptor.putTag("title",
                        StringUtils.removeStart(localAttributeName, LayoutConstants.ATTR_LAYOUT_PREFIX));
            }
            // converter & editor
            propertyDescriptor.setConverter(converter);
            propertyDescriptor.setEditor(editor);
            // store attribute local name, namespace & attribute info
            propertyDescriptor.putArbitraryValue(IPropertiesConstants.KEY_ATTRIBUTE_NAMESPACE_URI, namespaceUri);
            propertyDescriptor.putArbitraryValue(IPropertiesConstants.KEY_ATTRIBUTE_LOCAL_NAME, localAttributeName);
            propertyDescriptor.putArbitraryValue(IPropertiesConstants.KEY_ATTRIBUTE, attributeInfo);
            // store enums & flags values
            if (Format.FLAG.in(formats) || Format.ENUM.in(formats)) {
                propertyDescriptor.putArbitraryValue(IPropertiesConstants.KEY_ENUMS_FLAGS_VALUES_MAP,
                        enumFlagValues);
            }
            componentDescription.addProperty(propertyDescriptor);
        }

        /**
         * Tries to find the way how to get a default property value from java object.
         */
        private IJavaValueAccessor createJavaValueAccessor(ComponentDescription componentDescription,
                String localAttributeName) {
            Class<?> componentClass = componentDescription.getComponentClass();
            // remove 'layout_' prefix, layout params attrs have such prefix
            if (localAttributeName.startsWith(LayoutConstants.ATTR_LAYOUT_PREFIX)) {
                localAttributeName = StringUtils.removeStart(localAttributeName,
                        LayoutConstants.ATTR_LAYOUT_PREFIX);
            }
            // search for added class based property matching attribute
            String javaPropertyId = null;
            List<GenericPropertyDescription> properties = componentDescription.getProperties();
            for (GenericPropertyDescription propertyDescription : properties) {
                if (propertyDescription.getAccessor() instanceof AndroidExpressionAccessor) {
                    // don't check twice
                    continue;
                }
                String propertyName = propertyDescription.getName();
                if (localAttributeName.equals(propertyName)) {
                    javaPropertyId = propertyDescription.getId();
                    break;
                }
            }
            // try to get a method getter or field from java-based accessor, if any
            if (javaPropertyId != null) {
                GenericPropertyDescription javaPropertyDescriptor = componentDescription
                        .getProperty(javaPropertyId);
                if (javaPropertyDescriptor.getAccessor() instanceof MethodExpressionAccessor) {
                    MethodExpressionAccessor accessor = (MethodExpressionAccessor) javaPropertyDescriptor
                            .getAccessor();
                    Method getter = accessor.getGetter();
                    if (getter != null) {
                        return new MethodJavaValueAccessor(getter);
                    }
                } else if (javaPropertyDescriptor.getAccessor() instanceof FieldExpressionAccessor) {
                    FieldExpressionAccessor accessor = (FieldExpressionAccessor) javaPropertyDescriptor
                            .getAccessor();
                    Field field = accessor.getField();
                    if (field != null) {
                        return new FieldJavaValueAccessor(field);
                    }
                }
            }
            // look for field named as attribute
            {
                Field field = ReflectionUtils.getFieldByName(componentClass, localAttributeName);
                if (field != null) {
                    return new FieldJavaValueAccessor(field);
                }
            }
            // try special debug field
            {
                String fieldName = "m" + StringUtils.capitalize(localAttributeName);
                Field field = ReflectionUtils.getFieldByName(componentClass, fieldName);
                if (field != null) {
                    return new FieldJavaValueAccessor(field);
                }
            }
            return null;
        }

        /**
         * Creates the property editor for Android property.
         */
        private PropertyEditor getPropertyEditor(IAttributeInfo attribute, Map<String, Integer> enumFlagValues)
                throws Exception {
            PropertyEditor editor = getPropertyEditorPrimitive(attribute, enumFlagValues);
            Format[] formats = attribute.getFormats();
            if (Format.REFERENCE.in(formats)) {
                if (editor != null) {
                    DelegatingPropertyEditor delegatingEditor = new DelegatingPropertyEditor();
                    delegatingEditor.add(new ReferencePropertyEditor());
                    delegatingEditor.add(editor);
                    editor = delegatingEditor;
                } else {
                    editor = new ReferencePropertyEditor();
                }
            }
            return editor;
        }

        /**
         * Creates the property editor for Android property for simple types.
         */
        private PropertyEditor getPropertyEditorPrimitive(IAttributeInfo attributeInfo,
                Map<String, Integer> enumFlagValues) throws Exception {
            Format[] formats = attributeInfo.getFormats();
            if (Format.FLAG.in(formats)) {
                return new DefaultFlagsPropertyEditor(enumFlagValues);
            }
            if (Format.ENUM.in(formats)) {
                return new DefaultEnumPropertyEditor(enumFlagValues);
            }
            if (Format.BOOLEAN.in(formats)) {
                return BooleanPropertyEditor.INSTANCE;
            }
            if (Format.COLOR.in(formats)) {
                return ColorPropertyEditor.INSTANCE;
            }
            if (Format.INTEGER.in(formats)) {
                return IntegerPropertyEditor.INSTANCE;
            }
            if (Format.FLOAT.in(formats)) {
                return FloatPropertyEditor.INSTANCE;
            }
            return DescriptionPropertiesHelper.getEditorForType(String.class);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Inner classes
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Wrapper for attribute to have a namespace.
     */
    private static final class NamespacedAttribute {
        private final IAttributeInfo attribute;
        private final String namespace;

        ////////////////////////////////////////////////////////////////////////////
        //
        // Constructor
        //
        ////////////////////////////////////////////////////////////////////////////
        private NamespacedAttribute(String namespace, AttributeInfo attribute) {
            this.namespace = namespace;
            this.attribute = attribute;
        }

        ////////////////////////////////////////////////////////////////////////////
        //
        // Object
        //
        ////////////////////////////////////////////////////////////////////////////
        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof NamespacedAttribute)) {
                return false;
            }
            NamespacedAttribute other = (NamespacedAttribute) obj;
            return attribute.getName().equals(other.attribute.getName()) && namespace.equals(other.namespace);
        }

        @Override
        public int hashCode() {
            return (namespace + attribute.getName()).hashCode();
        }

        ////////////////////////////////////////////////////////////////////////////
        //
        // Access
        //
        ////////////////////////////////////////////////////////////////////////////
        public static NamespacedAttribute[] getNamespaced(String namespace, AttributeInfo[] attributes) {
            NamespacedAttribute[] named = new NamespacedAttribute[attributes.length];
            for (int i = 0; i < attributes.length; i++) {
                named[i] = new NamespacedAttribute(namespace, attributes[i]);
            }
            return named;
        }

        public static NamespacedAttribute getNamespaced(String namespace, AttributeInfo attribute) {
            return new NamespacedAttribute(namespace, attribute);
        }
    }

    /**
     * @return the key string to find a styleable in styleable map (attrs.xml).
     */
    private static String getStyleableKey(Class<?> componentClass) {
        // HACK: TableRow.LayoutParams has TableRow_Cell key
        if (ReflectionUtils.isSuccessorOf(componentClass, "android.widget.TableRow$LayoutParams")) {
            return "TableRow_Cell";
        }
        // for layout params it is like'LinearLayout_Layout', for ordinary view it is the short class name.
        String className = componentClass.getName();
        String classShortName = CodeUtils.getShortClass(className);
        String key;
        if (AndroidUtils.isLayoutParamClass(componentClass)) {
            Class<?> enclosingClass = componentClass.getEnclosingClass();
            // Transforms "LinearLayout" and "LayoutParams" into "LinearLayout_Layout".
            String xmlName = String.format("%1$s_%2$s", CodeUtils.getShortClass(enclosingClass.getName()),
                    classShortName);
            key = xmlName.replaceFirst("Params$", "");
        } else {
            key = classShortName;
        }
        return key;
    }
}