package io.vertx.codegen; /* * Copyright 2014 Red Hat, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ import io.vertx.codegen.annotations.GenIgnore; import io.vertx.codegen.type.ClassKind; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.*; import javax.lang.model.type.*; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.GenericDeclaration; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; /** * @author <a href="http://tfox.org">Tim Fox</a> */ public class Helper { public static final Function<Element, Stream<ExecutableElement>> FILTER_METHOD = element -> { if (element.getKind() == ElementKind.METHOD) { return Stream.of((ExecutableElement) element); } else { return Stream.empty(); } }; public static final Function<Element, Stream<VariableElement>> FILTER_FIELD = element -> { if (element.getKind() == ElementKind.FIELD) { return Stream.of((VariableElement) element); } else { return Stream.empty(); } }; static <T> Function<Object, Stream<T>> instanceOf(Class<T> type) { return o -> { if (type.isInstance(o)) { return Stream.of(type.cast(o)); } else { return Stream.empty(); } }; } static <T> Function<Object, Stream<T>> cast(Class<T> type) { return o -> Stream.of(type.cast(o)); } static final Function<Element, Stream<ExecutableElement>> CAST = element -> { if (element.getKind() == ElementKind.METHOD) { return Stream.of((ExecutableElement) element); } else { return Stream.empty(); } }; /** * Normalize a property name:<br/> * * <ul> * <li>the first char will always be a lower case</li> * <li>if the first char is an upper case, any following upper case char will be lower cased unless it is followed * by a lower case char</li> * </ul> * * For instance: * <ul> * <li>foo -> foo</li> * <li>Foo -> foo</li> * <li>URL -> url</li> * <li>URLFactory -> urlFactory</li> * </ul> * * @param propertyName the property name * @return the normalized property name */ public static String normalizePropertyName(String propertyName) { if (Character.isUpperCase(propertyName.charAt(0))) { StringBuilder buffer = new StringBuilder(propertyName); int index = 0; while (true) { buffer.setCharAt(index, Character.toLowerCase(buffer.charAt(index++))); if (index < buffer.length() && Character.isUpperCase(buffer.charAt(index))) { if (index + 1 < buffer.length() && Character.isLowerCase(buffer.charAt(index + 1))) { break; } } else { break; } } propertyName = buffer.toString(); } return propertyName; } public static String decapitaliseFirstLetter(String str) { if (str.length() == 0) { return str; } else { return str.substring(0, 1).toLowerCase() + str.substring(1); } } public static String convertCamelCaseToUnderscores(String str) { return str.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z\\d])([A-Z])", "$1_$2").toLowerCase(); } public static String getSimpleName(String type) { return type.substring(type.lastIndexOf('.') + 1); } public static String getPackageName(String type) { int index = type.lastIndexOf('.'); if (index >= 0) { return type.substring(0, index); } else { return ""; } } public static String getNonGenericType(String type) { int pos = type.indexOf("<"); if (pos >= 0) { String nonGenericType = type.substring(0, pos); return nonGenericType; } else { return type; } } public static String indentString(String str, String indent) { StringBuilder sb = new StringBuilder(indent); for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); sb.append(ch); if (ch == '\n' && i != str.length() - 1) { sb.append(indent); } } return sb.toString(); } public static String getJavadocTag(String comment, String tagName) { int pos = comment.indexOf(tagName); int endPos = comment.indexOf("\n", pos); String tag = comment.substring(pos + tagName.length() + 1, endPos); return tag; } public static String removeTags(String comment) { // we remove everything from the first tag to the end of the comment - // tags MUST be at the end of the comment int pos = comment.indexOf('@'); if (pos == -1) { return comment; } if (pos > 0) { String beforePos = comment.substring(0, pos); int prevReturn = beforePos.lastIndexOf('\n'); if (prevReturn != -1) { pos = prevReturn; } else { pos = 0; } } return comment.substring(0, pos); } /** * Resolve a method annotation, this method scan the specified method, if the annotation is not found * it will also scan the methods this method overrides and return the annotation when it is found. * * @param annotationType the annotation type, * @param elementUtils element utils * @param typeUtils type utils * @param declaring the element declaring the method * @param method the method to start the resolution from * @return the annotation if resolved otherwise null */ public static AnnotationMirror resolveMethodAnnotation( Class<? extends Annotation> annotationType, Elements elementUtils, Types typeUtils, TypeElement declaring, ExecutableElement method) { return resolveMethodAnnotation( (DeclaredType) elementUtils.getTypeElement(annotationType.getName()).asType(), elementUtils, typeUtils, declaring, method); } /** * Resolve a method annotation, this method scan the specified method, if the annotation is not found * it will also scan the methods this method overrides and return the annotation when it is found. * * @param annotationType the annotation type, * @param elementUtils element utils * @param typeUtils type utils * @param declaring the element declaring the method * @param method the method to start the resolution from * @return the annotation if resolved otherwise null */ public static AnnotationMirror resolveMethodAnnotation( DeclaredType annotationType, Elements elementUtils, Types typeUtils, TypeElement declaring, ExecutableElement method) { Optional<? extends AnnotationMirror> annotation = method.getAnnotationMirrors().stream().filter(mirror -> typeUtils.isSameType(mirror.getAnnotationType(), annotationType)).findFirst(); if (annotation.isPresent()) { return annotation.get(); } else { return isFluent(annotationType, elementUtils, typeUtils, declaring, method, method.getEnclosingElement().asType()); } } private static AnnotationMirror isFluent(DeclaredType annotationType, Elements elementUtils, Types typeUtils, TypeElement declaring, ExecutableElement method, TypeMirror type) { for (TypeMirror directSuperType : typeUtils.directSupertypes(type)) { Element directSuperTypeElt = typeUtils.asElement(directSuperType); if (directSuperTypeElt instanceof TypeElement) { List<ExecutableElement> methods = ((TypeElement) directSuperTypeElt).getEnclosedElements().stream(). filter(member -> member.getKind() == ElementKind.METHOD).map(member -> (ExecutableElement) member). collect(Collectors.toList()); for (ExecutableElement m : methods) { if (elementUtils.overrides(method, m, declaring)) { AnnotationMirror annotation = resolveMethodAnnotation(annotationType, elementUtils, typeUtils, (TypeElement) directSuperTypeElt, m); if (annotation != null) { return annotation; } } } AnnotationMirror annotation = isFluent(annotationType, elementUtils, typeUtils, declaring, method, directSuperType); if (annotation != null) { return annotation; } } } return null; } /** * Return the type of a type parameter element of a given type element when that type parameter * element is parameterized by a sub type, directly or indirectly. When the type parameter cannot * be resolved, null is returned. * * @param typeUtils the type utils * @param subType the sub type for which the type parameter is parameterized * @param typeParam the type parameter to resolve * @return the type parameterizing the type parameter */ public static TypeMirror resolveTypeParameter(Types typeUtils, DeclaredType subType, TypeParameterElement typeParam) { TypeMirror erased = typeUtils.erasure(typeParam.getGenericElement().asType()); TypeMirror erasedSubType = typeUtils.erasure(subType); if (typeUtils.isSameType(erased, erasedSubType)) { return typeUtils.asMemberOf(subType, ((TypeVariable) typeParam.asType()).asElement()); } else if (typeUtils.isSubtype(erasedSubType, erased)) { for (TypeMirror superType : typeUtils.directSupertypes(subType)) { TypeMirror resolved = resolveTypeParameter(typeUtils, (DeclaredType) superType, typeParam); if (resolved != null) { return resolved; } } } return null; } /** * Return the type of a type parameter element of a given type element when that type parameter * element is parameterized by a sub type, directly or indirectly. When the type parameter cannot * be resolve, null is returned. * * @param type the sub type for which the type parameter is parameterized * @param typeParam the type parameter to resolve * @return the type parameterizing the type parameter */ public static <T> Type resolveTypeParameter(Type type, java.lang.reflect.TypeVariable<java.lang.Class<T>> typeParam) { if (type instanceof Class<?>) { Class<?> classType = (Class<?>) type; if (Stream.of(classType.getTypeParameters()).filter(tp -> tp.equals(typeParam)).findFirst().isPresent()) { return typeParam; } List<Type> superTypes = new ArrayList<>(); if (classType.getGenericSuperclass() != null) { superTypes.add(classType.getGenericSuperclass()); } Collections.addAll(superTypes, classType.getGenericInterfaces()); for (Type superType : superTypes) { Type resolved = resolveTypeParameter(superType, typeParam); if (resolved != null) { return resolved; } } } else if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; Type rawType = parameterizedType.getRawType(); Type resolvedType = resolveTypeParameter(rawType, typeParam); if (resolvedType instanceof java.lang.reflect.TypeVariable<?>) { GenericDeclaration owner = ((java.lang.reflect.TypeVariable) resolvedType).getGenericDeclaration(); if (owner.equals(rawType)) { java.lang.reflect.TypeVariable<?>[] typeParams = owner.getTypeParameters(); for (int i = 0;i < typeParams.length;i++) { if (typeParams[i].equals(resolvedType)) { return parameterizedType.getActualTypeArguments()[i]; } } } } } else { throw new UnsupportedOperationException("Todo " + type.getTypeName() + " " + type.getClass().getName()); } return null; } private static final Pattern SIGNATURE_PATTERN = Pattern.compile("#(\\p{javaJavaIdentifierStart}(?:\\p{javaJavaIdentifierPart})*)(?:\\((.*)\\))?$"); public static final Pattern LINK_REFERENCE_PATTERN = Pattern.compile( "(?:(?:\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.)*" + "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)?" + "(?:" + SIGNATURE_PATTERN.pattern() + ")?"); /** * Resolves a documentation signature, null can be returned if no element can be resolved. * * @param elementUtils the element utils * @param typeUtils the type utils * @param declaringElt the declaring element, may be null * @param signature the signature to resolve * @return the resolved element */ public static Element resolveSignature( Elements elementUtils, Types typeUtils, TypeElement declaringElt, String signature) { Matcher signatureMatcher = SIGNATURE_PATTERN.matcher(signature); if (signatureMatcher.find()) { String memberName = signatureMatcher.group(1); String typeName = signature.substring(0, signatureMatcher.start()); TypeElement typeElt = resolveTypeElement(elementUtils, declaringElt, typeName); if (typeElt != null) { Predicate<? super Element> memberMatcher; if (signatureMatcher.group(2) != null) { String t = signatureMatcher.group(2).trim(); Predicate<ExecutableElement> parametersMatcher; if (t.length() == 0) { parametersMatcher = exeElt -> exeElt.getParameters().isEmpty(); } else { parametersMatcher = parametersMatcher(typeUtils, t.split("\\s*,\\s*")); } memberMatcher = elt -> matchesConstructor(elt, memberName, parametersMatcher) || matchesMethod(elt, memberName, parametersMatcher); } else { memberMatcher = elt -> matchesConstructor(elt, memberName, exeElt -> true) || matchesMethod(elt, memberName, exeElt -> true) || matchesField(elt, memberName); } // The order of kinds is important for (ElementKind kind : Arrays.asList(ElementKind.FIELD, ElementKind.CONSTRUCTOR, ElementKind.METHOD)) { for (Element memberElt : elementUtils.getAllMembers(typeElt)) { if(memberElt.getKind() == kind && memberMatcher.test(memberElt)) { return memberElt; } } } } return null; } else { return resolveTypeElement(elementUtils, declaringElt, signature); } } private static TypeElement resolveTypeElement(Elements elementUtils, TypeElement declaringElt, String typeName) { TypeElement resolvedElt; if (typeName.isEmpty()) { resolvedElt = declaringElt; } else { if (typeName.lastIndexOf('.') == -1) { resolvedElt = elementUtils.getTypeElement("java.lang." +typeName); if (resolvedElt == null) { String packageName = elementUtils.getPackageOf(declaringElt).getQualifiedName().toString(); resolvedElt = elementUtils.getTypeElement(packageName + '.' + typeName); } } else { resolvedElt = elementUtils.getTypeElement(typeName); } } return resolvedElt; } private static boolean matchesConstructor(Element elt, String memberName, Predicate<ExecutableElement> parametersMatcher) { if (elt.getKind() == ElementKind.CONSTRUCTOR) { ExecutableElement constructorElt = (ExecutableElement) elt; TypeElement typeElt = (TypeElement) constructorElt.getEnclosingElement(); return typeElt.getSimpleName().toString().equals(memberName) && parametersMatcher.test(constructorElt); } return false; } private static boolean matchesMethod(Element elt, String memberName, Predicate<ExecutableElement> parametersMatcher) { if (elt.getKind() == ElementKind.METHOD) { ExecutableElement methodElt = (ExecutableElement) elt; return methodElt.getSimpleName().toString().equals(memberName) && parametersMatcher.test(methodElt); } return false; } private static boolean matchesField(Element elt, String memberName) { return elt.getKind() == ElementKind.FIELD && elt.getSimpleName().toString().equals(memberName); } /** * Return a matcher for parameters, given the parameter type signature of an executable element. The parameter signature * is a list of parameter types formatted as a signature, i.e all types are raw, or primitive, or arrays. Unqualified * types are resolved against the import of the specified {@code compilationUnitTree} argument. * * @param parameterSignature the parameter type names * @return the matcher */ private static Predicate<ExecutableElement> parametersMatcher(Types typeUtils, String[] parameterSignature) { return exeElt -> { if (exeElt.getParameters().size() == parameterSignature.length) { TypeMirror tm2 = exeElt.asType(); ExecutableType tm3 = (ExecutableType) typeUtils.erasure(tm2); for (int j = 0; j < parameterSignature.length; j++) { String t1 = tm3.getParameterTypes().get(j).toString(); String t2 = parameterSignature[j]; if (t2.indexOf('.') == -1) { t1 = t1.substring(t1.lastIndexOf('.') + 1); } if (!t1.equals(t2)) { return false; } } return true; } else { return false; } }; } /** * Return the element type of the specified element. * * @param elt the element * @return the element type or null if none exists */ public static TypeElement getElementTypeOf(Element elt) { ElementKind kind = elt.getKind(); if (kind == ElementKind.CLASS || kind == ElementKind.INTERFACE || kind == ElementKind.ENUM) { return (TypeElement) elt; } Element enclosingElt = elt.getEnclosingElement(); if (enclosingElt != null) { return getElementTypeOf(enclosingElt); } return null; } private static final Pattern WHITESPACE_CLUSTER_PATTERN = Pattern.compile("\\s+"); /** * Trim and normalize the whitespaces in a string: any cluster of more than one whitespace char * is replaced by a space char, then the string is trimmed. * * @param s the string to normalize * @return the normalized string */ public static String normalizeWhitespaces(String s) { Matcher matcher = WHITESPACE_CLUSTER_PATTERN.matcher(s); return matcher.replaceAll(" ").trim(); } /** * Resolve the set of all the ancestors declared types of a given type element. * * @param typeElt the type element to resolve * @return the set of ancestors */ public static Set<DeclaredType> resolveAncestorTypes(TypeElement typeElt, boolean withSuper, boolean withInterfaces) { Set<DeclaredType> ancestors = new LinkedHashSet<>(); resolveAncestorTypes(typeElt, ancestors, withSuper, withInterfaces); return ancestors; } private static void resolveAncestorTypes(TypeElement typeElt, Set<DeclaredType> ancestors, boolean withSuper, boolean withInterfaces) { List<TypeMirror> superTypes = new ArrayList<>(); if (withSuper && typeElt.getSuperclass() != null) { superTypes.add(typeElt.getSuperclass()); } if (withInterfaces) { superTypes.addAll(typeElt.getInterfaces()); } for (TypeMirror superType : superTypes) { if (superType.getKind() == TypeKind.DECLARED) { DeclaredType superDeclaredType = (DeclaredType) superType; if (!ancestors.contains(superDeclaredType)) { ancestors.add(superDeclaredType); resolveAncestorTypes((TypeElement) superDeclaredType.asElement(), ancestors, withSuper, withInterfaces); } } } } static void checkUnderModule(Model model, String annotation) { if (model.getModule() == null) { throw new GenException(model.getElement(), "Declaration annotated with " + annotation + " must be under a package annotated" + "with @ModuleGen. Check that the package '" + model.getFqn() + "' or a parent package contains a 'package-info.java' using the @ModuleGen annotation"); } } static void ensureParentDir(File f) { if (!f.getParentFile().exists()) { f.getParentFile().mkdirs(); } } /** * Compute the string representation of a type mirror. * * @param mirror the type mirror * @return the string representation */ static String toString(TypeMirror mirror) { StringBuilder buffer = new StringBuilder(); toString(mirror, buffer); return buffer.toString(); } /** * Compute the string representation of a type mirror. * * @param mirror the type mirror * @param buffer the buffer appended with the string representation */ static void toString(TypeMirror mirror, StringBuilder buffer) { switch (mirror.getKind()) { case DECLARED: { DeclaredType dt = (DeclaredType) mirror; TypeElement elt = (TypeElement) dt.asElement(); buffer.append(elt.getQualifiedName().toString()); List<? extends TypeMirror> args = dt.getTypeArguments(); if (args.size() > 0) { buffer.append("<"); for (int i = 0;i < args.size();i++) { if (i > 0) { buffer.append(","); } toString(args.get(i), buffer); } buffer.append(">"); } break; } case WILDCARD: { javax.lang.model.type.WildcardType wt = (javax.lang.model.type.WildcardType) mirror; buffer.append("?"); if (wt.getSuperBound() != null) { buffer.append(" super "); toString(wt.getSuperBound(), buffer); } else if (wt.getExtendsBound() != null) { buffer.append(" extends "); toString(wt.getExtendsBound(), buffer); } break; } case TYPEVAR: { javax.lang.model.type.TypeVariable tv = (TypeVariable) mirror; TypeParameterElement elt = (TypeParameterElement) tv.asElement(); buffer.append(elt.getSimpleName().toString()); if (tv.getUpperBound() != null && !tv.getUpperBound().toString().equals("java.lang.Object")) { buffer.append(" extends "); toString(tv.getUpperBound(), buffer); } else if (tv.getLowerBound() != null && tv.getLowerBound().getKind() != TypeKind.NULL) { buffer.append(" super "); toString(tv.getUpperBound(), buffer); } break; } case BYTE: case SHORT: case INT: case LONG: case FLOAT: case DOUBLE: case CHAR: case BOOLEAN: { PrimitiveType pm = (PrimitiveType) mirror; buffer.append(pm.getKind().name().toLowerCase()); break; } case ARRAY: { ArrayType at = (ArrayType) mirror; toString(at.getComponentType(), buffer); buffer.append("[]"); break; } default: throw new UnsupportedOperationException("todo " + mirror + " " + mirror.getKind()); } } /** * Like {@link #getReflectMethod(ClassLoader, ExecutableElement)} but using the processing environment context. */ public static Method getReflectMethod(ProcessingEnvironment env, ExecutableElement modelMethod) { ClassLoader loader = CodeGen.loaderMap.get(env); if (loader != null) { return getReflectMethod(loader, modelMethod); } return null; } /** * Returns a {@link Method } corresponding to the {@literal methodElt} parameter. Obviously this work * only when the corresponding method is available on the classpath using java lang reflection. * * @param modelMethod the model method element * @return the method or null if not found */ public static Method getReflectMethod(ClassLoader loader, ExecutableElement modelMethod) { TypeElement typeElt = (TypeElement) modelMethod.getEnclosingElement(); Method method = null; try { Class<?> clazz = loader.loadClass(typeElt.getQualifiedName().toString()); StringBuilder sb = new StringBuilder(modelMethod.getSimpleName()); sb.append("("); List<? extends VariableElement> params = modelMethod.getParameters(); for (int i = 0;i < params.size();i++) { if (i > 0) { sb.append(","); } VariableElement param = params.get(i); toString(param.asType(), sb); } sb.append(")"); String s = sb.toString(); for (Method m : clazz.getMethods()) { String sign = m.toGenericString(); int pos = sign.indexOf('('); pos = sign.lastIndexOf('.', pos) + 1; sign = sign.substring(pos); sign = sign.replace(", ", ","); // Remove space between arguments if (sign.equals(s)) { // Test this case if (method != null) { if (method.getReturnType().isAssignableFrom(m.getReturnType())) { method = m; } } else { method = m; } } } } catch (ClassNotFoundException e) { } return method; } public static ClassKind getAnnotatedDataObjectAnnotatedSerializationType(Elements elementUtils, TypeElement dataObjectElt) { return elementUtils.getAllMembers(dataObjectElt) .stream() .flatMap(Helper.FILTER_METHOD) .filter(exeElt -> exeElt.getParameters().isEmpty() && exeElt.getSimpleName().toString().equals("toJson")) .flatMap(exeElt -> { ClassKind ck; switch (exeElt.getReturnType().toString()) { case "io.vertx.core.json.JsonObject": return Stream.of(ClassKind.JSON_OBJECT); case "java.lang.String": return Stream.of(ClassKind.STRING); } return Stream.empty(); }) .findFirst() .orElse(null); } public static boolean isConcreteClass(TypeElement element) { return element.getKind() == ElementKind.CLASS && !element.getModifiers().contains(Modifier.ABSTRACT); } public static boolean isAbstractClassOrInterface(TypeElement element) { return element.getKind().isInterface() || (element.getKind() == ElementKind.CLASS && element.getModifiers().contains(Modifier.ABSTRACT)); } private static final Set<String> dataObjectTypes = new HashSet<>(Arrays.asList("io.vertx.core.json.JsonObject", "java.lang.String")); public static ClassKind getAnnotatedDataObjectDeserialisationType(Elements elementUtils, Types typeUtils, TypeElement dataObjectElt) { if (isConcreteClass(dataObjectElt)) { Set<String> types = elementUtils .getAllMembers(dataObjectElt) .stream() .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR) .map(e -> (ExecutableElement) e) .filter(constructor -> constructor.getParameters().size() == 1 && constructor.getModifiers().contains(Modifier.PUBLIC) && dataObjectTypes.contains(constructor.getParameters().get(0).asType().toString())) .map(ctor -> ctor.getParameters().get(0).asType().toString()) .collect(Collectors.toSet()); // Order matter if (types.contains("io.vertx.core.json.JsonObject")) { return ClassKind.JSON_OBJECT; } else if (types.contains("java.lang.String")) { return ClassKind.STRING; } } return null; } /** * @param elt the element to check * @return Return {@code true} if the {@code elt} is annotated with {@code @GenIgnore}. */ public static boolean isGenIgnore(Element elt) { return elt.getAnnotation(GenIgnore.class) != null; } }