/*******************************************************************************
* 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.builders;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gdt.eclipse.designer.Activator;
import com.google.gdt.eclipse.designer.builders.participant.MyCompilationParticipant;
import com.google.gdt.eclipse.designer.common.Constants;
import com.google.gdt.eclipse.designer.refactoring.GwtRefactoringUtils;
import com.google.gdt.eclipse.designer.util.Utils;
import org.eclipse.wb.internal.core.utils.ast.AstNodeUtils;
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.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.ui.actions.OverrideMethodsAction;
import org.eclipse.jface.text.Document;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.apache.commons.lang.StringUtils;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* {@link IncrementalProjectBuilder} for building <code>RemoteSErvice</code> Async types.
*
* @author scheglov_ke
* @coverage gwt.builder
*/
public class GwtBuilder extends IncrementalProjectBuilder {
////////////////////////////////////////////////////////////////////////////
//
// Build time fields
//
////////////////////////////////////////////////////////////////////////////
private IResourceDelta m_delta;
private List<IFile> m_serviceFiles;
////////////////////////////////////////////////////////////////////////////
//
// Build
//
////////////////////////////////////////////////////////////////////////////
@Override
@SuppressWarnings("rawtypes")
protected IProject[] build(int kind, Map args, final IProgressMonitor _monitor)
throws CoreException {
ExecutionUtils.runLog(new RunnableEx() {
public void run() throws Exception {
buildEx(_monitor);
}
});
_monitor.done();
return null;
}
private void buildEx(final IProgressMonitor _monitor) throws Exception {
IProgressMonitor monitor = new SubProgressMonitor(_monitor, 3);
// prepare state
{
IProject project = getProject();
m_delta = getDelta(project);
}
// respond to delta
if (m_delta != null) {
if (Activator.getStore().getBoolean(Constants.P_BUILDER_GENERATE_ASYNC)) {
monitor.beginTask("Building Remote Service's...", 2);
//
monitor.subTask("Looking for RemoteService Files");
findRemoteServiceFiles();
monitor.worked(1);
//
monitor.subTask("Updating RemoteService's Async and Impl parts");
updateRemoteServices();
monitor.worked(1);
}
{
monitor.beginTask("Checking *.GWT.XML modifications...", 1);
checkModuleFileModification();
monitor.worked(1);
}
}
}
////////////////////////////////////////////////////////////////////////////
//
// Async interfaces generation
//
////////////////////////////////////////////////////////////////////////////
/**
* Prepares {@link IFile}'s with <code>RemoveService</code> for current build type (incremental or
* full).
*/
private void findRemoteServiceFiles() throws CoreException {
m_serviceFiles = Lists.newArrayList();
// visit delta
IResourceDeltaVisitor visitor = new IResourceDeltaVisitor() {
public boolean visit(IResourceDelta delta) throws CoreException {
IResource resource = delta.getResource();
switch (resource.getType()) {
case IResource.PROJECT :
IProject project = (IProject) resource;
// we are interesting only in GWT projects
return Utils.isGWTProject(project);
case IResource.FILE :
if (Utils.isRemoteService(resource)) {
m_serviceFiles.add((IFile) resource);
}
}
return true;
}
};
m_delta.accept(visitor);
}
/**
* Updates Async and Impl parts for each modified <code>RemoteService</code>.
*/
private void updateRemoteServices() throws Exception {
for (IFile serviceFile : m_serviceFiles) {
ICompilationUnit serviceUnit = (ICompilationUnit) JavaCore.create(serviceFile);
IPackageFragment servicePackage = (IPackageFragment) serviceUnit.getParent();
//
generateAsync(servicePackage, serviceUnit);
updateImpl(servicePackage, serviceUnit);
}
}
/**
* Generates Async type for given <code>RemoteService</code>.
*/
private void generateAsync(IPackageFragment servicePackage, ICompilationUnit serviceUnit)
throws Exception {
IJavaProject javaProject = serviceUnit.getJavaProject();
// parse service unit
CompilationUnit serviceRoot = Utils.parseUnit(serviceUnit);
// prepare AST and start modifications recording
AST ast = serviceRoot.getAST();
serviceRoot.recordModifications();
// modify imports (-com.google.gwt.*, -*Exception, +AsyncCallback)
{
List<ImportDeclaration> imports = DomGenerics.imports(serviceRoot);
// remove useless imports
for (Iterator<ImportDeclaration> I = imports.iterator(); I.hasNext();) {
ImportDeclaration importDeclaration = I.next();
String importName = importDeclaration.getName().getFullyQualifiedName();
if (importName.startsWith("com.google.gwt.user.client.rpc.")
|| importName.equals("com.google.gwt.core.client.GWT")
|| importName.endsWith("Exception")) {
I.remove();
}
}
}
// add Async to the name
TypeDeclaration serviceType = (TypeDeclaration) serviceRoot.types().get(0);
String remoteServiceAsyncName = serviceType.getName().getIdentifier() + "Async";
serviceType.setName(serviceRoot.getAST().newSimpleName(remoteServiceAsyncName));
// update interfaces
updateInterfacesOfAsync(javaProject, serviceRoot, serviceType);
// change methods, fields and inner classes
{
List<BodyDeclaration> bodyDeclarations = DomGenerics.bodyDeclarations(serviceType);
for (Iterator<BodyDeclaration> I = bodyDeclarations.iterator(); I.hasNext();) {
BodyDeclaration bodyDeclaration = I.next();
if (bodyDeclaration instanceof MethodDeclaration) {
MethodDeclaration methodDeclaration = (MethodDeclaration) bodyDeclaration;
// make return type void
Type returnType;
{
returnType = methodDeclaration.getReturnType2();
methodDeclaration.setReturnType2(ast.newPrimitiveType(PrimitiveType.VOID));
}
// process JavaDoc
{
Javadoc javadoc = methodDeclaration.getJavadoc();
if (javadoc != null) {
List<TagElement> tags = DomGenerics.tags(javadoc);
for (Iterator<TagElement> tagIter = tags.iterator(); tagIter.hasNext();) {
TagElement tag = tagIter.next();
if ("@gwt.typeArgs".equals(tag.getTagName())) {
tagIter.remove();
} else if ("@return".equals(tag.getTagName())) {
if (!tag.fragments().isEmpty()) {
tag.setTagName("@param callback the callback to return");
} else {
tagIter.remove();
}
} else if ("@wbp.gwt.Request".equals(tag.getTagName())) {
tagIter.remove();
addImport(serviceRoot, "com.google.gwt.http.client.Request");
methodDeclaration.setReturnType2(ast.newSimpleType(ast.newName("Request")));
}
}
// remove empty JavaDoc
if (tags.isEmpty()) {
methodDeclaration.setJavadoc(null);
}
}
}
// add AsyncCallback parameter
{
addImport(serviceRoot, "com.google.gwt.user.client.rpc.AsyncCallback");
// prepare "callback" type
Type callbackType;
{
callbackType = ast.newSimpleType(ast.newName("AsyncCallback"));
Type objectReturnType = getObjectType(returnType);
ParameterizedType parameterizedType = ast.newParameterizedType(callbackType);
DomGenerics.typeArguments(parameterizedType).add(objectReturnType);
callbackType = parameterizedType;
}
// prepare "callback" parameter
SingleVariableDeclaration asyncCallback = ast.newSingleVariableDeclaration();
asyncCallback.setType(callbackType);
asyncCallback.setName(ast.newSimpleName("callback"));
// add "callback" parameter
DomGenerics.parameters(methodDeclaration).add(asyncCallback);
}
// remove throws
methodDeclaration.thrownExceptions().clear();
} else if (bodyDeclaration instanceof FieldDeclaration
|| bodyDeclaration instanceof TypeDeclaration) {
// remove the fields and inner classes
I.remove();
}
}
}
// apply modifications to prepare new source code
String newSource;
{
String source = serviceUnit.getBuffer().getContents();
Document document = new Document(source);
// prepare text edits
MultiTextEdit edits =
(MultiTextEdit) serviceRoot.rewrite(document, javaProject.getOptions(true));
removeAnnotations(serviceType, source, edits);
// prepare new source code
edits.apply(document);
newSource = document.get();
}
// update compilation unit
{
ICompilationUnit unit =
servicePackage.createCompilationUnit(
remoteServiceAsyncName + ".java",
newSource,
true,
null);
unit.getBuffer().save(null, true);
}
}
private static void addImport(CompilationUnit compilationUnit, String qualifiedName) {
AST ast = compilationUnit.getAST();
List<ImportDeclaration> imports = DomGenerics.imports(compilationUnit);
// check for existing ImportDeclaration
for (ImportDeclaration importDeclaration : imports) {
if (importDeclaration.getName().toString().equals(qualifiedName)) {
return;
}
}
// add new ImportDeclaration
ImportDeclaration importDeclaration = ast.newImportDeclaration();
importDeclaration.setName(ast.newName(qualifiedName));
imports.add(importDeclaration);
}
/**
* Keeps Async interfaces for services and remove other interfaces.
*/
private static void updateInterfacesOfAsync(IJavaProject javaProject,
CompilationUnit serviceUnit,
TypeDeclaration serviceType) throws Exception {
String serviceName = AstNodeUtils.getFullyQualifiedName(serviceType, false);
AST ast = serviceType.getAST();
for (Iterator<?> I = serviceType.superInterfaceTypes().iterator(); I.hasNext();) {
Type type = (Type) I.next();
ITypeBinding typeBinding = AstNodeUtils.getTypeBinding(type);
if (AstNodeUtils.isSuccessorOf(typeBinding, Constants.CLASS_REMOTE_SERVICE)) {
String superServiceName = AstNodeUtils.getFullyQualifiedName(typeBinding, false);
String superAsyncName = superServiceName + "Async";
if (javaProject.findType(superAsyncName) != null) {
if (type instanceof SimpleType) {
{
SimpleType simpleType = (SimpleType) type;
String superAsyncNameSimple = CodeUtils.getShortClass(superAsyncName);
simpleType.setName(ast.newSimpleName(superAsyncNameSimple));
}
if (!CodeUtils.isSamePackage(serviceName, superAsyncName)) {
addImport(serviceUnit, superAsyncName);
}
continue;
}
}
}
I.remove();
}
}
/**
* Removes annotations of service {@link TypeDeclaration}.
*/
private static void removeAnnotations(TypeDeclaration serviceType,
String source,
MultiTextEdit edits) {
int typePos = serviceType.getStartPosition();
{
Javadoc javadoc = serviceType.getJavadoc();
if (javadoc != null) {
typePos = javadoc.getStartPosition() + javadoc.getLength();
while (Character.isWhitespace(source.charAt(typePos))) {
typePos++;
}
}
}
int pureTypePos =
StringUtils.indexOfAny(
source,
new String[]{"public ", "protected ", "class ", "interface "});
if (pureTypePos != -1 && pureTypePos != typePos) {
edits.addChild(new DeleteEdit(typePos, pureTypePos - typePos));
}
}
private void updateImpl(IPackageFragment servicePackage, ICompilationUnit serviceUnit)
throws Exception {
// find single Impl type
ICompilationUnit implUnit;
{
IType implType = GwtRefactoringUtils.getServiceImplType(serviceUnit.findPrimaryType(), null);
if (implType == null) {
return;
}
implUnit = implType.getCompilationUnit();
}
// prepare AST unit and type
CompilationUnit implRoot = Utils.parseUnit(implUnit);
TypeDeclaration implType = (TypeDeclaration) implRoot.types().get(0);
// use standard JDT operation
final IWorkspaceRunnable workspaceRunnable =
OverrideMethodsAction.createRunnable(implRoot, implType.resolveBinding(), null, -1, false);
// execute in UI because operation works with widgets during apply
ExecutionUtils.runLogUI(new RunnableEx() {
public void run() throws Exception {
workspaceRunnable.run(null);
}
});
implUnit.save(null, true);
implUnit.getBuffer().save(null, true);
}
/**
* @return the {@link Type} that extends {@link Object}, even if given is primitive.
*/
private static Type getObjectType(Type type) {
if (type.isPrimitiveType()) {
String identifier = ((PrimitiveType) type).getPrimitiveTypeCode().toString();
if (identifier.equals("int")) {
identifier = "Integer";
} else {
identifier = StringUtils.capitalize(identifier);
}
SimpleName typeName = type.getAST().newSimpleName(identifier);
return type.getAST().newSimpleType(typeName);
} else {
return type;
}
}
////////////////////////////////////////////////////////////////////////////
//
// Recompile on *.gwt.xml modification
//
////////////////////////////////////////////////////////////////////////////
/**
* Rebuilds GWT project if any of the *.gwt.xml file was modified. We need this to force out
* {@link MyCompilationParticipant} to validate again source, because *.gwt.xml may be
* included/removed some other GWT module and some types become accessible/inaccessible in
* "source" classpath.
*/
private void checkModuleFileModification() throws CoreException {
// optimization for tests
if (!MyCompilationParticipant.ENABLED) {
return;
}
//
// prepare projects with changed *.gwt.xml files
final Set<IProject> projectsToBuild = Sets.newHashSet();
IResourceDeltaVisitor visitor = new IResourceDeltaVisitor() {
public boolean visit(IResourceDelta delta) throws CoreException {
IResource resource = delta.getResource();
switch (resource.getType()) {
case IResource.PROJECT :
// process only GWT projects
IProject project = (IProject) resource;
return Utils.isGWTProject(project);
case IResource.FOLDER : {
// process only resources in "src" folders
IJavaProject javaProject = JavaCore.create(getProject());
return javaProject.isOnClasspath(resource);
}
case IResource.FILE :
if (Utils.getExactModule(resource) != null) {
projectsToBuild.add(resource.getProject());
}
}
return true;
}
};
m_delta.accept(visitor);
// run "Clean" job for found projects
if (!projectsToBuild.isEmpty()) {
WorkspaceJob cleanJob = new WorkspaceJob("GWT project rebuild") {
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
for (IProject project : projectsToBuild) {
project.build(IncrementalProjectBuilder.CLEAN_BUILD, new NullProgressMonitor());
}
return Status.OK_STATUS;
}
};
cleanJob.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
cleanJob.setUser(true);
cleanJob.schedule();
}
}
}