package com.google.collide.dtogen;
import com.google.collide.dtogen.DtoTemplate.MalformedDtoInterfaceException;
import com.google.collide.dtogen.shared.CompactJsonDto;
import com.google.collide.dtogen.shared.SerializationIndex;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonStringMap;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Abstract base class for the source generating template for a single DTO.
*/
abstract class DtoImpl {
// If routingType is RoutableDto.INVALID_TYPE, then we simply exclude it
// from our routing table.
private final int routingType;
private final Class<?> dtoInterface;
private final DtoTemplate enclosingTemplate;
private final boolean compactJson;
final String implClassName;
private final List<Method> dtoMethods;
DtoImpl(DtoTemplate enclosingTemplate, int routingType, Class<?> dtoInterface) {
this.enclosingTemplate = enclosingTemplate;
this.routingType = routingType;
this.dtoInterface = dtoInterface;
this.implClassName = dtoInterface.getSimpleName() + "Impl";
this.compactJson = DtoTemplate.implementsInterface(dtoInterface, CompactJsonDto.class);
this.dtoMethods = ImmutableList.copyOf(calcDtoMethods());
}
protected boolean isCompactJson() {
return compactJson;
}
public Class<?> getDtoInterface() {
return dtoInterface;
}
public DtoTemplate getEnclosingTemplate() {
return enclosingTemplate;
}
public int getRoutingType() {
return routingType;
}
protected String getFieldName(String methodName) {
// TODO: Consier field name obfuscation for code savings on the
// wire. For now just use a munging of the getter's name (strip get and
// make first letter lower cased).
String fieldName = methodName.replaceFirst("get", "");
fieldName = Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
return fieldName;
}
protected String getImplClassName() {
return implClassName;
}
protected String getSetterName(String fieldName) {
return "set" + getCamelCaseName(fieldName);
}
protected String getListAdderName(String fieldName) {
return "add" + getCamelCaseName(fieldName);
}
protected String getMapPutterName(String fieldName) {
return "put" + getCamelCaseName(fieldName);
}
protected String getClearName(String fieldName) {
return "clear" + getCamelCaseName(fieldName);
}
protected String getEnsureName(String fieldName) {
return "ensure" + getCamelCaseName(fieldName);
}
protected String getCamelCaseName(String fieldName) {
return Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
}
/**
* Our super interface may implement some other interface (or not). We need to
* know because if it does then we need to directly extend said super
* interfaces impl class.
*/
protected Class<?> getSuperInterface() {
Class<?>[] superInterfaces = dtoInterface.getInterfaces();
return superInterfaces.length == 0 ? null : superInterfaces[0];
}
/**
* We need not generate a field and method for any method present on a parent
* interface that our interface may inherit from. We only care about the new
* methods defined on our superInterface.
*/
protected boolean ignoreMethod(Method method) {
if (method == null) {
return true;
}
// Look at any interfaces our superInterface implements.
Class<?>[] superInterfaces = dtoInterface.getInterfaces();
List<Method> methodsToExclude = new ArrayList<Method>();
// Collect methods on parent interfaces
for (Class<?> parent : superInterfaces) {
for (Method m : parent.getMethods()) {
methodsToExclude.add(m);
}
}
for (Method m : methodsToExclude) {
if (m.equals(method)) {
return true;
}
}
return false;
}
/**
* Tests whether or not a given generic type is allowed to be used as a
* generic.
*/
protected static boolean isWhitelisted(Class<?> genericType) {
return DtoTemplate.jreWhitelist.contains(genericType);
}
/**
* Tests whether or not a given return type is a JsonArray.
*/
public static boolean isJsonArray(Class<?> returnType) {
return returnType.equals(JsonArray.class);
}
/**
* Tests whether or not a given return type is a JsonArray.
*/
public static boolean isJsonStringMap(Class<?> returnType) {
return returnType.equals(JsonStringMap.class);
}
/**
* Expands the type and its first generic parameter (which can also have a
* first generic parameter (...)).
*
* For example, JsonArray<JsonStringMap<JsonArray<SomeDto>>>
* would produce [JsonArray, JsonStringMap, JsonArray, SomeDto].
*/
public static List<Type> expandType(Type curType) {
List<Type> types = new ArrayList<Type>();
do {
types.add(curType);
if (curType instanceof ParameterizedType) {
Type[] genericParamTypes = ((ParameterizedType) curType).getActualTypeArguments();
if (genericParamTypes.length != 1) {
throw new IllegalStateException("Multiple type parameters are not supported"
+ "(neither are zero type parameters)");
}
Type genericParamType = genericParamTypes[0];
if (genericParamType instanceof Class<?>) {
Class<?> genericParamTypeClass = (Class<?>) genericParamType;
if (isWhitelisted(genericParamTypeClass)) {
assert genericParamTypeClass.equals(String.class) :
"For JSON serialization there can be only strings or DTO types. "
+ "Please ping smok@ if you see this assert happening.";
}
}
curType = genericParamType;
} else {
if (curType instanceof Class) {
Class<?> clazz = (Class<?>) curType;
if (isJsonArray(clazz) || isJsonStringMap(clazz)) {
throw new MalformedDtoInterfaceException(
"JsonArray and JsonStringMap MUST have a generic type specified (and no... ? "
+ "doesn't cut it!).");
}
}
curType = null;
}
} while (curType != null);
return types;
}
public static Class<?> getRawClass(Type type) {
return (Class<?>) ((type instanceof ParameterizedType) ? ((ParameterizedType) type)
.getRawType() : type);
}
String getRoutingTypeField() {
return dtoInterface.getSimpleName().toUpperCase() + "_TYPE";
}
/**
* Returns public methods specified in DTO interface.
*
* <p>For compact DTO (see {@link CompactJsonDto}) methods are ordered
* corresponding to {@link SerializationIndex} annotation.
*
* <p>Gaps in index sequence are filled with {@code null}s.
*/
protected List<Method> getDtoMethods() {
return dtoMethods;
}
private Method[] calcDtoMethods() {
if (!compactJson) {
return dtoInterface.getMethods();
}
Map<Integer, Method> methodsMap = new HashMap<Integer, Method>();
int maxIndex = 0;
for (Method method : dtoInterface.getMethods()) {
if (method.getName().equals("getType")) {
continue;
}
SerializationIndex serializationIndex = method.getAnnotation(SerializationIndex.class);
Preconditions.checkNotNull(serializationIndex,
"Serialization index is not specified for %s in %s",
method.getName(), dtoInterface.getSimpleName());
// "53" is the number of bits in JS integer.
// This restriction will allow to add simple bit-field
// "serialization-skipping-list" in the future.
int index = serializationIndex.value();
Preconditions.checkState(index > 0 && index <= 53,
"Serialization index out of range [1..53] for %s in %s",
method.getName(), dtoInterface.getSimpleName());
Preconditions.checkState(!methodsMap.containsKey(index),
"Duplicate serialization index for %s in %s",
method.getName(), dtoInterface.getSimpleName());
maxIndex = Math.max(index, maxIndex);
methodsMap.put(index, method);
}
Method[] result = new Method[maxIndex];
for (int index = 0; index < maxIndex; index++) {
result[index] = methodsMap.get(index + 1);
}
return result;
}
protected boolean isLastMethod(Method method) {
Preconditions.checkNotNull(method);
return method == dtoMethods.get(dtoMethods.size() - 1);
}
/**
* @return String representing the source definition for the DTO impl as an
* inner class.
*/
abstract String serialize();
}