com.google.gdt.eclipse.designer.uibinder.parser.UiBinderContext.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gdt.eclipse.designer.uibinder.parser.UiBinderContext.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.parser;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gdt.eclipse.designer.GwtToolkitDescription;
import com.google.gdt.eclipse.designer.model.widgets.support.GwtState;
import com.google.gdt.eclipse.designer.parser.IClassLoaderValidator;
import com.google.gdt.eclipse.designer.uibinder.IExceptionConstants;
import com.google.gdt.eclipse.designer.util.ModuleDescription;
import com.google.gdt.eclipse.designer.util.Utils;

import org.eclipse.wb.internal.core.model.description.resource.IDescriptionVersionsProvider;
import org.eclipse.wb.internal.core.model.description.resource.IDescriptionVersionsProviderFactory;
import org.eclipse.wb.internal.core.model.property.Property;
import org.eclipse.wb.internal.core.model.util.ScriptUtils;
import org.eclipse.wb.internal.core.utils.IOUtils2;
import org.eclipse.wb.internal.core.utils.ast.AstEditor;
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.external.ExternalFactoriesHelper;
import org.eclipse.wb.internal.core.utils.jdt.core.CodeUtils;
import org.eclipse.wb.internal.core.utils.reflect.CompositeClassLoader;
import org.eclipse.wb.internal.core.utils.xml.DocumentElement;
import org.eclipse.wb.internal.core.xml.model.EditorContext;
import org.eclipse.wb.internal.core.xml.model.ILiveEditorContext;
import org.eclipse.wb.internal.core.xml.model.XmlObjectInfo;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;

import org.apache.commons.lang.StringUtils;

import java.beans.Beans;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@link EditorContext} for GWT UiBinder.
 * 
 * @author scheglov_ke
 * @coverage GWT.UiBinder.parser
 */
public class UiBinderContext extends EditorContext {
    private String m_binderClassName;
    private String m_binderResourceName;
    private String m_formClassName;
    private IType m_formType;
    private IFile m_formFile;
    private AstEditor m_formEditor;
    private long m_formFileModification;
    private ModuleDescription m_module;
    private GwtState m_state;

    ////////////////////////////////////////////////////////////////////////////
    //
    // Constructor
    //
    ////////////////////////////////////////////////////////////////////////////
    public UiBinderContext(IFile file, IDocument document) throws Exception {
        super(GwtToolkitDescription.INSTANCE, file, document);
        prepareBinderNames();
        configureDescriptionVersionsProviders();
        addVersions(ImmutableMap.of("isUiBinder", "true"));
    }

