blob: 6122dfc73e7fa066b6af83b4a3abe2136c1fc813 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* 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.web.bindery.autobean.gwt.rebind.model;
import com.google.web.bindery.autobean.shared.AutoBean;
import com.google.web.bindery.autobean.shared.AutoBeanFactory;
import com.google.web.bindery.autobean.shared.AutoBeanFactory.Category;
import com.google.web.bindery.autobean.shared.AutoBeanFactory.NoWrap;
import com.google.web.bindery.autobean.shared.impl.EnumMap.ExtraEnums;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JEnumConstant;
import com.google.gwt.core.ext.typeinfo.JEnumType;
import com.google.gwt.core.ext.typeinfo.JGenericType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.editor.rebind.model.ModelUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
*/
public class AutoBeanFactoryModel {
private static final JType[] EMPTY_JTYPE = new JType[0];
private final JGenericType autoBeanInterface;
private final JClassType autoBeanFactoryInterface;
private final Map<JEnumConstant, String> allEnumConstants = new LinkedHashMap<JEnumConstant, String>();
private final List<JClassType> categoryTypes;
private final List<JClassType> noWrapTypes;
private final TreeLogger logger;
private final List<AutoBeanFactoryMethod> methods = new ArrayList<AutoBeanFactoryMethod>();
private final List<JMethod> objectMethods;
private final TypeOracle oracle;
private final Map<JClassType, AutoBeanType> peers = new LinkedHashMap<JClassType, AutoBeanType>();
private boolean poisoned;
/**
* Accumulates bean types that are reachable through the type graph.
*/
private Set<JClassType> toCalculate = new LinkedHashSet<JClassType>();
public AutoBeanFactoryModel(TreeLogger logger, JClassType factoryType)
throws UnableToCompleteException {
this.logger = logger;
oracle = factoryType.getOracle();
autoBeanInterface = oracle.findType(AutoBean.class.getCanonicalName()).isGenericType();
autoBeanFactoryInterface = oracle.findType(
AutoBeanFactory.class.getCanonicalName()).isInterface();
/*
* We want to allow the user to override some of the useful Object methods,
* so we'll extract them here.
*/
JClassType objectType = oracle.getJavaLangObject();
objectMethods = Arrays.asList(
objectType.findMethod("equals", new JType[] {objectType}),
objectType.findMethod("hashCode", EMPTY_JTYPE),
objectType.findMethod("toString", EMPTY_JTYPE));
// Process annotations
{
Category categoryAnnotation = factoryType.getAnnotation(Category.class);
if (categoryAnnotation != null) {
categoryTypes = new ArrayList<JClassType>(
categoryAnnotation.value().length);
processClassArrayAnnotation(categoryAnnotation.value(), categoryTypes);
} else {
categoryTypes = null;
}
noWrapTypes = new ArrayList<JClassType>();
noWrapTypes.add(oracle.findType(AutoBean.class.getCanonicalName()));
NoWrap noWrapAnnotation = factoryType.getAnnotation(NoWrap.class);
if (noWrapAnnotation != null) {
processClassArrayAnnotation(noWrapAnnotation.value(), noWrapTypes);
}
ExtraEnums extraEnumsAnnotation = factoryType.getAnnotation(ExtraEnums.class);
if (extraEnumsAnnotation != null) {
for (Class<?> clazz : extraEnumsAnnotation.value()) {
JEnumType asEnum = oracle.findType(clazz.getCanonicalName()).isEnum();
assert asEnum != null;
for (JEnumConstant value : asEnum.getEnumConstants()) {
allEnumConstants.put(value, AutoBeanMethod.getEnumName(value));
}
}
}
}
for (JMethod method : factoryType.getOverridableMethods()) {
if (method.getEnclosingType().equals(autoBeanFactoryInterface)) {
// Ignore methods in AutoBeanFactory
continue;
}
JClassType returnType = method.getReturnType().isInterface();
if (returnType == null) {
poison("The return type of method %s is a primitive type",
method.getName());
continue;
}
// AutoBean<FooIntf> blah() --> beanType = FooIntf
JClassType beanType = ModelUtils.findParameterizationOf(
autoBeanInterface, returnType)[0];
if (beanType.isInterface() == null) {
poison("The %s parameterization is not an interface",
beanType.getQualifiedSourceName());
continue;
}
// AutoBean<FooIntf> blah(FooIntfSub foo) --> toWrap = FooIntfSub
JClassType toWrap;
if (method.getParameters().length == 0) {
toWrap = null;
} else if (method.getParameters().length == 1) {
toWrap = method.getParameters()[0].getType().isClassOrInterface();
if (!beanType.isAssignableFrom(toWrap)) {
poison(
"The %s parameterization %s is not assignable from the delegate"
+ " type %s", autoBeanInterface.getSimpleSourceName(),
toWrap.getQualifiedSourceName());
continue;
}
} else {
poison("Unexpecetd parameters in method %s", method.getName());
continue;
}
AutoBeanType autoBeanType = getAutoBeanType(beanType);
// Must wrap things that aren't simple interfaces
if (!autoBeanType.isSimpleBean() && toWrap == null) {
if (categoryTypes != null) {
poison("The %s parameterization is not simple and the following"
+ " methods did not have static implementations:",
beanType.getQualifiedSourceName());
for (AutoBeanMethod missing : autoBeanType.getMethods()) {
if (missing.getAction().equals(JBeanMethod.CALL)
&& missing.getStaticImpl() == null) {
poison(missing.getMethod().getReadableDeclaration());
}
}
} else {
poison("The %s parameterization is not simple, but the %s method"
+ " does not provide a delegate",
beanType.getQualifiedSourceName(), method.getName());
}
continue;
}
AutoBeanFactoryMethod.Builder builder = new AutoBeanFactoryMethod.Builder();
builder.setAutoBeanType(autoBeanType);
builder.setMethod(method);
methods.add(builder.build());
}
while (!toCalculate.isEmpty()) {
Set<JClassType> examine = toCalculate;
toCalculate = new LinkedHashSet<JClassType>();
for (JClassType beanType : examine) {
getAutoBeanType(beanType);
}
}
if (poisoned) {
die("Unable to complete due to previous errors");
}
}
public Collection<AutoBeanType> getAllTypes() {
return Collections.unmodifiableCollection(peers.values());
}
public List<JClassType> getCategoryTypes() {
return categoryTypes;
}
public Map<JEnumConstant, String> getEnumTokenMap() {
return Collections.unmodifiableMap(allEnumConstants);
}
public List<AutoBeanFactoryMethod> getMethods() {
return Collections.unmodifiableList(methods);
}
public AutoBeanType getPeer(JClassType beanType) {
beanType = ModelUtils.ensureBaseType(beanType);
return peers.get(beanType);
}
private List<AutoBeanMethod> computeMethods(JClassType beanType) {
List<JMethod> toExamine = new ArrayList<JMethod>();
toExamine.addAll(Arrays.asList(beanType.getInheritableMethods()));
toExamine.addAll(objectMethods);
List<AutoBeanMethod> toReturn = new ArrayList<AutoBeanMethod>(
toExamine.size());
for (JMethod method : toExamine) {
if (method.isPrivate()) {
// Ignore private methods
continue;
}
AutoBeanMethod.Builder builder = new AutoBeanMethod.Builder();
builder.setMethod(method);
// See if this method shouldn't have its return type wrapped
// TODO: Allow class return types?
JClassType classReturn = method.getReturnType().isInterface();
if (classReturn != null) {
maybeCalculate(classReturn);
if (noWrapTypes != null) {
for (JClassType noWrap : noWrapTypes) {
if (noWrap.isAssignableFrom(classReturn)) {
builder.setNoWrap(true);
break;
}
}
}
}
// GET, SET, or CALL
JBeanMethod action = JBeanMethod.which(method);
builder.setAction(action);
if (JBeanMethod.CALL.equals(action)) {
JMethod staticImpl = findStaticImpl(beanType, method);
if (staticImpl == null && objectMethods.contains(method)) {
// Don't complain about lack of implementation for Object methods
continue;
}
builder.setStaticImp(staticImpl);
}
AutoBeanMethod toAdd = builder.build();
// Collect referenced enums
if (toAdd.hasEnumMap()) {
allEnumConstants.putAll(toAdd.getEnumMap());
}
// See if parameterizations will pull in more types
if (toAdd.isCollection()) {
maybeCalculate(toAdd.getElementType());
} else if (toAdd.isMap()) {
maybeCalculate(toAdd.getKeyType());
maybeCalculate(toAdd.getValueType());
}
toReturn.add(toAdd);
}
return toReturn;
}
private void die(String message) throws UnableToCompleteException {
poison(message);
throw new UnableToCompleteException();
}
/**
* Find <code>Object __intercept(AutoBean&lt;?> bean, Object value);</code> in
* the category types.
*/
private JMethod findInterceptor(JClassType beanType) {
if (categoryTypes == null) {
return null;
}
for (JClassType category : categoryTypes) {
for (JMethod method : category.getOverloads("__intercept")) {
// Ignore non-static, non-public methods
// TODO: Implement visibleFrom() to allow package-protected categories
if (!method.isStatic() || !method.isPublic()) {
continue;
}
JParameter[] params = method.getParameters();
if (params.length != 2) {
continue;
}
if (!methodAcceptsAutoBeanAsFirstParam(beanType, method)) {
continue;
}
JClassType value = params[1].getType().isClassOrInterface();
if (value == null) {
continue;
}
if (!oracle.getJavaLangObject().isAssignableTo(value)) {
continue;
}
return method;
}
}
return null;
}
/**
* Search the category types for a static implementation of an interface
* method. Given the interface method declaration:
*
* <pre>
* Foo bar(Baz baz);
* </pre>
*
* this will search the types in {@link #categoryTypes} for the following
* method:
*
* <pre>
* public static Foo bar(AutoBean&lt;Intf> bean, Baz baz) {}
* </pre>
*/
private JMethod findStaticImpl(JClassType beanType, JMethod method) {
if (categoryTypes == null) {
return null;
}
for (JClassType category : categoryTypes) {
// One extra argument for the AutoBean
JParameter[] methodParams = method.getParameters();
int requiredArgs = methodParams.length + 1;
overload : for (JMethod overload : category.getOverloads(method.getName())) {
if (!overload.isStatic() || !overload.isPublic()) {
// Ignore non-static, non-public methods
continue;
}
JParameter[] overloadParams = overload.getParameters();
if (overloadParams.length != requiredArgs) {
continue;
}
if (!methodAcceptsAutoBeanAsFirstParam(beanType, overload)) {
// Ignore if the first parameter is a primitive or not assignable
continue;
}
// Match the rest of the parameters
for (int i = 1; i < requiredArgs; i++) {
JType methodType = methodParams[i - 1].getType();
JType overloadType = overloadParams[i].getType();
if (methodType.equals(overloadType)) {
// Match; exact, the usual case
} else if (methodType.isClassOrInterface() != null
&& overloadType.isClassOrInterface() != null
&& methodType.isClassOrInterface().isAssignableTo(
overloadType.isClassOrInterface())) {
// Match; assignment-compatible
} else {
// No match, keep looking
continue overload;
}
}
return overload;
}
}
return null;
}
private AutoBeanType getAutoBeanType(JClassType beanType) {
beanType = ModelUtils.ensureBaseType(beanType);
AutoBeanType toReturn = peers.get(beanType);
if (toReturn == null) {
AutoBeanType.Builder builder = new AutoBeanType.Builder();
builder.setOwnerFactory(this);
builder.setPeerType(beanType);
builder.setMethods(computeMethods(beanType));
builder.setInterceptor(findInterceptor(beanType));
if (noWrapTypes != null) {
for (JClassType noWrap : noWrapTypes) {
if (noWrap.isAssignableFrom(beanType)) {
builder.setNoWrap(true);
break;
}
}
}
toReturn = builder.build();
peers.put(beanType, toReturn);
}
return toReturn;
}
/**
* Enqueue a type in {@link #toCalculate} if {@link #peers} does not already
* contain an entry.
*/
private void maybeCalculate(JClassType type) {
if (type.isInterface() == null || ModelUtils.isValueType(oracle, type)) {
return;
}
if (!peers.containsKey(type)) {
toCalculate.add(type);
}
}
private boolean methodAcceptsAutoBeanAsFirstParam(JClassType beanType,
JMethod method) {
JParameter[] params = method.getParameters();
if (params.length == 0) {
return false;
}
JClassType paramAsClass = params[0].getType().isClassOrInterface();
// First parameter is a primitive
if (paramAsClass == null) {
return false;
}
// Check using base types to account for erasure semantics
JParameterizedType expectedFirst = oracle.getParameterizedType(
autoBeanInterface,
new JClassType[] {ModelUtils.ensureBaseType(beanType)});
return expectedFirst.isAssignableTo(paramAsClass);
}
private void poison(String message, Object... args) {
logger.log(TreeLogger.ERROR, String.format(message, args));
poisoned = true;
}
private void processClassArrayAnnotation(Class<?>[] classes,
Collection<JClassType> accumulator) {
for (Class<?> clazz : classes) {
JClassType category = oracle.findType(clazz.getCanonicalName());
if (category == null) {
poison("Could not find @%s type %s in the TypeOracle",
Category.class.getSimpleName(), clazz.getCanonicalName());
continue;
} else if (!category.isPublic()) {
poison("Category type %s is not public",
category.getQualifiedSourceName());
continue;
} else if (!category.isStatic() && category.isMemberType()) {
poison("Category type %s must be static",
category.getQualifiedSourceName());
continue;
}
accumulator.add(category);
}
}
}