org.eclipse.wb.android.internal.model.property.event.AndroidEventProperty.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.wb.android.internal.model.property.event.AndroidEventProperty.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.model.property.event;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import org.eclipse.wb.android.internal.editor.AndroidPairResourceProvider;
import org.eclipse.wb.android.internal.model.widgets.ViewInfo;
import org.eclipse.wb.android.internal.support.AndroidUtils;
import org.eclipse.wb.android.internal.support.IdSupport;
import org.eclipse.wb.core.editor.IContextMenuConstants;
import org.eclipse.wb.core.model.broadcast.ObjectEventListener;
import org.eclipse.wb.internal.core.DesignerPlugin;
import org.eclipse.wb.internal.core.model.property.Property;
import org.eclipse.wb.internal.core.model.property.event.EventsPropertyUtils;
import org.eclipse.wb.internal.core.model.util.ObjectInfoAction;
import org.eclipse.wb.internal.core.model.variable.NamesManager;
import org.eclipse.wb.internal.core.utils.GenericTypeResolver;
import org.eclipse.wb.internal.core.utils.GenericsUtils;
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.AstParser;
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.ast.StatementTarget;
import org.eclipse.wb.internal.core.utils.execution.ExecutionUtils;
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.XmlObjectInfo;
import org.eclipse.wb.internal.core.xml.model.property.event.AbstractListenerProperty;

import org.eclipse.core.resources.IFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnonymousTypeDeclaration2;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.ITextEditor;

import org.apache.commons.lang.StringUtils;

import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.List;

/**
 * {@link Property} for single Android UI event.
 * 
 * @author mitin_aa
 * @coverage android.model.property
 */
public final class AndroidEventProperty extends AbstractListenerProperty {
    // constants
    private static final String IDENTIFIER_FIND_VIEW_BY_ID = "findViewById";
    private static final String SIGNATURE_FIND_VIEW_BY_ID = IDENTIFIER_FIND_VIEW_BY_ID + "(int)";
    private static final String SIGNATURE_SET_CONTENT_VIEW = "setContentView(int)";
    private static final String KEY_COMPANION_EDITOR = "companion editor";
    // fields
    private final ViewInfo m_view;
    private final ListenerInfo m_listener;
    private IFile m_javaFile;

