io.micronaut.inject.writer.AbstractClassFileWriter Maven / Gradle / Ivy
/*
* Copyright 2017-2020 original authors
*
* 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 io.micronaut.inject.writer;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Generated;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.inject.annotation.AnnotationMetadataWriter;
import io.micronaut.inject.annotation.DefaultAnnotationMetadata;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.TypedElement;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Supplier;
/**
* Abstract class that writes generated classes to disk and provides convenience methods for building classes.
*
* @author Graeme Rocher
* @since 1.0
*/
@Internal
public abstract class AbstractClassFileWriter implements Opcodes {
protected static final Type TYPE_ARGUMENT = Type.getType(Argument.class);
protected static final Type TYPE_ARGUMENT_ARRAY = Type.getType(Argument[].class);
protected static final String ZERO_ARGUMENTS_CONSTANT = "ZERO_ARGUMENTS";
protected static final String CONSTRUCTOR_NAME = "";
protected static final String DESCRIPTOR_DEFAULT_CONSTRUCTOR = "()V";
protected static final Method METHOD_DEFAULT_CONSTRUCTOR = new Method(CONSTRUCTOR_NAME, DESCRIPTOR_DEFAULT_CONSTRUCTOR);
protected static final Type TYPE_OBJECT = Type.getType(Object.class);
protected static final Type TYPE_CLASS = Type.getType(Class.class);
protected static final int DEFAULT_MAX_STACK = 13;
protected static final Type TYPE_GENERATED = Type.getType(Generated.class);
protected static final Map NAME_TO_TYPE_MAP = new HashMap<>();
private static final Method METHOD_CREATE_ARGUMENT_SIMPLE = Method.getMethod(
ReflectionUtils.getRequiredInternalMethod(
Argument.class,
"of",
Class.class,
String.class
)
);
private static final Method METHOD_CREATE_ARGUMENT_WITH_GENERICS = Method.getMethod(
ReflectionUtils.getRequiredInternalMethod(
Argument.class,
"of",
Class.class,
String.class,
Argument[].class
)
);
private static final Method METHOD_CREATE_ARGUMENT_WITH_ANNOTATION_METADATA_GENERICS = Method.getMethod(
ReflectionUtils.getRequiredInternalMethod(
Argument.class,
"of",
Class.class,
String.class,
AnnotationMetadata.class,
Argument[].class
)
);
static {
NAME_TO_TYPE_MAP.put("void", "V");
NAME_TO_TYPE_MAP.put("boolean", "Z");
NAME_TO_TYPE_MAP.put("char", "C");
NAME_TO_TYPE_MAP.put("int", "I");
NAME_TO_TYPE_MAP.put("byte", "B");
NAME_TO_TYPE_MAP.put("long", "J");
NAME_TO_TYPE_MAP.put("double", "D");
NAME_TO_TYPE_MAP.put("float", "F");
}
/**
* Pushes type arguments onto the stack.
*
* @param generatorAdapter The generator adapter
* @param declaringElement The declaring class element of the generics
* @param types The type references
*/
protected static void pushTypeArgumentElements(
GeneratorAdapter generatorAdapter,
TypedElement declaringElement,
Map types) {
if (types == null || types.isEmpty()) {
generatorAdapter.visitInsn(ACONST_NULL);
return;
}
Set visitedTypes = new HashSet<>(5);
pushTypeArgumentElements(generatorAdapter, declaringElement, types, visitedTypes);
}
private static void pushTypeArgumentElements(
GeneratorAdapter generatorAdapter,
TypedElement declaringElement,
Map types,
Set visitedTypes) {
if (visitedTypes.contains(declaringElement.getName())) {
generatorAdapter.getStatic(
TYPE_ARGUMENT,
ZERO_ARGUMENTS_CONSTANT,
TYPE_ARGUMENT_ARRAY
);
} else {
visitedTypes.add(declaringElement.getName());
int len = types.size();
// Build calls to Argument.create(...)
pushNewArray(generatorAdapter, Argument.class, len);
int i = 0;
for (Map.Entry entry : types.entrySet()) {
// the array index
generatorAdapter.push(i);
String argumentName = entry.getKey();
ClassElement classElement = entry.getValue();
Object classReference = toClassReference(classElement);
Map typeArguments = null;
if (!classElement.getName().equals(declaringElement.getName())) {
typeArguments = classElement.getTypeArguments();
}
if (CollectionUtils.isNotEmpty(typeArguments)) {
buildArgumentWithGenerics(generatorAdapter, argumentName, classReference, classElement, typeArguments, visitedTypes);
} else {
buildArgument(generatorAdapter, argumentName, classReference);
}
// store the type reference
generatorAdapter.visitInsn(AASTORE);
// if we are not at the end of the array duplicate array onto the stack
if (i != (len - 1)) {
generatorAdapter.visitInsn(DUP);
}
i++;
}
}
}
private static Object toClassReference(ClassElement classElement) {
String n = classElement.getName();
Object classReference;
if (classElement.isPrimitive()) {
if (classElement.isArray()) {
classReference = ClassUtils.arrayTypeForPrimitive(n).map(t -> (Object) t).orElse(n);
} else {
classReference = ClassUtils.getPrimitiveType(n).map(t -> (Object) t).orElse(n);
}
} else {
if (classElement.isArray()) {
classReference = n + "[]";
} else {
classReference = n;
}
}
return classReference;
}
/**
* Pushes type arguments onto the stack.
*
* @param generatorAdapter The generator adapter
* @param types The type references
*/
protected static void pushTypeArguments(GeneratorAdapter generatorAdapter, Map types) {
if (types == null || types.isEmpty()) {
generatorAdapter.visitInsn(ACONST_NULL);
return;
}
int len = types.size();
// Build calls to Argument.create(...)
pushNewArray(generatorAdapter, Argument.class, len);
int i = 0;
for (Map.Entry entry : types.entrySet()) {
// the array index
generatorAdapter.push(i);
String typeParameterName = entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
buildArgumentWithGenerics(generatorAdapter, typeParameterName, (Map) value);
} else {
buildArgument(generatorAdapter, typeParameterName, value);
}
// store the type reference
generatorAdapter.visitInsn(AASTORE);
// if we are not at the end of the array duplicate array onto the stack
if (i != (len - 1)) {
generatorAdapter.visitInsn(DUP);
}
i++;
}
}
/**
* Builds an argument instance.
*
* @param generatorAdapter The generator adapter.
* @param argumentName The argument name
* @param objectType The object type
*/
protected static void buildArgument(GeneratorAdapter generatorAdapter, String argumentName, Object objectType) {
// 1st argument: the type
generatorAdapter.push(getTypeReference(objectType));
// 2nd argument: the name
generatorAdapter.push(argumentName);
// Argument.create( .. )
invokeInterfaceStaticMethod(
generatorAdapter,
Argument.class,
METHOD_CREATE_ARGUMENT_SIMPLE
);
}
/**
* Builds generic type arguments recursively.
* @param generatorAdapter The generator adapter to use
* @param argumentName The argument name
* @param typeReference The type name
* @param classElement The class element that declares the generics
* @param typeArguments The nested type arguments
* @param visitedTypes
*/
private static void buildArgumentWithGenerics(
GeneratorAdapter generatorAdapter, String argumentName,
Object typeReference,
ClassElement classElement,
Map typeArguments, Set visitedTypes) {
// 1st argument: the type
generatorAdapter.push(getTypeReference(typeReference));
// 2nd argument: the name
generatorAdapter.push(argumentName);
// 3rd argument, more generics
pushTypeArgumentElements(generatorAdapter, classElement, typeArguments, visitedTypes);
// Argument.create( .. )
invokeInterfaceStaticMethod(
generatorAdapter,
Argument.class,
METHOD_CREATE_ARGUMENT_WITH_GENERICS
);
}
/**
* This method should be replaced by the above method.
*
* @param generatorAdapter The {@link GeneratorAdapter}
* @param argumentName The argument name
* @param nestedTypeObject The nested type object
*/
static void buildArgumentWithGenerics(GeneratorAdapter generatorAdapter, String argumentName, Map nestedTypeObject) {
Map nestedTypes = null;
@SuppressWarnings("unchecked") Optional nestedEntry = nestedTypeObject.entrySet().stream().findFirst();
Object objectType;
if (nestedEntry.isPresent()) {
Map.Entry data = nestedEntry.get();
Object key = data.getKey();
Object map = data.getValue();
objectType = key;
if (map instanceof Map) {
nestedTypes = (Map) map;
}
} else {
throw new IllegalArgumentException("Must be a map with a single key containing the argument type and a map of generics as the value");
}
// 1st argument: the type
generatorAdapter.push(getTypeReference(objectType));
// 2nd argument: the name
generatorAdapter.push(argumentName);
// 3rd argument: generic types
boolean hasGenerics = nestedTypes != null && !nestedTypes.isEmpty();
if (hasGenerics) {
pushTypeArguments(generatorAdapter, nestedTypes);
}
// Argument.create( .. )
invokeInterfaceStaticMethod(
generatorAdapter,
Argument.class,
hasGenerics ? METHOD_CREATE_ARGUMENT_WITH_GENERICS : METHOD_CREATE_ARGUMENT_SIMPLE
);
}
/**
* @param owningType The owning type
* @param declaringClassWriter The declaring class writer
* @param generatorAdapter The {@link GeneratorAdapter}
* @param argumentTypes The argument types
* @param argumentAnnotationMetadata The argument annotation metadata
* @param genericTypes The generic types
* @param loadTypeMethods The load type methods
*/
protected static void pushBuildArgumentsForMethod(
Type owningType,
ClassWriter declaringClassWriter,
GeneratorAdapter generatorAdapter,
Map argumentTypes,
Map argumentAnnotationMetadata,
Map> genericTypes,
Map loadTypeMethods) {
int len = argumentTypes.size();
pushNewArray(generatorAdapter, Argument.class, len);
int i = 0;
for (Map.Entry entry : argumentTypes.entrySet()) {
// the array index position
generatorAdapter.push(i);
String argumentName = entry.getKey();
final Object value = entry.getValue();
Type argumentType;
if (value instanceof Map) {
// for generic types the info as passed with the class name being the key of the map
// bit of a hack and we need better data structures for this.
argumentType = getTypeReference(((Map) value).keySet().iterator().next());
} else {
argumentType = getTypeReference(value);
}
// 1st argument: The type
generatorAdapter.push(argumentType);
// 2nd argument: The argument name
generatorAdapter.push(argumentName);
// 3rd argument: The annotation metadata
AnnotationMetadata annotationMetadata = argumentAnnotationMetadata.get(argumentName);
if (annotationMetadata == null || annotationMetadata == AnnotationMetadata.EMPTY_METADATA) {
generatorAdapter.visitInsn(ACONST_NULL);
} else {
AnnotationMetadataWriter.instantiateNewMetadata(
owningType,
declaringClassWriter,
generatorAdapter,
(DefaultAnnotationMetadata) annotationMetadata,
loadTypeMethods
);
}
// 4th argument: The generic types
if (genericTypes != null && genericTypes.containsKey(argumentName)) {
Map types = genericTypes.get(argumentName);
pushTypeArguments(generatorAdapter, types);
} else {
generatorAdapter.visitInsn(ACONST_NULL);
}
// Argument.create( .. )
invokeInterfaceStaticMethod(
generatorAdapter,
Argument.class,
METHOD_CREATE_ARGUMENT_WITH_ANNOTATION_METADATA_GENERICS
);
// store the type reference
generatorAdapter.visitInsn(AASTORE);
// if we are not at the end of the array duplicate array onto the stack
if (i != (len - 1)) {
generatorAdapter.visitInsn(DUP);
}
i++;
}
}
/**
* Write the class to the target directory.
*
* @param targetDir The target directory
* @throws IOException if there is an error writing the file
*/
public void writeTo(File targetDir) throws IOException {
accept(newClassWriterOutputVisitor(targetDir));
}
/**
* Obtain the type for a given element.
*
* @param type The element type
* @return The type
*/
protected Type getTypeForElement(@NonNull TypedElement type) {
Type propertyType;
final Optional pt;
final String typeName = type.getName();
if (type.isPrimitive()) {
if (type.isArray()) {
pt = ClassUtils.arrayTypeForPrimitive(typeName);
} else {
pt = ClassUtils.getPrimitiveType(typeName);
}
} else {
pt = Optional.empty();
}
if (pt.isPresent()) {
propertyType = getTypeReference(pt.get());
} else {
if (type.isArray()) {
propertyType = getTypeReference(typeName + "[]");
} else {
propertyType = getTypeReference(typeName);
}
}
return propertyType;
}
/**
* Converts a map of class elements to type arguments.
*
* @param typeArguments The type arguments
* @return The type arguments
*/
@NotNull
protected Map toTypeArguments(@NotNull Map typeArguments) {
Set visitedTypes = new HashSet<>(5);
return toTypeArguments(typeArguments, visitedTypes);
}
@NotNull
private Map toTypeArguments(@NotNull Map typeArguments, Set visitedTypes) {
final LinkedHashMap map = new LinkedHashMap<>(typeArguments.size());
for (Map.Entry entry : typeArguments.entrySet()) {
final ClassElement ce = entry.getValue();
String className = ce.getName();
if (!visitedTypes.contains(entry.getKey())) {
visitedTypes.add(entry.getKey());
final Map subArgs = ce.getTypeArguments();
if (CollectionUtils.isNotEmpty(subArgs)) {
Map m = toTypeArguments(subArgs, visitedTypes);
if (CollectionUtils.isNotEmpty(m)) {
map.put(entry.getKey(), m);
} else {
map.put(entry.getKey(), Collections.singletonMap(entry.getKey(), className));
}
} else {
final Type typeReference = getTypeForElement(ce);
map.put(entry.getKey(), typeReference);
}
}
}
return map;
}
/**
* Converts a map of class elements to type arguments.
*
* @param parameters The parametesr
* @return The type arguments
*/
@NotNull
protected Map> toTypeArguments(ParameterElement... parameters) {
final LinkedHashMap> map = new LinkedHashMap<>(parameters.length);
for (ParameterElement ce : parameters) {
final ClassElement type = ce.getType();
final Map subArgs = type.getTypeArguments();
if (CollectionUtils.isNotEmpty(subArgs)) {
map.put(ce.getName(), toTypeArguments(subArgs));
}
}
return map;
}
/**
* Converts a parameters to type arguments.
*
* @param parameters The parameters
* @return The type arguments
*/
@NotNull
protected Map toParameterTypes(ParameterElement... parameters) {
final LinkedHashMap map = new LinkedHashMap<>(parameters.length);
for (ParameterElement ce : parameters) {
final ClassElement type = ce.getType();
if (type == null) {
continue;
}
final Type typeReference = getTypeForElement(type);
map.put(ce.getName(), typeReference);
}
return map;
}
/**
* Writes a method that returns a boolean value with the value supplied by the given supplier.
* @param classWriter The class writer
* @param methodName The method name
* @param valueSupplier The supplier
*/
protected void writeBooleanMethod(ClassWriter classWriter, String methodName, Supplier valueSupplier) {
GeneratorAdapter isSingletonMethod = startPublicMethodZeroArgs(
classWriter,
boolean.class,
methodName
);
isSingletonMethod.loadThis();
isSingletonMethod.push(valueSupplier.get());
isSingletonMethod.returnValue();
isSingletonMethod.visitMaxs(1, 1);
isSingletonMethod.visitEnd();
}
/**
* Accept a ClassWriterOutputVisitor to write this writer to disk.
*
* @param classWriterOutputVisitor The {@link ClassWriterOutputVisitor}
* @throws IOException if there is an error writing to disk
*/
public abstract void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException;
/**
* Returns the descriptor corresponding to the given class.
*
* @param type The type
* @return The descriptor for the class
*/
protected static String getTypeDescriptor(Object type) {
if (type instanceof Class) {
return Type.getDescriptor((Class) type);
} else if (type instanceof Type) {
return ((Type) type).getDescriptor();
} else {
String className = type.toString();
return getTypeDescriptor(className, new String[0]);
}
}
/**
* Returns the Type reference corresponding to the given class.
*
* @param className The class name
* @param genericTypes The generic types
* @return The {@link Type}
*/
protected static Type getTypeReferenceForName(String className, String... genericTypes) {
String referenceString = getTypeDescriptor(className, genericTypes);
return Type.getType(referenceString);
}
/**
* Return the type reference for a class.
*
* @param type The type
* @return The {@link Type}
*/
protected static Type getTypeReference(Object type) {
if (type instanceof Type) {
return (Type) type;
} else if (type instanceof Class) {
return Type.getType((Class) type);
} else if (type instanceof String) {
String className = type.toString();
String internalName = getInternalName(className);
if (className.endsWith("[]")) {
internalName = "[L" + internalName + ";";
}
return Type.getObjectType(internalName);
} else {
throw new IllegalArgumentException("Type reference [" + type + "] should be a Class or a String representing the class name");
}
}
/**
* @param fieldType The field type
* @param injectMethodVisitor The {@link MethodVisitor}
*/
protected static void pushBoxPrimitiveIfNecessary(Object fieldType, MethodVisitor injectMethodVisitor) {
if (fieldType instanceof Type) {
final Type t = (Type) fieldType;
final Optional pt = ClassUtils.getPrimitiveType(t.getClassName());
Class wrapperType = pt.map(ReflectionUtils::getWrapperType).orElse(null);
if (wrapperType != null) {
Type wrapper = Type.getType(wrapperType);
String primitiveName = t.getClassName();
String sig = wrapperType.getName() + " valueOf(" + primitiveName + ")";
org.objectweb.asm.commons.Method valueOfMethod = org.objectweb.asm.commons.Method.getMethod(sig);
injectMethodVisitor.visitMethodInsn(INVOKESTATIC, wrapper.getInternalName(), "valueOf", valueOfMethod.getDescriptor(), false);
}
} else {
Class wrapperType = AbstractClassFileWriter.getWrapperType(fieldType);
if (wrapperType != null) {
Class primitiveType = (Class) fieldType;
Type wrapper = Type.getType(wrapperType);
String primitiveName = primitiveType.getName();
String sig = wrapperType.getName() + " valueOf(" + primitiveName + ")";
org.objectweb.asm.commons.Method valueOfMethod = org.objectweb.asm.commons.Method.getMethod(sig);
injectMethodVisitor.visitMethodInsn(INVOKESTATIC, wrapper.getInternalName(), "valueOf", valueOfMethod.getDescriptor(), false);
}
}
}
/**
* @param methodVisitor The {@link MethodVisitor}
* @param type The type
*/
protected static void pushCastToType(MethodVisitor methodVisitor, Object type) {
String internalName = getInternalNameForCast(type);
methodVisitor.visitTypeInsn(CHECKCAST, internalName);
Type primitiveType = null;
if (type instanceof Class) {
Class typeClass = (Class) type;
if (typeClass.isPrimitive()) {
primitiveType = Type.getType(typeClass);
}
} else if (type instanceof Type) {
final Optional pt = ClassUtils.getPrimitiveType(((Type) type).getClassName());
if (pt.isPresent()) {
primitiveType = Type.getType(pt.get());
}
}
if (primitiveType != null) {
org.objectweb.asm.commons.Method valueMethod = null;
switch (primitiveType.getSort()) {
case Type.BOOLEAN:
valueMethod = org.objectweb.asm.commons.Method.getMethod("boolean booleanValue()");
break;
case Type.CHAR:
valueMethod = org.objectweb.asm.commons.Method.getMethod("char charValue()");
break;
case Type.BYTE:
valueMethod = org.objectweb.asm.commons.Method.getMethod("byte byteValue()");
break;
case Type.SHORT:
valueMethod = org.objectweb.asm.commons.Method.getMethod("short shortValue()");
break;
case Type.INT:
valueMethod = org.objectweb.asm.commons.Method.getMethod("int intValue()");
break;
case Type.LONG:
valueMethod = org.objectweb.asm.commons.Method.getMethod("long longValue()");
break;
case Type.DOUBLE:
valueMethod = org.objectweb.asm.commons.Method.getMethod("double doubleValue()");
break;
case Type.FLOAT:
valueMethod = org.objectweb.asm.commons.Method.getMethod("float floatValue()");
break;
default:
// no-ip
}
if (valueMethod != null) {
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, internalName, valueMethod.getName(), valueMethod.getDescriptor(), false);
}
}
}
/**
* @param methodVisitor The {@link MethodVisitor}
* @param type The type
*/
protected static void pushReturnValue(MethodVisitor methodVisitor, Object type) {
if (type instanceof Class) {
Class typeClass = (Class) type;
if (typeClass.isPrimitive()) {
Type primitiveType = Type.getType(typeClass);
switch (primitiveType.getSort()) {
case Type.BOOLEAN:
case Type.INT:
case Type.CHAR:
case Type.BYTE:
case Type.SHORT:
methodVisitor.visitInsn(IRETURN);
break;
case Type.VOID:
methodVisitor.visitInsn(RETURN);
break;
case Type.LONG:
methodVisitor.visitInsn(LRETURN);
break;
case Type.DOUBLE:
methodVisitor.visitInsn(DRETURN);
break;
case Type.FLOAT:
methodVisitor.visitInsn(FRETURN);
break;
default:
//no-op
}
} else {
methodVisitor.visitInsn(ARETURN);
}
} else {
methodVisitor.visitInsn(ARETURN);
}
}
/**
* @param type The type
* @return The class
*/
protected static Class getWrapperType(Object type) {
if (isPrimitive(type)) {
return ReflectionUtils.getWrapperType((Class) type);
}
return null;
}
/**
* @param type The type
* @return Whether a type is primitive
*/
protected static boolean isPrimitive(Object type) {
if (type instanceof Class) {
Class typeClass = (Class) type;
return typeClass.isPrimitive();
}
return false;
}
/**
* @param methodVisitor The method visitor as {@link GeneratorAdapter}
* @param methodName The method name
* @param argumentTypes The argument types
*/
protected static void pushMethodNameAndTypesArguments(GeneratorAdapter methodVisitor, String methodName, Collection
© 2015 - 2025 Weber Informatics LLC | Privacy Policy