com.google.gdt.eclipse.designer.uibinder.model.util.EventHandlerProperty.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gdt.eclipse.designer.uibinder.model.util.EventHandlerProperty.java

Source

/*******************************************************************************
 * 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.gdt.eclipse.designer.uibinder.parser.UiBinderContext;

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.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.execution.ExecutionUtils;
import org.eclipse.wb.internal.core.utils.execution.RunnableEx;
import org.eclipse.wb.internal.core.utils.jdt.core.CodeUtils;
import org.eclipse.wb.internal.core.xml.editor.DesignContextMenuProvider;
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.IType;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TypeDeclaration;
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.util.List;

/**
 * {@link Property} for single UiBinder event.
 * 
 * @author scheglov_ke
 * @coverage GWT.UiBinder.model
 */
public final class EventHandlerProperty extends AbstractListenerProperty {
    private final EventHandlerDescription m_handler;

    ////////////////////////////////////////////////////////////////////////////
    //
    // Constructor
    //
    ////////////////////////////////////////////////////////////////////////////
    public EventHandlerProperty(XmlObjectInfo object, EventHandlerDescription handler) {
        super(object, handler.getMethodName(), EventHandlerPropertyEditor.INSTANCE);
        m_handler = handler;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Property
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    public boolean isModified() throws Exception {
        return getMethodDeclaration(false) != null;
    }

    @Override
    public void setValue(Object value) throws Exception {
        if (value == UNKNOWN_VALUE && isModified()) {
            if (MessageDialog.openConfirm(DesignerPlugin.getShell(), "Confirm",
                    "Do you really want delete handle '" + m_handler.getMethodName() + "'?")) {
                ExecutionUtils.run(m_object, new RunnableEx() {
                    public void run() throws Exception {
                        removeListener();
                    }
                });
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Access
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    protected void removeListener() throws Exception {
        prepareAST();
        try {
            MethodDeclaration method = getMethodDeclaration0();
            m_editor.removeBodyDeclaration(method);
        } finally {
            clearAST();
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // 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(DesignContextMenuProvider.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 {
        String name = m_handler.getMethodName();
        IAction[] actions = new IAction[2];
        // try to find existing stub method
        {
            MethodDeclaration method = getMethodDeclaration(false);
            if (method != null) {
                actions[0] = new ObjectInfoAction(m_object) {
                    @Override
                    protected void runEx() throws Exception {
                        openListener();
                    }
                };
                actions[0].setText(name + " -> " + method.getName().getIdentifier());
                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(name);
            actions[1].setImageDescriptor(EventsPropertyUtils.LISTENER_METHOD_IMAGE_DESCRIPTOR);
        }
        //
        return actions;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Handler
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    protected void openListener() throws Exception {
        final MethodDeclaration method = getMethodDeclaration(true);
        if (method != null) {
            ExecutionUtils.runAsync(new RunnableEx() {
                public void run() throws Exception {
                    openMethod_inEditor(method);
                }
            });
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // AST
    //
    ////////////////////////////////////////////////////////////////////////////
    private IFile m_javaFile;
    private AstEditor m_editor;
    private TypeDeclaration m_typeDeclaration;

    /**
     * Prepares {@link #m_editor} for {@link #m_javaFile}.
     */
    private void prepareAST() throws Exception {
        m_editor = ((UiBinderContext) m_object.getContext()).getFormEditor();
        m_javaFile = (IFile) m_editor.getModelUnit().getUnderlyingResource();
        m_typeDeclaration = m_editor.getPrimaryType();
    }

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

    ////////////////////////////////////////////////////////////////////////////
    //
    // Implementation
    //
    ////////////////////////////////////////////////////////////////////////////
    MethodDeclaration getMethodDeclaration(boolean addNew) throws Exception {
        if (canHaveHandler()) {
            prepareAST();
            try {
                MethodDeclaration method = getMethodDeclaration0();
                if (method != null || !addNew) {
                    return method;
                }
            } finally {
                clearAST();
            }
        }
        // new is not requested
        if (!addNew) {
            return null;
        }
        // ensure method
        prepareAST();
        try {
            return createMethodDeclaration0();
        } finally {
            clearAST();
            ExecutionUtils.refresh(m_object);
        }
    }

    /**
     * This method is used for optimization - parsing AST is fairly costly operation, so we try to
     * check quickly at first and do exact check only if needed. Alternative approach is remembering
     * {@link AstEditor} somewhere in context and track Java file changes. But for now we use simplest
     * solution.
     * 
     * @return <code>true</code> if it is possible that there is handler method, or <code>false</code>
     *         if there is definitely no such handler.
     */
    private boolean canHaveHandler() throws Exception {
        String name = NameSupport.getName(m_object);
        if (name == null) {
            return false;
        }
        // prepare Java source
        String formSource;
        {
            UiBinderContext context = (UiBinderContext) m_object.getContext();
            formSource = getTypeSource(context.getFormType());
        }
        // widget name
        if (!formSource.contains("\"" + name + "\"")) {
            return false;
        }
        // handler type name
        {
            String typeName = m_handler.getEventTypeName();
            String shortTypeName = CodeUtils.getShortClass(typeName);
            if (!formSource.contains(shortTypeName)) {
                return false;
            }
        }
        // may be there is handler
        return true;
    }

    /**
     * @return the source of {@link IType}. In theory first call of {@link IType#getSource()} should
     *         be successful, however when we change {@link IType} opened in Java editor there is some
     *         delay between change and time when source will be visible again (reconciling?).
     */
    private static String getTypeSource(IType type) throws Exception {
        String source = null;
        for (int i = 0; i < 5000; i++) {
            source = type.getSource();
            if (source != null) {
                break;
            }
            ExecutionUtils.waitEventLoop(5);
        }
        return source;
    }

    /**
     * @return the existing or new {@link MethodDeclaration} for this event.
     */
    private MethodDeclaration getMethodDeclaration0() throws Exception {
        String name = NameSupport.getName(m_object);
        // try to find existing
        for (MethodDeclaration methodDeclaration : m_typeDeclaration.getMethods()) {
            if (isEventHandler(methodDeclaration) && isObjectHandler(methodDeclaration, name)) {
                return methodDeclaration;
            }
        }
        // no method
        return null;
    }

    /**
     * @return <code>true</code> if {@link MethodDeclaration} has required event type parameter.
     */
    private boolean isEventHandler(MethodDeclaration methodDeclaration) {
        List<SingleVariableDeclaration> parameters = DomGenerics.parameters(methodDeclaration);
        if (parameters.size() == 1) {
            SingleVariableDeclaration parameter = parameters.get(0);
            String parameterTypeName = AstNodeUtils.getFullyQualifiedName(parameter, false);
            return m_handler.getEventTypeName().equals(parameterTypeName);
        }
        return false;
    }

    /**
     * @return <code>true</code> if {@link MethodDeclaration} has "@UiHandler" annotation with
     *         required name.
     */
    static boolean isObjectHandler(MethodDeclaration methodDeclaration, String name) {
        SingleMemberAnnotation handlerAnnotation = getHandlerAnnotation(methodDeclaration);
        if (handlerAnnotation != null && handlerAnnotation.getValue() instanceof StringLiteral) {
            StringLiteral handlerLiteral = (StringLiteral) handlerAnnotation.getValue();
            String handlerName = handlerLiteral.getLiteralValue();
            return name.equals(handlerName);
        }
        return false;
    }

    /**
     * @return the existing or new {@link MethodDeclaration} for this event.
     */
    private MethodDeclaration createMethodDeclaration0() throws Exception {
        String name = NameSupport.ensureName(m_object);
        String eventTypeName = m_handler.getEventTypeName();
        // prepare name of method
        String methodName;
        {
            String eventName = StringUtils.removeStart(m_handler.getMethodName(), "on");
            String baseName = "on" + StringUtils.capitalize(name) + eventName;
            methodName = m_editor.getUniqueMethodName(baseName);
        }
        // add method
        String uiHandlerAnnotation = "@com.google.gwt.uibinder.client.UiHandler(\"" + name + "\")";
        String header = "void " + methodName + "(" + eventTypeName + " event)";
        MethodDeclaration method = m_editor.addMethodDeclaration(ImmutableList.<String>of(uiHandlerAnnotation),
                header, ImmutableList.<String>of(), new BodyDeclarationTarget(m_typeDeclaration, false));
        // done
        return method;
    }

    /**
     * @return the "@UiHandler" {@link SingleMemberAnnotation}, may be <code>null</code>.
     */
    private static SingleMemberAnnotation getHandlerAnnotation(MethodDeclaration methodDeclaration) {
        for (IExtendedModifier modifier : DomGenerics.modifiers(methodDeclaration)) {
            if (modifier instanceof SingleMemberAnnotation) {
                SingleMemberAnnotation annotation = (SingleMemberAnnotation) modifier;
                if (AstNodeUtils.isSuccessorOf(annotation, "com.google.gwt.uibinder.client.UiHandler")) {
                    return annotation;
                }
            }
        }
        return null;
    }

    /**
     * Opens source of given Java {@link IFile} at position that corresponds {@link MethodDeclaration}
     * .
     */
    private void openMethod_inEditor(MethodDeclaration method) throws Exception {
        IEditorPart javaEditor = IDE.openEditor(DesignerPlugin.getActivePage(), m_javaFile);
        if (javaEditor instanceof ITextEditor) {
            ((ITextEditor) javaEditor).selectAndReveal(method.getStartPosition(), 0);
        }
    }
}