/*
* Copyright 2014 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 org.inferred.freebuilder.processor.util.testing;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/** Static utility methods pertaining to {@link TypeMirror} instances. */
public class TypeMirrors {
/** e.g. '%1' */
private static final Pattern ARG_REF_PATTERN = Pattern.compile("%(\\d+)");
/** e.g. '%1[]' or '%1<%2,%3>' */
private static final Pattern GENERIC_OR_ARRAY_PATTERN = Pattern.compile(
"(%\\d+)\\s*(?:(\\[\\s*\\])|<\\s*((?:%\\d+)\\s*(?:,\\s*(?:%\\d+)\\s*)*)>)");
/** e.g. '><', '][' or ']<' */
private static final Pattern INVALID_TYPE_SNIPPET_PATTERN = Pattern.compile(
">\\s*[\\w<]|\\]\\s*[\\w<\\[]");
/** e.g. 'java.lang.String' */
private static final Pattern RAW_TYPE_PATTERN = Pattern.compile(
"[^\\W\\d]\\w*(\\s*[.]\\s*[^\\W\\d]\\w*)*");
/** Returns a {@link TypeMirror} for the given class (raw T, not T<?>, if T is generic). */
public static TypeMirror typeMirror(
Types typeUtils,
Elements elementUtils,
Class<?> cls) {
if (cls.equals(void.class)) {
return typeUtils.getNoType(TypeKind.VOID);
} else if (cls.isPrimitive()) {
return typeUtils.getPrimitiveType(TypeKind.valueOf(cls.getSimpleName().toUpperCase()));
} else if (cls.isArray()) {
return typeUtils.getArrayType(typeMirror(typeUtils, elementUtils, cls.getComponentType()));
} else {
return rawType(typeUtils, elementUtils, cls.getCanonicalName());
}
}
/** Returns a {@link TypeMirror} for the given type. */
public static TypeMirror typeMirror(Types typeUtils, Elements elementUtils, TypeToken<?> type) {
return typeMirror(typeUtils, elementUtils, type.getType());
}
/** Returns a {@link TypeMirror} for the given type. */
public static TypeMirror typeMirror(Types typeUtils, Elements elementUtils, Type type) {
if (type instanceof Class) {
return typeMirror(typeUtils, elementUtils, (Class<?>) type);
} else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) type).getGenericComponentType();
return typeUtils.getArrayType(typeMirror(typeUtils, elementUtils, componentType));
} else if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
DeclaredType rawType = (DeclaredType) typeMirror(typeUtils, elementUtils, pType.getRawType());
List<TypeMirror> typeArgumentMirrors = new ArrayList<TypeMirror>();
for (Type typeArgument : pType.getActualTypeArguments()) {
typeArgumentMirrors.add(typeMirror(typeUtils, elementUtils, typeArgument));
}
DeclaredType owner = (DeclaredType) typeMirror(typeUtils, elementUtils, pType.getOwnerType());
return typeUtils.getDeclaredType(
owner,
(TypeElement) rawType.asElement(),
typeArgumentMirrors.toArray(new TypeMirror[typeArgumentMirrors.size()]));
} else if (type instanceof WildcardType) {
Type lowerBound = getOnlyType(((WildcardType) type).getLowerBounds());
Type upperBound = getOnlyType(((WildcardType) type).getUpperBounds());
if (Object.class.equals(upperBound)) {
upperBound = null;
}
return typeUtils.getWildcardType(
typeMirror(typeUtils, elementUtils, upperBound),
typeMirror(typeUtils, elementUtils, lowerBound));
} else if (type == null) {
return null;
} else if (type instanceof TypeVariable) {
throw new IllegalArgumentException("Type variables not supported");
} else {
throw new IllegalArgumentException("Unrecognized Type subclass " + type.getClass());
}
}
/**
* Returns a {@link TypeMirror} for the given type, substituting any provided arguments for
* %1, %2, etc.
*
* <p>e.g. {@code typeMirror(types, elements, "java.util.List<%1>",
* typeMirror(types, elements, String.class))} will return the same thing as
* {@code typeMirror(types, elements, "java.util.List<java.lang.String>")}
*
* @param typeUtils an implementation of {@link Types}
* @param elementUtils an implementation of {@link Elements}
* @param typeSnippet the type, represented as a snippet of Java code, e.g.
* {@code "java.lang.String"}, {@code "java.util.Map<%1, %2>"}
* @param args existing {@link TypeMirror} instances to be substituted into the type
*/
public static TypeMirror typeMirror(
Types typeUtils,
Elements elementUtils,
String typeSnippet,
TypeMirror... args) {
checkArgReferences(typeSnippet, (args == null) ? 0 : args.length);
// Check for illegal patterns that the substitution algorithm may invalidly accept
Preconditions.checkArgument(
!INVALID_TYPE_SNIPPET_PATTERN.matcher(typeSnippet).find(),
"Invalid type string '%s'", typeSnippet);
Substitutions substitutions = new Substitutions(args);
MutableString mutableSnippet = new MutableString(typeSnippet.trim());
substituteRawTypes(typeUtils, elementUtils, mutableSnippet, substitutions);
substituteGenericsAndArrays(typeUtils, mutableSnippet, substitutions);
// Either we have only a '%d' left, or the input was invalid
Preconditions.checkArgument(
substitutions.containsKey(mutableSnippet.toString()),
"Invalid type string '%s'", typeSnippet);
return substitutions.get(mutableSnippet.toString());
}
private static Type getOnlyType(Type[] types) {
checkArgument(types.length <= 1, "Wildcard types with multiple bounds not supported");
return (types.length == 0) ? null : types[0];
}
/**
* Evaluate raw types, and substitute new %d strings in their place.
*
* <p>e.g. {@code "Map<String,List<Integer>>"} ⟼ {@code "%1<%2,%3<%4>>"}
*/
private static void substituteRawTypes(
Types typeUtils,
Elements elementUtils,
MutableString snippet,
Substitutions substitutions) {
for (MatchResult m : snippet.instancesOf(RAW_TYPE_PATTERN)) {
snippet.replace(m, substitutions.put(rawType(typeUtils, elementUtils, m.group(0))));
}
}
/**
* Evaluate generics and arrays depth-first, and substitute %d strings in their place.
* e.g. %1<%2,%3<%4>> --> %1<%2,%5> --> %6
*/
private static void substituteGenericsAndArrays(
Types typeUtils,
MutableString snippet,
Substitutions substitutions) {
for (MatchResult m : snippet.instancesOf(GENERIC_OR_ARRAY_PATTERN)) {
// Group 1 is the type on the left, e.g. '%1' in '%1<%2,%5>'
// Group 2 contains the array brackets if this is an array, e.g. '[]' in '%1[]'
// Group 3 contains the type list if this is a generic type, e.g. '%2,%5' in '%1<%2,%5>'
TypeMirror type = substitutions.get(m.group(1));
if (Strings.isNullOrEmpty(m.group(2))) {
List<TypeMirror> argTypes = Lists.transform(
Splitter.on(",").trimResults().splitToList(m.group(3)),
substitutions.asFunction());
snippet.replace(m, substitutions.put(parameterisedType(typeUtils, type, argTypes)));
} else {
snippet.replace(m, substitutions.put(typeUtils.getArrayType(type)));
}
}
}
/**
* Returns a parameterised generic type.
*
* @throws IllegalArgumentException if {@code rawType} is not in fact a raw type, or if
* the number of given parameters does not match the number declared on the raw type.
*/
private static DeclaredType parameterisedType(
Types typeUtils,
TypeMirror rawType,
List<TypeMirror> paramTypes) {
Preconditions.checkArgument(
rawType.getKind() == TypeKind.DECLARED
&& ((DeclaredType) rawType).getTypeArguments().isEmpty(),
"Expected raw type, got '%s'",
rawType);
TypeElement genericType = (TypeElement) typeUtils.asElement(rawType);
Preconditions.checkArgument(
genericType.getTypeParameters().size() == paramTypes.size(),
"Incorrect number of arguments for %s (expected %s, got %s)",
genericType,
genericType.getTypeParameters().size(),
paramTypes.size());
DeclaredType declaredType = typeUtils.getDeclaredType(
genericType, paramTypes.toArray(new TypeMirror[paramTypes.size()]));
return declaredType;
}
/** Check that all %d references in the given type snippet are within bounds. */
private static void checkArgReferences(String typeSnippet, int numberOfArgs) {
Matcher argRefMatcher = ARG_REF_PATTERN.matcher(typeSnippet);
while (argRefMatcher.find()) {
int index = Integer.parseInt(argRefMatcher.group(1), 10) - 1;
Preconditions.checkArgument(index >= 0,
"%s not allowed, indices start at 1", argRefMatcher.group(0));
Preconditions.checkArgument(index < numberOfArgs,
"%s too large for number of provided type mirrors", argRefMatcher.group(0));
}
}
private static TypeMirror rawType(
Types typeUtils,
Elements elementUtils,
String typeSnippet) {
TypeElement typeElement = elementUtils.getTypeElement(typeSnippet);
if (typeElement == null && !typeSnippet.contains(".")) {
typeElement = elementUtils.getTypeElement("java.lang." + typeSnippet);
}
Preconditions.checkArgument(typeElement != null, "Unrecognised type '%s'", typeSnippet);
return typeUtils.erasure(typeElement.asType());
}
/** Stores a mutable string, with easy regular expression search-and-replacement. */
private static class MutableString {
private String value;
MutableString(String value) {
this.value = Preconditions.checkNotNull(value);
}
/** Iterates through instances of the given pattern, resetting every time the string mutates. */
Iterable<MatchResult> instancesOf(final Pattern pattern) {
return new Iterable<MatchResult>() {
@Override public Iterator<MatchResult> iterator() {
return new AbstractIterator<MatchResult>() {
Matcher matcher;
String matchingAgainst;
@Override protected MatchResult computeNext() {
if (matchingAgainst != value) {
matchingAgainst = value;
matcher = pattern.matcher(matchingAgainst);
}
if (matcher.find()) {
return matcher.toMatchResult();
}
return endOfData();
}
};
}
};
}
/** Replaces the given match with the given replacement string. */
void replace(MatchResult match, String replacement) {
Preconditions.checkArgument(
match.group().equals(value.substring(match.start(), match.end())),
"MatchResult does not match the current value of this string");
value = value.substring(0, match.start()) + replacement + value.substring(match.end());
}
/** Returns this mutable string's current value. */
@Override
public String toString() {
return value;
}
}
/** Stores substitute strings of the form %d, mapped to the TypeMirrors that they represent. */
private static class Substitutions {
private final Map<String, TypeMirror> substitutions = Maps.newHashMap();
Substitutions(TypeMirror... args) {
for (int i = 0; args != null && i < args.length; ++i) {
substitutions.put("%" + (i + 1), args[i]);
}
}
boolean containsKey(String substitute) {
return substitutions.containsKey(substitute);
}
TypeMirror get(String substitute) {
return substitutions.get(substitute);
}
Function<String, TypeMirror> asFunction() {
return Functions.forMap(substitutions);
}
String put(TypeMirror t) {
String pattern = "%" + (substitutions.size() + 1);
substitutions.put(pattern, t);
return pattern;
}
}
}