    ////////////////////////////////////////////////////////////////////////////
    //
    // Constructor
    //
    ////////////////////////////////////////////////////////////////////////////
    public AndroidEventProperty(XmlObjectInfo object, ListenerInfo listener) {
        super(object, listener.getSimpleName(), AndroidEventPropertyEditor.INSTANCE);
        m_view = (ViewInfo) object;
        m_listener = listener;
        m_view.getRoot().addBroadcastListener(new ObjectEventListener() {
            @Override
            public void refreshDispose() throws Exception {
                // remove cached editor 
                m_view.getRoot().putArbitraryValue(KEY_COMPANION_EDITOR, null);
                clearAST();
            }
        });
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Property
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    public boolean isModified() throws Exception {
        return getSetListenerLine() != -1;
    }

    @Override
    public void setValue(Object value) throws Exception {
        if (value == UNKNOWN_VALUE) {
            if (MessageDialog.openConfirm(DesignerPlugin.getShell(), "Confirm",
                    "Are you sure to delete '" + m_listener.getSimpleName() + "' event handler?")) {
                removeListener();
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Access
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    protected void removeListener() throws Exception {
        if (!prepareAST()) {
            return;
        }
        MethodInvocation listenerMethod = findSetListenerMethod();
        if (listenerMethod != null) {
            m_editor.removeStatement(AstNodeUtils.getEnclosingStatement(listenerMethod));
            saveAST();
            ExecutionUtils.refresh(m_object);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Context menu
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    protected void addListenerActions(IMenuManager manager, IMenuManager implementMenuManager) throws Exception {
        IAction[] actions = createListenerMethodActions();
        // append existing stub action
        if (actions[0] != null) {
            manager.appendToGroup(IContextMenuConstants.GROUP_EVENTS, actions[0]);
        }
        // append existing or new method action
        implementMenuManager.add(actions[0] != null ? actions[0] : actions[1]);
    }

    /**
     * For given {@link ListenerMethodProperty} creates two {@link Action}'s:
     * 
     * [0] - for existing stub method, may be <code>null</code>;<br>
     * [1] - for creating new stub method.
     */
    private IAction[] createListenerMethodActions() throws Exception {
        IAction[] actions = new IAction[2];
        // try to find existing
        {
            int line = getSetListenerLine();
            if (line != -1) {
                actions[0] = new ObjectInfoAction(m_object) {
                    @Override
                    protected void runEx() throws Exception {
                        openListener();
                    }
                };
                actions[0].setText(m_listener.getSimpleName() + " -> line " + line);
                actions[0].setImageDescriptor(EventsPropertyUtils.LISTENER_METHOD_IMAGE_DESCRIPTOR);
            }
        }
        // in any case prepare action for creating new stub method
        {
            actions[1] = new ObjectInfoAction(m_object) {
                @Override
                protected void runEx() throws Exception {
                    openListener();
                }
            };
            actions[1].setText(m_listener.getSimpleName());
            actions[1].setImageDescriptor(EventsPropertyUtils.LISTENER_METHOD_IMAGE_DESCRIPTOR);
        }
        //
        return actions;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Handler
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    protected void openListener() throws Exception {
        if (!prepareAST()) {
            return;
        }
        MethodDeclaration mdOnCreate = AstNodeUtils.getMethodByName(m_typeDeclaration, "onCreate");
        String id = IdSupport.getSimpleId(IdSupport.getId(m_view));
        // get target to add set-listener method invocation
        // TODO: only local unique variables supported
        Statement stFindViewById = ensureFindViewById(mdOnCreate, id);
        if (stFindViewById == null) {
            return;
        }
        // get reference expression to variable
        String referenceExpression = getReferenceExpression(stFindViewById);
        MethodInvocation setListenerMethod = findSetListenerMethod(mdOnCreate, referenceExpression);
        if (setListenerMethod == null) {
            StatementTarget target = new StatementTarget(stFindViewById, false);
            // prepare listener source
            String source = "new " + getListenerTypeNameSource() + "() {\n}";
            // add listener and get added listener type
            setListenerMethod = addMethodInvocation(referenceExpression, target, m_listener.getMethodSignature(),
                    source);
            // ensure listener type
            TypeDeclaration listenerType = findListenerTypeDeclaration(mdOnCreate, referenceExpression);
            // implement all methods
            {
                List<ListenerMethodInfo> interfaceMethods = m_listener.getMethods();
                for (ListenerMethodInfo interfaceMethodInfo : interfaceMethods) {
                    if (interfaceMethodInfo.isAbstract()) {
                        addListenerMethod(listenerType, interfaceMethodInfo);
                    }
                }
            }
        }
        saveAST();
        ExecutionUtils.refresh(m_object);
        {
            MethodDeclaration method = findListenerMethod(setListenerMethod, referenceExpression);
            openMethodInEditor(method);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Working with AST
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return a variable name to get referent with.
     */
    private String getReferenceExpression(Statement statement) {
        if (statement instanceof VariableDeclarationStatement) {
            VariableDeclarationStatement vdStatement = (VariableDeclarationStatement) statement;
            List<?> fragments = vdStatement.fragments();
            for (Object fragment : fragments) {
                if (fragment instanceof VariableDeclarationFragment) {
                    VariableDeclarationFragment vdFragment = (VariableDeclarationFragment) fragment;
                    return vdFragment.getName().getIdentifier();
                }
            }
        }
        return null;
    }

    /**
     * Adds {@link MethodInvocation}.
     */
    private MethodInvocation addMethodInvocation(String reference, StatementTarget target, String signature,
            String arguments) throws Exception {
        // create invocation source
        String invocationSource;
        {
            String methodName = StringUtils.substringBefore(signature, "(");
            invocationSource = MessageFormat.format("{0}.{1}({2});", reference, methodName, arguments);
        }
        // add statement with invocation
        ExpressionStatement statement = (ExpressionStatement) m_editor.addStatement(invocationSource, target);
        return (MethodInvocation) statement.getExpression();
    }

    /**
     * @return the {@link TypeDeclaration} for the listener added.
     */
    private TypeDeclaration findListenerTypeDeclaration(ASTNode node, String referenceExpression) {
        Expression argument = getListenerArgumentExpression(node, referenceExpression);
        if (argument != null) {
            // check for "this"
            if (argument instanceof ThisExpression) {
                return AstNodeUtils.getEnclosingType(argument);
            }
            // check for listener creation
            if (argument instanceof ClassInstanceCreation) {
                ClassInstanceCreation creation = (ClassInstanceCreation) argument;
                // check for anonymous class
                if (creation.getAnonymousClassDeclaration() != null) {
                    return new AnonymousTypeDeclaration2(creation.getAnonymousClassDeclaration());
                }
                // find inner type
                return AstNodeUtils.getTypeDeclaration(creation);
            }
        }
        // no listener found
        return null;
    }

    /**
     * @return the {@link Expression} used as direct argument for <code>setOnXXXListener()</code>.
     */
    private Expression getListenerArgumentExpression(ASTNode node, String reference) {
        MethodInvocation invocation = findSetListenerMethod(node, reference);
        if (invocation != null) {
            return (Expression) invocation.arguments().get(0);
        }
        // no listener found
        return null;
    }

    /**
     * @return the listener method in event listener.
     */
    private MethodDeclaration findListenerMethod(ASTNode node, String referenceExpression) {
        TypeDeclaration listenerType = findListenerTypeDeclaration(node, referenceExpression);
        if (listenerType != null) {
            return AstNodeUtils.getMethodBySignature(listenerType,
                    m_listener.getMethods().get(0).getSignatureAST());
        }
        return null;
    }

    /**
     * @return the line number for set-listener method or -1 if no such invocation.
     */
    int getSetListenerLine() throws Exception {
        if (prepareAST()) {
            MethodInvocation listenerMethod = findSetListenerMethod();
            if (listenerMethod != null) {
                return m_editor.getLineNumber(listenerMethod.getStartPosition());
            }
        }
        return -1;
    }

    /**
     * @return the set-listener method invocation, ex. button1.setOnClickListener(). Returns
     *         <code>null</code> if the View has no id or this View is not yet referenced in Java.
     */
    private MethodInvocation findSetListenerMethod() throws Exception {
        MethodDeclaration onCreateMethod = AstNodeUtils.getMethodByName(m_typeDeclaration, "onCreate");
        String id = IdSupport.getSimpleId(IdSupport.getIdOrNull(m_view));
        // no id set yet
        if (id == null) {
            return null;
        }
        MethodInvocation miFindViewById = getMethodInvocationWithId(onCreateMethod, SIGNATURE_FIND_VIEW_BY_ID, id);
        // not referenced in Java
        if (miFindViewById == null) {
            return null;
        }
        // get reference expression to variable
        String reference = getReferenceExpression(AstNodeUtils.getEnclosingStatement(miFindViewById));
        return findSetListenerMethod(onCreateMethod, reference);
    }

    /**
     * Searches for set-listener method within <code>node</code> using <code>reference</code>
     * variable.
     */
    private MethodInvocation findSetListenerMethod(ASTNode node, String reference) {
        String addListenerMethodSignature = m_listener.getMethodSignature();
        // try to find listener adding
        return getMethodInvocationWithReference(node, addListenerMethodSignature, reference);
    }

    /**
     * Adds listener method implementation.
     */
    private MethodDeclaration addListenerMethod(TypeDeclaration typeDeclaration, ListenerMethodInfo methodInfo)
            throws Exception {
        // prepare annotations
        List<String> annotations = Lists.newArrayList();
        // prepare parameter names
        String[] parameterNames = null;
        {
            String listenerTypeName = m_listener.getListenerType().getCanonicalName();
            IType listenerType = m_editor.getJavaProject().findType(listenerTypeName);
            IMethod listenerMethod = CodeUtils.findMethod(listenerType, methodInfo.getSignature());
            parameterNames = listenerMethod.getParameterNames();
        }
        // prepare header code
        String headerCode;
        {
            // prepare parameters
            String parametersCode = "";
            {
                String[] parameterTypes = methodInfo.getActualParameterTypes();
                for (int i = 0; i < parameterTypes.length; i++) {
                    String parameterType = parameterTypes[i];
                    // comma
                    if (parametersCode.length() != 0) {
                        parametersCode += ", ";
                    }
                    // append type
                    parametersCode += parameterType;
                    parametersCode += " ";
                    // append name
                    parametersCode += parameterNames[i];
                }
            }
            // prepare full header code
            headerCode = "public " + methodInfo.getMethod().getReturnType().getName() + " " + methodInfo.getName()
                    + "(" + parametersCode + ")";
        }
        // prepare body
        List<String> bodyLines = getListenerMethodBody(methodInfo);
        // add method
        BodyDeclarationTarget target = new BodyDeclarationTarget(typeDeclaration, false);
        return m_editor.addMethodDeclaration(annotations, headerCode, bodyLines, target);
    }

    private static List<String> getListenerMethodBody(ListenerMethodInfo methodInfo) {
        Class<?> returnType = methodInfo.getMethod().getReturnType();
        if (returnType == Void.TYPE) {
            return ImmutableList.of();
        } else {
            String defaultValue = AstParser.getDefaultValue(returnType.getName());
            return ImmutableList.of("return " + defaultValue + ";");
        }
    }

    /**
     * @return the name of listener type, including generic arguments.
     */
    private String getListenerTypeNameSource() {
        // simple case - no generics
        {
            Class<?> listenerType = m_listener.getListenerType();
            if (listenerType.getTypeParameters().length == 0) {
                return listenerType.getCanonicalName();
            }
        }
        // listener with generics
        Type listenerType = m_listener.getMethod().getGenericParameterTypes()[0];
        GenericTypeResolver resolver_2 = m_listener.getResolver();
        return GenericsUtils.getTypeName(resolver_2, listenerType);
    }

    /**
     * Finds 'findViewById' invocation to get local variable for set listener. Adds local variable if
     * not found.
     * 
     * @return statement after which a set-listener method invocation should be added.
     */
    private Statement ensureFindViewById(MethodDeclaration methodDeclaration, String id) throws Exception {
        MethodInvocation methodInvocation = getMethodInvocationWithId(methodDeclaration, SIGNATURE_FIND_VIEW_BY_ID,
                id);
        if (methodInvocation != null) {
            return AstNodeUtils.getEnclosingStatement(methodInvocation);
        }
        MethodInvocation miSetContentView = getMethodInvocationWithId(methodDeclaration, SIGNATURE_SET_CONTENT_VIEW,
                getThisLayoutId());
        if (miSetContentView == null) {
            // can't determine where to place local variable maybe TODO: add field?
            return null;
        }
        StatementTarget target = new StatementTarget(miSetContentView, false);
        // get variable name
        String qualifiedClassName = ReflectionUtils.getCanonicalName(m_view.getDescription().getComponentClass());
        String baseName = NamesManager.getDefaultName(qualifiedClassName);
        String uniqueVariableName = m_editor.getUniqueVariableName(target.getPosition(), baseName, null);
        // get resource id for layout
        String layoutIdRef = AndroidUtils.getPackageFromManifest(m_view.getContext().getJavaProject().getProject())
                + ".R.id." + id;
        // add source 
        String source = qualifiedClassName + " " + uniqueVariableName + " = (" + qualifiedClassName + ")"
                + IDENTIFIER_FIND_VIEW_BY_ID + "(" + layoutIdRef + ");";
        return m_editor.addStatement(source, target);
    }

    /**
     * @return the {@link MethodInvocation} with required signature for given id or <code>null</code>
     *         if none.
     */
    private MethodInvocation getMethodInvocationWithId(MethodDeclaration methodDeclaration, final String signature,
            final String id) {
        final MethodInvocation[] result = new MethodInvocation[1];
        methodDeclaration.accept(new ASTVisitor() {
            @Override
            public void endVisit(MethodInvocation node) {
                // check for already found
                if (result[0] != null) {
                    return;
                }
                // proceed
                IMethodBinding binding = AstNodeUtils.getMethodBinding(node);
                if (binding != null) {
                    // compare signature
                    String methodSignature = AstNodeUtils.getMethodSignature(binding);
                    if (signature.equals(methodSignature)) {
                        Object object = node.arguments().get(0);
                        if (object instanceof QualifiedName) {
                            // compare reference
                            QualifiedName name = (QualifiedName) object;
                            if (id.equals(name.getName().getIdentifier())) {
                                result[0] = node;
                            }
                        }
                    }
                }
            }
        });
        return result[0];
    }

    /**
     * @return the method invocation with given signature and having <code>reference</code> as
     *         expression.
     */
    private MethodInvocation getMethodInvocationWithReference(ASTNode node, final String signature,
            final String reference) {
        final MethodInvocation[] result = new MethodInvocation[1];
        node.accept(new ASTVisitor() {
            @Override
            public void endVisit(MethodInvocation miNode) {
                // check for already found
                if (result[0] != null) {
                    return;
                }
                // proceed
                IMethodBinding binding = AstNodeUtils.getMethodBinding(miNode);
                if (binding != null) {
                    // compare signature
                    String methodSignature = AstNodeUtils.getMethodSignature(binding);
                    if (signature.equals(methodSignature)) {
                        Expression expression = miNode.getExpression();
                        if (expression instanceof SimpleName) {
                            SimpleName simpleName = (SimpleName) expression;
                            if (simpleName.getIdentifier().equals(reference)) {
                                result[0] = miNode;
                            }
                        }
                    }
                }
            }
        });
        return result[0];
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Utils
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Opens source of companion Java file at position that corresponds {@link MethodDeclaration}.
     */
    private void openMethodInEditor(MethodDeclaration method) throws Exception {
        prepareJavaFile();
        if (m_javaFile == null) {
            return;
        }
        IEditorPart javaEditor = IDE.openEditor(DesignerPlugin.getActivePage(), m_javaFile);
        if (javaEditor instanceof ITextEditor) {
            ((ITextEditor) javaEditor).selectAndReveal(method.getStartPosition(), 0);
        }
    }

    /**
     * @return the layout ID corresponding to this xml-objects hierarchy.
     */
    private String getThisLayoutId() {
        IFile xmlFile = m_object.getContext().getFile();
        return StringUtils.removeEndIgnoreCase(xmlFile.getName(), ".xml");
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // AST life-cycle
    //
    ////////////////////////////////////////////////////////////////////////////
    private AstEditor m_editor;
    private TypeDeclaration m_typeDeclaration;
    private long m_formFileModification;

    /**
     * Prepares {@link #m_editor} for {@link #m_javaFile}.
     */
    private boolean prepareAST() throws Exception {
        prepareJavaFile();
        if (m_javaFile == null) {
            return false;
        }
        m_editor = (AstEditor) m_view.getRoot().getArbitraryValue(KEY_COMPANION_EDITOR);
        long currentModification = m_javaFile.getModificationStamp();
        if (m_editor == null || currentModification != m_formFileModification) {
            m_formFileModification = currentModification;
            ICompilationUnit unit = JavaCore.createCompilationUnitFrom(m_javaFile);
            m_editor = new AstEditor(unit);
            m_view.getRoot().putArbitraryValue(KEY_COMPANION_EDITOR, m_editor);
        }
        m_typeDeclaration = DomGenerics.types(m_editor.getAstUnit()).get(0);
        return true;
    }

    /**
     * Saves changes performed in {@link #m_editor}.
     */
    private void saveAST() throws Exception {
        m_editor.saveChanges(false);
    }

    /**
     * Clears {@link #m_editor} after finishing AST operations.
     */
    private void clearAST() {
        m_editor = null;
        m_typeDeclaration = null;
    }

    /**
     * Searches for a companion java file.
     */
    private void prepareJavaFile() {
        if (m_javaFile == null) {
            IFile xmlFile = m_object.getContext().getFile();
            m_javaFile = AndroidPairResourceProvider.INSTANCE.getPair(xmlFile);
        }
    }
}