    public UiBinderContext(GwtState state, ClassLoader classLoader, IFile file, IDocument document)
            throws Exception {
        this(file, document);
        m_state = state;
        m_classLoader = classLoader;
        updateFromGWTState();
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Access
    //
    ////////////////////////////////////////////////////////////////////////////
    public String getBinderClassName() {
        return m_binderClassName;
    }

    /**
     * @return the {@link IType} of class with "Binder".
     */
    public IType getFormType() {
        return m_formType;
    }

    /**
     * @return the {@link AstEditor} for Java source.
     */
    public AstEditor getFormEditor() throws Exception {
        long currentModificationStamp = m_formFile.getModificationStamp();
        if (m_formEditor == null || currentModificationStamp != m_formFileModification) {
            m_formEditor = new AstEditor(m_formType.getCompilationUnit());
            m_formFileModification = currentModificationStamp;
        }
        return m_formEditor;
    }

    /**
     * Saves and clears {@link AstEditor} for Java source.
     */
    public void saveFormEditor() throws Exception {
        if (m_formEditor != null) {
            m_formEditor.saveChanges(true);
            m_formEditor = null;
        }
    }

    /**
     * @return the {@link GwtState} of this editor.
     */
    public GwtState getState() {
        return m_state;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Notification
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Notifies {@link AboutToParseProcessor} that this {@link UiBinderContext} is about to parse.
     */
    public void notifyAboutToParse() throws Exception {
        List<AboutToParseProcessor> processors = ExternalFactoriesHelper.getElementsInstances(
                AboutToParseProcessor.class, "com.google.gdt.eclipse.designer.UiBinder.aboutToParse", "processor");
        for (AboutToParseProcessor processor : processors) {
            processor.process(this);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Utils
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Ensures that UiBinder is configured for design time.
     */
    public void runDesignTime(RunnableEx runnable) throws Exception {
        String isKey = "gwt.UiBinder.isDesignTime " + m_binderClassName.replace('$', '.');
        String resKey = "gwt.UiBinder.designTime " + m_binderResourceName;
        boolean old_designTime = Beans.isDesignTime();
        try {
            Beans.setDesignTime(true);
            // mark "Binder" as design time
            System.setProperty(isKey, "true");
            // put current document content into System, to make it available to UiBinderGenerator
            {
                String content = getContent();
                content = removeWbpNameAttributes(content);
                System.setProperty(resKey, content);
            }
            // do run
            runnable.run();
        } finally {
            m_state.getDevModeBridge().invalidateRebind(m_binderClassName);
            Beans.setDesignTime(old_designTime);
            System.clearProperty(isKey);
            System.clearProperty(resKey);
            getBroadcastSupport().getListener(AfterRunDesignTime.class).invoke();
        }
    }

    /**
     * In tests we use "wbp:name" attribute to access widgets by such "internal" names, but UiBinder
     * does not like when it sees unknown attributes, so we should remove them.
     */
    private static String removeWbpNameAttributes(String content) {
        Matcher matcher = Pattern.compile("wbp:name=\"[^\"]*\"").matcher(content);
        // process each match
        int last = 0;
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            int start = matcher.start();
            int end = matcher.end();
            // not matched part
            sb.append(content.substring(last, start));
            last = end;
            // replace matched part with spaces
            for (int i = start; i < end; i++) {
                sb.append(' ');
            }
        }
        // append tail
        sb.append(content.substring(last));
        return sb.toString();
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Attributes
    //
    ////////////////////////////////////////////////////////////////////////////
    private final Map<String, Object> m_attributeValues = Maps.newHashMap();

    /**
     * Registers values for attributes, during rendering.
     */
    public void setAttributeValues(Map<String, Object> attributes) {
        m_attributeValues.clear();
        m_attributeValues.putAll(attributes);
    }

    /**
     * Registers value for attribute.
     */
    public void setAttributeValue(DocumentElement element, String name, Object value) {
        String key = UiBinderParser.getPath(element) + " " + name;
        m_attributeValues.put(key, value);
    }

    /**
     * @return all attributes of the element with the given path.
     */
    public Map<String, Object> getAttributeValues(String path) {
        Map<String, Object> attribute = Maps.newHashMap();
        String pathPrefix = path + " ";
        for (Entry<String, Object> entry : m_attributeValues.entrySet()) {
            if (entry.getKey().startsWith(pathPrefix)) {
                attribute.put(StringUtils.substringAfter(entry.getKey(), pathPrefix), entry.getValue());
            }
        }
        return attribute;
    }

    /**
     * @return the attribute value, remembered earlier during rendering. Value <code>null</code> is
     *         just value, not flag that there are no value. If no value for attribute, then
     *         {@link Property#UNKNOWN_VALUE} will be returned.
     */
    public Object getAttributeValue(DocumentElement element, String name) {
        String key = UiBinderParser.getPath(element) + " " + name;
        if (m_attributeValues.containsKey(key)) {
            return m_attributeValues.get(key);
        }
        return Property.UNKNOWN_VALUE;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // ClassLoader
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    protected void createClassLoader() throws Exception {
        if (m_classLoader == null) {
            m_module = Utils.getSingleModule(m_file);
            super.createClassLoader();
            executeClassLoaderInitializationScripts();
        }
    }

    /**
     * Allows users to perform {@link ClassLoader} initialization actions, to prepare environment.
     */
    private void executeClassLoaderInitializationScripts() {
        IResource resource = m_file;
        while (true) {
            if (resource.getParent() instanceof IFolder) {
                IFolder folder = (IFolder) resource.getParent();
                resource = folder;
                // use script in current folder
                final IFile scriptFile = folder.getFile("ClassLoaderInitializer.gwtd.mvel");
                if (scriptFile.exists()) {
                    ExecutionUtils.runLog(new RunnableEx() {
                        public void run() throws Exception {
                            String script = IOUtils2.readString(scriptFile);
                            ScriptUtils.evaluate(m_classLoader, script);
                        }
                    });
                }
            } else {
                break;
            }
        }
    }

    @Override
    protected void addParentClassLoaders(CompositeClassLoader parentClassLoader) throws Exception {
        super.addParentClassLoaders(parentClassLoader);
        // add ClassLoader to use only for loading resources
        {
            ClassLoader resourcesClassLoader = m_module.getClassLoader();
            parentClassLoader.add(resourcesClassLoader, ImmutableList.<String>of(), null);
        }
    }

    @Override
    protected ClassLoader createProjectClassLoader(CompositeClassLoader parentClassLoader) throws Exception {
        createGWTState(parentClassLoader);
        {
            // process ClassLoader validators
            List<IClassLoaderValidator> validators = ExternalFactoriesHelper.getElementsInstances(
                    IClassLoaderValidator.class, "com.google.gdt.eclipse.designer.classLoaderValidators",
                    "validator");
            for (IClassLoaderValidator validator : validators) {
                validator.validate(m_javaProject, m_state);
            }
        }
        return m_state.getClassLoader();
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Utils
    //
    ////////////////////////////////////////////////////////////////////////////
    private void prepareBinderNames() throws Exception {
        // template
        IPath templatePath = m_file.getFullPath();
        String templatePathString = templatePath.toPortableString();
        // package
        IPackageFragment uiPackage;
        {
            if (!(m_file.getParent() instanceof IFolder)) {
                throw new DesignerException(IExceptionConstants.NO_FORM_PACKAGE, templatePathString);
            }
            // prepare package
            IFolder uiFolder = (IFolder) m_file.getParent();
            IJavaElement uiElement = JavaCore.create(uiFolder);
            if (!(uiElement instanceof IPackageFragment)) {
                throw new DesignerException(IExceptionConstants.NO_FORM_PACKAGE, templatePathString);
            }
            uiPackage = (IPackageFragment) uiElement;
            // has client package
            if (!Utils.isModuleSourcePackage(uiPackage)) {
                throw new DesignerException(IExceptionConstants.NOT_CLIENT_PACKAGE, templatePathString);
            }
        }
        // binder resource
        m_binderResourceName = uiPackage.getElementName().replace('.', '/') + "/" + m_file.getName();
        // try current package
        {
            String formName = StringUtils.removeEnd(m_file.getName(), ".ui.xml");
            m_formClassName = uiPackage.getElementName() + "." + formName;
            m_formType = m_javaProject.findType(m_formClassName);
            if (m_formType != null) {
                m_formFile = (IFile) m_formType.getCompilationUnit().getUnderlyingResource();
                prepareBinderClass();
                if (m_binderClassName != null) {
                    return;
                }
            }
        }
        // try @UiTemplate
        IType uiTemplateType = m_javaProject.findType("com.google.gwt.uibinder.client.UiTemplate");
        List<IJavaElement> references = CodeUtils.searchReferences(uiTemplateType);
        for (IJavaElement reference : references) {
            if (reference instanceof IAnnotation) {
                IAnnotation annotation = (IAnnotation) reference;
                IMemberValuePair[] valuePairs = annotation.getMemberValuePairs();
                if (valuePairs.length == 1 && valuePairs[0].getValue() instanceof String) {
                    String templateName = (String) valuePairs[0].getValue();
                    // prepare ICompilationUnit
                    ICompilationUnit compilationUnit = (ICompilationUnit) annotation
                            .getAncestor(IJavaElement.COMPILATION_UNIT);
                    // prepare qualified template name
                    templateName = StringUtils.removeEnd(templateName, ".ui.xml");
                    if (templateName.contains(".")) {
                        templateName = templateName.replace('.', '/');
                    } else {
                        String packageName = compilationUnit.getPackageDeclarations()[0].getElementName();
                        templateName = packageName.replace('.', '/') + "/" + templateName;
                    }
                    templateName += ".ui.xml";
                    // if found, initialize "form" element
                    if (m_binderResourceName.equals(templateName)) {
                        m_formType = (IType) annotation.getParent().getParent();
                        m_formClassName = m_formType.getFullyQualifiedName();
                        m_formFile = (IFile) m_formType.getCompilationUnit().getUnderlyingResource();
                        prepareBinderClass();
                        if (m_binderClassName != null) {
                            return;
                        }
                    }
                }
            }
        }
        // no Java form
        throw new DesignerException(IExceptionConstants.NO_FORM_TYPE, m_binderResourceName);
    }

    /**
     * Attempts for put into {@link #m_binderClassName} the "UiBinder" inner {@link IType} from
     * {@link #m_formType}.
     */
    private void prepareBinderClass() throws Exception {
        for (IType innerType : m_formType.getTypes()) {
            if (CodeUtils.isSuccessorOf(innerType, "com.google.gwt.uibinder.client.UiBinder")) {
                IType binderType = innerType;
                assertWidgetBased(binderType);
                m_binderClassName = m_formClassName + "$" + innerType.getElementName();
            }
        }
    }

    /**
     * Asserts that given <code>binderType</code> generates <code>Widget</code> when rendered.
     */
    private void assertWidgetBased(IType binderType) throws Exception {
        String[] superSignatures = binderType.getSuperInterfaceTypeSignatures();
        for (String superSignature : superSignatures) {
            int binderIndex = superSignature.indexOf("UiBinder<");
            if (binderIndex != -1) {
                int objectTypeBegin = binderIndex + "UiBinder<".length();
                int objectTypeEnd = superSignature.indexOf(";", binderIndex);
                String objectTypeSignature = superSignature.substring(objectTypeBegin, objectTypeEnd + 1);
                String objectTypeName = CodeUtils.getResolvedTypeName(m_formType, objectTypeSignature);
                if (objectTypeName != null) {
                    IType objectType = m_javaProject.findType(objectTypeName);
                    if (!CodeUtils.isSuccessorOf(objectType, "com.google.gwt.user.client.ui.Widget")) {
                        throw new DesignerException(IExceptionConstants.ONLY_WIDGET_BASED, m_formClassName,
                                objectTypeName);
                    }
                }
            }
        }
    }

    private void createGWTState(CompositeClassLoader parentClassLoader) throws Exception {
        if (m_sharedUse && m_shared_GWTState != null) {
            m_state = m_shared_GWTState;
        } else {
            // initialize GWTState
            m_state = new GwtState(parentClassLoader, m_module);
            m_state.initialize();
            // remember shared state
            if (m_sharedUse) {
                m_state.setShared(true);
                m_shared_GWTState = m_state;
            }
        }
        updateFromGWTState();
    }

    /**
     * Updates this {@link UiBinderContext} from {@link #m_state}.
     */
    private void updateFromGWTState() {
        addVersions(ImmutableMap.of("gwt_isStrictMode", m_state.isStrictMode()));
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Sharing GWTState
    //
    ////////////////////////////////////////////////////////////////////////////
    private static boolean m_sharedUse = false;
    private static GwtState m_shared_GWTState;

    /**
     * Specifies if next parsing should use shared {@link GwtState}.
     */
    public static void setUseSharedGWTState(boolean use) {
        m_sharedUse = use;
    }

    /**
     * Disposes existing shared {@link GwtState}.
     */
    public static void disposeSharedGWTState() {
        if (m_shared_GWTState != null) {
            m_shared_GWTState.setShared(false);
            m_shared_GWTState.dispose();
            m_shared_GWTState = null;
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Live support
    //
    ////////////////////////////////////////////////////////////////////////////
    @Override
    public ILiveEditorContext getLiveContext() {
        return m_liveEditorContext;
    }

    private final ILiveEditorContext m_liveEditorContext = new ILiveEditorContext() {
        public XmlObjectInfo parse(String[] sourceLines) throws Exception {
            // prepare document
            String source = StringUtils.join(sourceLines, "\n");
            IDocument document = new Document(source);
            // prepare context
            UiBinderContext context = new UiBinderContext(m_state, m_classLoader, m_file, document);
            context.setLiveComponent(true);
            // do parse
            UiBinderParser parser = new UiBinderParser(context);
            return parser.parse();
        }

        public void dispose() throws Exception {
        }
    };

    ////////////////////////////////////////////////////////////////////////////
    //
    // IDescriptionVersionsProvider
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Installs {@link IDescriptionVersionsProvider}'s.
     */
    private void configureDescriptionVersionsProviders() throws Exception {
        List<IDescriptionVersionsProviderFactory> factories = ExternalFactoriesHelper.getElementsInstances(
                IDescriptionVersionsProviderFactory.class,
                "org.eclipse.wb.core.descriptionVersionsProviderFactories", "factory");
        for (IDescriptionVersionsProviderFactory factory : factories) {
            // versions
            addVersions(factory.getVersions(m_javaProject, m_classLoader));
            // version providers
            {
                IDescriptionVersionsProvider provider = factory.getProvider(m_javaProject, m_classLoader);
                if (provider != null) {
                    addDescriptionVersionsProvider(provider);
                }
            }
        }
    }
}