/*******************************************************************************
* 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);
}
}
}
}
}