/*
* Copyright 2011 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.devtools.j2objc.translate;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.Sets;
import com.google.devtools.j2objc.ast.Annotation;
import com.google.devtools.j2objc.ast.Block;
import com.google.devtools.j2objc.ast.BooleanLiteral;
import com.google.devtools.j2objc.ast.CompilationUnit;
import com.google.devtools.j2objc.ast.ConditionalExpression;
import com.google.devtools.j2objc.ast.EmptyStatement;
import com.google.devtools.j2objc.ast.Expression;
import com.google.devtools.j2objc.ast.IfStatement;
import com.google.devtools.j2objc.ast.MethodDeclaration;
import com.google.devtools.j2objc.ast.MethodInvocation;
import com.google.devtools.j2objc.ast.SingleMemberAnnotation;
import com.google.devtools.j2objc.ast.Statement;
import com.google.devtools.j2objc.ast.StringLiteral;
import com.google.devtools.j2objc.ast.TreeUtil;
import com.google.devtools.j2objc.ast.UnitTreeVisitor;
import com.google.devtools.j2objc.types.ExecutablePair;
import com.google.devtools.j2objc.util.ElementUtil;
import com.google.devtools.j2objc.util.TypeUtil;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
/**
* Updates the Java AST to remove code bound by GWT.isClient and
* GWT.isScript tests, and translate GWT.create(Class) invocations
* into Class.newInstance().
*
* @author Tom Ball
*/
public class GwtConverter extends UnitTreeVisitor {
private static final String GWT_CLASS = "com.google.gwt.core.client.GWT";
/**
* The list of APIs that can be translated by J2ObjC, but not by GWT. These
* strings were found by scanning common sources like the Google Guava
* library.
*
* Note: this list is neither exhaustive, nor guaranteed to match a
* specified GwtIncompatible value, since it takes unchecked strings.
*/
private static final Set<String> compatibleAPIs = Sets.newHashSet(
"Array.newArray(Class, int)", "Array.newInstance(Class, int)", "Class.isInstance",
"Class.isAssignableFrom", "CopyOnWriteArraySet", "InputStream", "java.io.BufferedReader",
"java.io.Closeable,java.io.Flushable", "java.io.Writer", "java.lang.reflect",
"java.lang.String.getBytes()", "java.lang.System#getProperty", "java.util.ArrayDeque",
"java.util.BitSet", "java.util.Locale", "java.util.regex", "java.util.regex.Pattern",
"java.util.String(byte[], Charset)", "MapMakerInternalMap", "NavigableMap", "NavigableAsMap",
"NavigableSet", "Non-UTF-8 Charset", "OutputStream", "proto", "protos", "Readable",
"Reader", "Reader,InputStream", "reflection", "regular expressions", "String.format()",
"uses NavigableMap", "Writer", "Writer,OutputStream");
public GwtConverter(CompilationUnit unit) {
super(unit);
}
@Override
public boolean visit(ConditionalExpression node) {
if (isGwtTest(node.getExpression())) {
// Replace this node with the else expression, removing this conditional.
node.replaceWith(TreeUtil.remove(node.getElseExpression()));
}
node.getElseExpression().accept(this);
return false;
}
@Override
public boolean visit(IfStatement node) {
if (isGwtTest(node.getExpression())) {
if (node.getElseStatement() != null) {
// Replace this node with the else statement.
Statement replacement = TreeUtil.remove(node.getElseStatement());
node.replaceWith(replacement);
replacement.accept(this);
} else {
// No else statement, so remove this if statement or replace it
// with an empty statement.
if (node.getParent() instanceof Block) {
node.remove();
} else {
node.replaceWith(new EmptyStatement());
}
}
return false;
}
return true;
}
@Override
public boolean visit(MethodDeclaration node) {
if (options.stripGwtIncompatibleMethods() && isIncompatible(node.getAnnotations())) {
// Remove method from its declaring class.
node.remove();
return false;
}
return true;
}
@Override
public boolean visit(MethodInvocation node) {
ExecutableElement method = node.getExecutableElement();
List<Expression> args = node.getArguments();
if (ElementUtil.getName(method).equals("create")
&& ElementUtil.getQualifiedName(ElementUtil.getDeclaringClass(method)).equals(GWT_CLASS)
&& args.size() == 1) {
// Convert GWT.create(Foo.class) to Foo.class.newInstance().
ExecutableElement newMethod = ElementUtil.findMethod(typeUtil.getJavaClass(), "newInstance");
Expression clazz = args.remove(0);
node.setExpression(clazz);
node.setExecutablePair(new ExecutablePair(newMethod));
} else if (isGwtTest(node)) {
node.replaceWith(new BooleanLiteral(false, typeUtil));
}
return true;
}
/**
* Returns true if expression is a method invocation, and that method refers to GWT.isClient() or
* GWT.isScript().
*
* NOTE: this method only checks for GWT.isClient() method invocations, not any other boolean
* expression that might contain them. For example, !GWT.isClient() won't detect the method, since
* that expression won't have a method element. The isGwtTest() call in visit(MethodInvocation)
* warns when code like this is translated.
*/
private boolean isGwtTest(Expression node) {
ExecutableElement method = TreeUtil.getExecutableElement(node);
if (method != null) {
if (ElementUtil.getQualifiedName(ElementUtil.getDeclaringClass(method)).equals(GWT_CLASS)) {
String name = ElementUtil.getName(method);
return name.equals("isClient") || name.equals("isScript");
}
}
return false;
}
private boolean isIncompatible(List<Annotation> annotations) {
// Annotation mirrors don't have the annotation's package.
for (Annotation annotationNode : annotations) {
String annotationName =
TypeUtil.getQualifiedName(annotationNode.getAnnotationMirror().getAnnotationType());
if (!annotationName.equals(GwtIncompatible.class.getName())) {
continue;
}
if (annotationNode.isSingleMemberAnnotation()) {
Expression value = ((SingleMemberAnnotation) annotationNode).getValue();
if (value instanceof StringLiteral) {
if (compatibleAPIs.contains(((StringLiteral) value).getLiteralValue())) {
// Pretend incompatible annotation isn't present, since what it's
// flagging is J2ObjC-compatible.
return false;
}
}
}
return true;
}
return false;
}
}