Skip to content

Commit

Permalink
Issue #296: avoided reifying type arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
raner committed Nov 17, 2024
1 parent e545a6d commit 429496e
Show file tree
Hide file tree
Showing 10 changed files with 520 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,17 @@
import net.bytebuddy.dynamic.DynamicType.Builder.FieldDefinition.Valuable;
import net.bytebuddy.dynamic.DynamicType.Builder.MethodDefinition.ImplementationDefinition;
import net.bytebuddy.dynamic.DynamicType.Loaded;
import net.bytebuddy.dynamic.scaffold.MethodGraph.Compiler;
import net.bytebuddy.dynamic.scaffold.MethodGraph.Compiler.Default;
import net.bytebuddy.dynamic.scaffold.MethodGraph.Compiler.Default.Harmonizer;
import net.bytebuddy.dynamic.scaffold.MethodGraph.Compiler.Default.Merger;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.implementation.DefaultMethodCall;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.Implementation.Composable;
import net.bytebuddy.implementation.MethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaConstant;
import pro.projo.Projo;
import pro.projo.annotations.Cached;
Expand All @@ -76,6 +81,8 @@
import pro.projo.internal.rcg.runtime.ToStringValueObject;
import pro.projo.internal.rcg.runtime.ValueObject;
import pro.projo.internal.rcg.utilities.GenericTypeResolver;
import pro.projo.internal.rcg.utilities.GenericVisitor;
import pro.projo.internal.rcg.utilities.RawGenericLoadedType;
import pro.projo.internal.rcg.utilities.UncheckedMethodDescription;
import pro.projo.utilities.AnnotationList;
import pro.projo.utilities.MethodInfo;
Expand Down Expand Up @@ -496,7 +503,21 @@ private MethodDescription.Latent latent(TypeDescription declaringType, Generic r

private ByteBuddy codeGenerator()
{
return new ByteBuddy(JAVA_V8).with(TypeValidation.DISABLED);
return codeGenerator(false);
}

private ByteBuddy codeGenerator(boolean requiresGenericVisitor)
{
Compiler compiler = !requiresGenericVisitor? Default.forJavaHierarchy():new Default<>
(
Harmonizer.ForJavaMethod.INSTANCE,
Merger.Directional.LEFT,
new GenericVisitor(),
ElementMatchers.any()
);
return new ByteBuddy(JAVA_V8)
.with(TypeValidation.DISABLED)
.with(compiler);
}

/**
Expand Down Expand Up @@ -607,9 +628,15 @@ private Class<?> baseclass(Class<_Artifact_> type)
private Builder<_Artifact_> create(Class<_Artifact_> type, List<String> additionalImplements, ClassLoader classLoader)
{
Stream<TypeDefinition> baseTypes = Stream.of(type(type), type(ProjoObject.class));
TypeDefinition[] interfaces = Stream.concat(baseTypes, additionalImplements.stream().map(it -> type(it, classLoader))).toArray(TypeDefinition[]::new);
Generic[] interfaces = Stream
.concat(baseTypes, additionalImplements.stream().map(it -> type(it, classLoader)))
.toArray(Generic[]::new);
Stream<String> annotationNames = Stream.of(interfaces)
.flatMap(it -> it.getDeclaredAnnotations().asTypeNames().stream());
boolean requiresGenericVisitor = GenericVisitor.isNecessaryFor(annotationNames);
@SuppressWarnings("unchecked")
Builder<_Artifact_> builder = (Builder<_Artifact_>)codeGenerator().subclass(baseclass(type)).implement(interfaces);
Builder<_Artifact_> builder = (Builder<_Artifact_>)codeGenerator(requiresGenericVisitor)
.subclass(baseclass(type)).implement(interfaces);
return builder;
}

Expand All @@ -633,12 +660,12 @@ private Class<?> classOf(Type type)
throw new UnsupportedOperationException(type + " (" + type.getClass() + ")");
}

private TypeDefinition type(Class<?> type)
private Generic type(Class<?> type)
{
return new TypeDescription.ForLoadedType(type);
return new RawGenericLoadedType(type);
}

private TypeDefinition type(String typeName, ClassLoader classLoader)
private Generic type(String typeName, ClassLoader classLoader)
{
Matcher matcher = typeNamePattern.matcher(typeName);
matcher.matches();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// //
// Copyright 2024 Mirko Raner //
// //
// 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 pro.projo.internal.rcg.utilities;

import java.util.stream.Stream;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.description.type.TypeDescription.Generic;
import net.bytebuddy.description.type.TypeDescription.Generic.Visitor;
import net.bytebuddy.description.type.TypeList;
import pro.projo.annotations.RawInterfaces;
import static java.util.stream.Collectors.toList;

/**
* {@link GenericVisitor} implements a variation of {@link Visitor.Reifying#INITIATING}
* that will strip type arguments from all interfaces extended by an interface that is
* annotated with {@link RawInterfaces} (effectively causing the interface to extend the
* corresponding raw types).
*
* @author Mirko Raner
**/
public class GenericVisitor implements Visitor<Generic>
{
private final Visitor<Generic> delegate = Generic.Visitor.Reifying.INITIATING;

/**
* Determines if use of the {@link GenericVisitor} is necessary.
*
* @param annotationNames the type names of the annotations present
* @return {@code true} if the annotation names include {@link RawInterfaces},
* otherwise {@code false}
**/
public static boolean isNecessaryFor(Stream<String> annotationNames)
{
return annotationNames.anyMatch(RawInterfaces.class.getName()::equals);
}

@Override
public Generic onGenericArray(Generic genericArray)
{
return delegate.onGenericArray(genericArray);
}

@Override
public Generic onWildcard(Generic wildcard)
{
return delegate.onWildcard(wildcard);
}

@Override
public Generic onParameterizedType(Generic parameterizedType)
{
return delegate.onParameterizedType(parameterizedType);
}

@Override
public Generic onTypeVariable(Generic typeVariable)
{
return delegate.onTypeVariable(typeVariable);
}

@Override
public Generic onNonGenericType(Generic typeDescription)
{
// NOTE: typeDescription.getDeclaredAnnotations() will typically not return any
// annotations for Generics corresponding to loaded types because
// Generic.OfNonGenericType.ForLoadedType uses AnnotationReader.NoOp.INSTANCE.
// This method will only work with custom Generics that retain annotations.
//
AnnotationList annotations = typeDescription.getDeclaredAnnotations();
if (typeDescription instanceof RawGenericLoadedType
&& (isNecessaryFor(annotations.asTypeNames().stream())))
{
// Even though the type name is known from typeDescription.getActualName()
// simply regenerating the Class via Class.forName(...) is tricky and prone
// to failure because the correct class loader is not known. Unless, the
// current class loader was also used to originally load the Class in question
// the class may not be found in the current class loader, or, even worse,
// a duplicate class is loaded. Therefore, the only way to obtain the Class
// reliably is when the Generic type provided is a RawGenericLoadedType that
// provides access to the encapsulated Class object:
//
Class<?> type = ((RawGenericLoadedType)typeDescription).getType();
return new Generic.OfNonGenericType.ForLoadedType(type)
{
@Override
public TypeList.Generic getInterfaces()
{
Stream<Class<?>> interfaces = Stream.of(type.getInterfaces());
Stream<TypeDescription.ForLoadedType> rawInterfaces = interfaces.map
(
it -> new TypeDescription.ForLoadedType(it)
{
private final static long serialVersionUID = 1L;

@Override
public TypeList.Generic getTypeVariables()
{
return new TypeList.Generic.Empty();
}

@Override
public boolean isGenerified()
{
return false;
}
}
);
return new TypeList.Generic.Explicit(rawInterfaces.collect(toList()));
}
};
}
return delegate.onNonGenericType(typeDescription);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// //
// Copyright 2024 Mirko Raner //
// //
// 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 pro.projo.internal.rcg.utilities;

import net.bytebuddy.description.type.TypeDescription.Generic;

/**
* {@link RawGenericLoadedType} is a customization of
* {@link Generic.OfNonGenericType.ForLoadedType} that provides the following
* additional features:
* <ul>
* <li>it preserves annotations declared by the wrapped {@link Class}</li>
* <li>it provides access to the original wrapped {@link Class} object</li>
* </ul>
* @author Mirko Raner
**/
public class RawGenericLoadedType extends Generic.OfNonGenericType.ForLoadedType
{
private final Class<?> type;

public RawGenericLoadedType(Class<?> type)
{
super(type, new AnnotationReader.Delegator.Simple(type));
this.type = type;
}

public Class<?> getType()
{
return type;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// //
// Copyright 2024 Mirko Raner //
// //
// 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 pro.projo.integration.tests;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.TypeLiteral;
import pro.projo.Projo;
import pro.projo.test.implementations.Ranges;
import pro.projo.test.implementations.Utilities;
import pro.projo.test.interfaces.Decimals;
import pro.projo.test.interfaces.Literals;
import pro.projo.test.interfaces.Provider;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class ProjoDecimalsIT
{
@BeforeClass
public static void deleteDecimalClass() throws URISyntaxException
{
ClassLoader classLoader = ProjoDecimalsIT.class.getClassLoader();
URL url = classLoader.getResource("pro/projo/test/interfaces/Decimal.class");
if (url != null)
{
new File(url.toURI()).delete();
}
}

@Test
public void implementationOfDecimalsIsProperlyBound() throws Exception
{
Literals literals = new Literals() {};
Injector injector = Guice.createInjector(module(literals));
Decimals<?> naturals = injector.getInstance(Decimals.class);
Method literalsMethod = Provider.class.getMethod("literals");
Literals result = (Literals)literalsMethod.invoke(naturals);
assertEquals(literals, result);
}

@Test
public void interfaceMethodsAreProperlyInvoked() throws Exception
{
Injector injector = Guice.createInjector(module(new Literals() {}));
Decimals<?> decimals = injector.getInstance(Decimals.class);
Method parseMethod = Provider.class.getMethod("parse", Object.class);
parseMethod.invoke(decimals, "");
assertTrue(Utilities.nullValueCalled);
}

private Module module(Literals literals)
{
return new AbstractModule()
{
@Override
@SuppressWarnings("unchecked")
protected void configure()
{
Class<?> classDecimals = (Class<?>)Decimals.class;
TypeLiteral<Object> interfaceDecimals = TypeLiteral.get((Class<Object>)classDecimals);
Class<?> implementationNaturals = Projo.getImplementationClass(pro.projo.test.implementations.Decimals.class);
TypeLiteral<Object> implementation = TypeLiteral.get((Class<Object>)implementationNaturals);
bind(interfaceDecimals).to(implementation).asEagerSingleton();
bind(Literals.class).toInstance(literals);
bind(Ranges.class).toInstance(new Ranges() {});
}
};
}
}
Loading

0 comments on commit 429496e

Please sign in to comment.