Skip to content

Commit

Permalink
Build and store generic type metadata for beans / improve functions
Browse files Browse the repository at this point in the history
  • Loading branch information
graemerocher committed Aug 1, 2018
1 parent cc19bab commit 10e1a23
Show file tree
Hide file tree
Showing 39 changed files with 1,433 additions and 97 deletions.
5 changes: 5 additions & 0 deletions aop/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,11 @@ public void setRequiresMethodProcessing(boolean shouldPreProcess) {
proxyBeanDefinitionWriter.setRequiresMethodProcessing(shouldPreProcess);
}

@Override
public void visitTypeArguments(Map<String, Map<String, Object>> typeArguments) {
proxyBeanDefinitionWriter.visitTypeArguments(typeArguments);
}

@Override
public boolean requiresMethodProcessing() {
return proxyBeanDefinitionWriter.requiresMethodProcessing();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public static Optional<Class> resolveInterfaceTypeArgument(Class type, Class int
* @param genericType The generic type
* @return An {@link Optional} of the type
*/
private static Optional<Class> resolveSingleTypeArgument(Type genericType) {
private static Optional<Class> resolveSingleTypeArgument(Type genericType) {
if (genericType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) genericType;
Type[] actualTypeArguments = pt.getActualTypeArguments();
Expand Down
15 changes: 1 addition & 14 deletions core/src/main/java/io/micronaut/core/util/CollectionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,7 @@

import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.*;

/**
* <p>Utility methods for working with {@link java.util.Collection} types</p>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.discovery.ServiceInstance;
import io.micronaut.discovery.metadata.ServiceInstanceMetadataContributor;
Expand All @@ -39,10 +40,7 @@

import javax.inject.Singleton;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -92,26 +90,68 @@ public void process(BeanDefinition<?> beanDefinition, ExecutableMethod<?, ?> met
String methodName = method.getMethodName();
Class<?> declaringType = method.getDeclaringType();
String functionName = beanDefinition.getValue(FunctionBean.class, String.class).orElse(methodName);
String functionMethod = beanDefinition.getValue(FunctionBean.class, "method", String.class).orElse(null);

UriRoute route = null;
if (Stream.of(java.util.function.Function.class, Consumer.class, BiFunction.class, BiConsumer.class).anyMatch(type -> type.isAssignableFrom(declaringType))) {
if (methodName.equals("accept") || methodName.equals("apply") || methodName.equals(functionMethod)) {
String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
String[] argumentNames = method.getArgumentNames();
String argumentName = argumentNames[0];
int argCount = argumentNames.length;

String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
route = POST(functionPath, method);
route = POST(functionPath, beanDefinition, method);
if (argCount == 1) {
route.body(argumentName);
}

List<Argument<?>> typeArguments = beanDefinition.getTypeArguments();
if (!typeArguments.isEmpty()) {
int size = typeArguments.size();

Argument<?> firstArgument = typeArguments.get(0);
if (size < 3 && ClassUtils.isJavaLangType(firstArgument.getType())) {
route.consumes(MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE);
}

if (size < 3) {
route.body(Argument.of(firstArgument.getType(), argumentName));
}

if (size > 1) {
Argument<?> argument = typeArguments.get(size == 3 ? 2 : 1);
if (ClassUtils.isJavaLangType(argument.getType())) {
route.produces(MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE);
}
}
}
else {
if (argCount == 1 && ClassUtils.isJavaLangType(method.getArgumentTypes()[0])) {
route.consumes(MediaType.TEXT_PLAIN_TYPE, MediaType.APPLICATION_JSON_TYPE);
}
}
}
} else if (Supplier.class.isAssignableFrom(declaringType) && methodName.equals("get")) {
String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
route = GET(functionPath, method);
route = GET(functionPath, beanDefinition, method);
} else {
String functionMethod = beanDefinition.getValue(FunctionBean.class, "method", String.class).orElse(null);
if (StringUtils.isNotEmpty(functionMethod)) {
if (functionMethod.equals(methodName)) {
int argCount = method.getArguments().length;
Argument[] argumentTypes = method.getArguments();
int argCount = argumentTypes.length;
if (argCount < 3) {
String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
if (argCount == 0) {
route = GET(functionPath, method);
route = GET(functionPath, beanDefinition, method);

} else {
route = POST(functionPath, method);
route = POST(functionPath, beanDefinition, method);
if (argCount == 2 || !ClassUtils.isJavaLangType(argumentTypes[0].getType())) {
route.consumes(MediaType.APPLICATION_JSON_TYPE);
} else {
route.body(method.getArgumentNames()[0])
.acceptAll();
}
}
}
}
Expand All @@ -125,16 +165,7 @@ public void process(BeanDefinition<?> beanDefinition, ExecutableMethod<?, ?> met

String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
availableFunctions.put(functionName, URI.create(functionPath));
Class[] argumentTypes = method.getArgumentTypes();
int argCount = argumentTypes.length;
if (argCount > 0) {
if (argCount == 2 || !ClassUtils.isJavaLangType(argumentTypes[0])) {
route.consumes(MediaType.APPLICATION_JSON_TYPE);
} else {
route.body(method.getArgumentNames()[0])
.acceptAll();
}
}

((ExecutableMethodProcessor) localFunctionRegistry).process(beanDefinition, method);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package io.micronaut.function.web

import groovy.transform.NotYetImplemented
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification

class JavaLambdaFunctionSpec extends Specification {

@NotYetImplemented
void "test string supplier"() {
given:
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
Expand All @@ -21,7 +22,58 @@ class JavaLambdaFunctionSpec extends Specification {

then:
response.code() == HttpStatus.OK.code
response.body() == 'value'
response.body() == 'myvalue'

cleanup:
embeddedServer.stop()
}


void "test func primitive"() {
given:
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

when:
HttpResponse<Long> response = client.toBlocking().exchange(HttpRequest.POST('/java/function/round', '10.2')
.contentType(MediaType.TEXT_PLAIN_TYPE), Long)

then:
response.code() == HttpStatus.OK.code
response.body() == 10

cleanup:
embeddedServer.stop()
}

void "test func pojo"() {
given:
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

when:
HttpResponse<TestFunctionFactory.Name> response = client.toBlocking().exchange(HttpRequest.POST('/java/function/upper', new TestFunctionFactory.Name(name: "fred")), TestFunctionFactory.Name)

then:
response.code() == HttpStatus.OK.code
response.body().name == "FRED"

cleanup:
embeddedServer.stop()
}

@NotYetImplemented // Not sure if support for BiFunctions makes sense for Java
void "test bi func pojo"() {
given:
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

when:
HttpResponse<TestFunctionFactory.Name> response = client.toBlocking().exchange(HttpRequest.POST('/java/function/fullname','{"arg0":"Fred", "arg1":"Flintstone"}'), TestFunctionFactory.Name)

then:
response.code() == HttpStatus.OK.code
response.body().name == "Fred Flinstone"

cleanup:
embeddedServer.stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io.micronaut.context.annotation.Factory;
import io.micronaut.function.FunctionBean;

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

@Factory
Expand All @@ -16,4 +18,40 @@ public class TestFunctionFactory {
Supplier<String> get() {
return () -> "myvalue";
}

// This should work but is currently not implemented
// the reason is because when @FunctionBean is defined on a factory
// we do not go through and visit public Executable methods unless
// it is an AOP proxy
@FunctionBean("java/function/round")
Function<Double, Long> round() {
return (doub) -> Math.round(doub.doubleValue());
}

@FunctionBean("java/function/upper")
Function<Name, Name> upper() {
return (n) -> n.setName(n.getName().toUpperCase());
}

@FunctionBean("java/function/fullname")
BiFunction<String, String, Name> fullname() {
return (s, s2) -> {
Name name = new Name();
name.setName(s + " " + s2);
return name;
};
}

static class Name {
private String name;

public String getName() {
return name;
}

public Name setName(String name) {
this.name = name;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.context.annotation.Executable;

import javax.inject.Named;
import javax.inject.Singleton;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
Expand All @@ -45,12 +46,14 @@
* @return An optional ID of the function which may or may not be used depending on the target platform
*/
@AliasFor(member = "name")
@AliasFor(annotation = Named.class, member = "value")
String value() default "";

/**
* @return An optional ID of the function which may or may not be used depending on the target platform
*/
@AliasFor(member = "value")
@AliasFor(annotation = Named.class, member = "value")
String name() default "";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.micronaut.function.executor;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.Qualifier;
import io.micronaut.context.env.Environment;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
Expand All @@ -32,13 +33,16 @@
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.codec.MediaTypeCodec;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.qualifiers.Qualifiers;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;

/**
Expand Down Expand Up @@ -82,7 +86,11 @@ protected void execute(InputStream input, OutputStream output, C context) throws
Argument[] requiredArguments = method.getArguments();
int argCount = requiredArguments.length;
Object result;
Object bean = applicationContext.getBean(method.getDeclaringType());
Qualifier<Object> qualifier = Qualifiers.byName(functionName);
Class<Object> functionType = method.getDeclaringType();
BeanDefinition<Object> beanDefinition = applicationContext.getBeanDefinition(functionType, qualifier);
Object bean = applicationContext.getBean(functionType, qualifier);
List<Argument<?>> typeArguments = beanDefinition.getTypeArguments();

switch (argCount) {
case 0:
Expand All @@ -91,12 +99,20 @@ protected void execute(InputStream input, OutputStream output, C context) throws
case 1:

Argument arg = requiredArguments[0];
if (!typeArguments.isEmpty()) {
arg = Argument.of(typeArguments.get(0).getType(), arg.getName());
}
Object value = decodeInputArgument(env, localFunctionRegistry, arg, input);
result = method.invoke(bean, value);
break;
case 2:
Argument firstArgument = requiredArguments[0];
Argument secondArgument = requiredArguments[1];

if (!typeArguments.isEmpty()) {
firstArgument = Argument.of(typeArguments.get(0).getType(), firstArgument.getName());
}

Object first = decodeInputArgument(env, localFunctionRegistry, firstArgument, input);
Object second = decodeContext(env, secondArgument, context);
result = method.invoke(bean, first, second);
Expand Down Expand Up @@ -129,7 +145,7 @@ static void encode(Environment environment, LocalFunctionRegistry registry, Clas
output.write((byte[]) result);
} else {
byte[] bytes = environment
.convert(result, byte[].class)
.convert(result.toString(), byte[].class)
.orElseThrow(() -> new InvocationException("Unable to convert result [" + result + "] for output stream"));
output.write(bytes);
}
Expand Down
Loading

0 comments on commit 10e1a23

Please sign in to comment.