Support HTTP GET of single value value Function

E.g. a Function<Long, Foo> can be used to fetch a single entity of
type Foo with a long ID., via /{function}/{id}
This commit is contained in:
Dave Syer
2017-03-10 16:33:34 +00:00
parent 17b644f563
commit 2ee97721ab
3 changed files with 76 additions and 25 deletions

View File

@@ -27,6 +27,8 @@ import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
@@ -47,8 +49,6 @@ import org.springframework.core.type.StandardMethodMetadata;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
/** /**
@@ -70,9 +70,9 @@ public class ContextFunctionCatalogAutoConfiguration {
private Map<String, Consumer<?>> consumers = Collections.emptyMap(); private Map<String, Consumer<?>> consumers = Collections.emptyMap();
@Bean @Bean
public FunctionCatalog functionCatalog(ContextFunctionPostProcessor processor, ObjectMapper mapper) { public FunctionCatalog functionCatalog(ContextFunctionPostProcessor processor,
return new InMemoryFunctionCatalog( ObjectMapper mapper) {
processor.wrapSuppliers(mapper, suppliers), return new InMemoryFunctionCatalog(processor.wrapSuppliers(mapper, suppliers),
processor.wrapFunctions(mapper, functions), processor.wrapFunctions(mapper, functions),
processor.wrapConsumers(mapper, consumers)); processor.wrapConsumers(mapper, consumers));
} }
@@ -99,7 +99,7 @@ public class ContextFunctionCatalogAutoConfiguration {
if (this.suppliers.contains(key)) { if (this.suppliers.contains(key)) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Supplier<Flux<?>> supplier = (Supplier<Flux<?>>) suppliers.get(key); Supplier<Flux<?>> supplier = (Supplier<Flux<?>>) suppliers.get(key);
result.put(key, wrapSupplier(supplier, mapper)); result.put(key, wrapSupplier(supplier, mapper, key));
} }
else { else {
result.put(key, suppliers.get(key)); result.put(key, suppliers.get(key));
@@ -119,7 +119,9 @@ public class ContextFunctionCatalogAutoConfiguration {
result.put(key, wrapFunction(function, mapper, key)); result.put(key, wrapFunction(function, mapper, key));
} }
else if (!isFluxFunction(key, functions.get(key))) { else if (!isFluxFunction(key, functions.get(key))) {
result.put(key, new FluxFunction(functions.get(key))); @SuppressWarnings({ "unchecked", "rawtypes" })
FluxFunction value = new FluxFunction(functions.get(key));
result.put(key, value);
} }
else { else {
result.put(key, functions.get(key)); result.put(key, functions.get(key));
@@ -166,12 +168,16 @@ public class ContextFunctionCatalogAutoConfiguration {
Object source = beanDefinition.getSource(); Object source = beanDefinition.getSource();
if (source instanceof StandardMethodMetadata) { if (source instanceof StandardMethodMetadata) {
StandardMethodMetadata metadata = (StandardMethodMetadata) source; StandardMethodMetadata metadata = (StandardMethodMetadata) source;
Type returnType = metadata.getIntrospectedMethod().getGenericReturnType(); Type returnType = metadata.getIntrospectedMethod()
.getGenericReturnType();
if (returnType instanceof ParameterizedType) { if (returnType instanceof ParameterizedType) {
Type[] types = ((ParameterizedType) returnType).getActualTypeArguments(); Type[] types = ((ParameterizedType) returnType)
.getActualTypeArguments();
if (types != null && types.length == 2) { if (types != null && types.length == 2) {
return (types[0].getTypeName().startsWith(Flux.class.getName()) return (types[0].getTypeName()
&& types[1].getTypeName().startsWith(Flux.class.getName())); .startsWith(Flux.class.getName())
&& types[1].getTypeName()
.startsWith(Flux.class.getName()));
} }
} }
} }
@@ -213,9 +219,10 @@ public class ContextFunctionCatalogAutoConfiguration {
} }
private ProxySupplier wrapSupplier(Supplier<Flux<?>> supplier, private ProxySupplier wrapSupplier(Supplier<Flux<?>> supplier,
ObjectMapper mapper) { ObjectMapper mapper, String name) {
ProxySupplier wrapped = new ProxySupplier(mapper); ProxySupplier wrapped = new ProxySupplier(mapper);
wrapped.setDelegate(supplier); wrapped.setDelegate(supplier);
wrapped.setOutputType(findType(name));
return wrapped; return wrapped;
} }
@@ -223,7 +230,8 @@ public class ContextFunctionCatalogAutoConfiguration {
ObjectMapper mapper, String name) { ObjectMapper mapper, String name) {
ProxyFunction wrapped = new ProxyFunction(mapper); ProxyFunction wrapped = new ProxyFunction(mapper);
wrapped.setDelegate(function); wrapped.setDelegate(function);
wrapped.setType(findType(name)); wrapped.setInputType(findType(name));
wrapped.setOutputType(findOutputType(name));
return wrapped; return wrapped;
} }
@@ -231,16 +239,16 @@ public class ContextFunctionCatalogAutoConfiguration {
ObjectMapper mapper, String name) { ObjectMapper mapper, String name) {
ProxyConsumer wrapped = new ProxyConsumer(mapper); ProxyConsumer wrapped = new ProxyConsumer(mapper);
wrapped.setDelegate(consumer); wrapped.setDelegate(consumer);
wrapped.setType(findType(name)); wrapped.setInputType(findType(name));
return wrapped; return wrapped;
} }
private Class<?> findType(RootBeanDefinition definition) { private Class<?> findType(RootBeanDefinition definition, int index) {
StandardMethodMetadata source = (StandardMethodMetadata) definition StandardMethodMetadata source = (StandardMethodMetadata) definition
.getSource(); .getSource();
ParameterizedType type = (ParameterizedType) (source.getIntrospectedMethod() ParameterizedType type = (ParameterizedType) (source.getIntrospectedMethod()
.getGenericReturnType()); .getGenericReturnType());
type = (ParameterizedType) type.getActualTypeArguments()[0]; type = (ParameterizedType) type.getActualTypeArguments()[index];
Type param = type.getActualTypeArguments()[0]; Type param = type.getActualTypeArguments()[0];
if (param instanceof ParameterizedType) { if (param instanceof ParameterizedType) {
ParameterizedType concrete = (ParameterizedType) param; ParameterizedType concrete = (ParameterizedType) param;
@@ -251,7 +259,11 @@ public class ContextFunctionCatalogAutoConfiguration {
} }
private Class<?> findType(String name) { private Class<?> findType(String name) {
return findType((RootBeanDefinition) registry.getBeanDefinition(name)); return findType((RootBeanDefinition) registry.getBeanDefinition(name), 0);
}
private Class<?> findOutputType(String name) {
return findType((RootBeanDefinition) registry.getBeanDefinition(name), 1);
} }
} }
@@ -263,7 +275,9 @@ abstract class ProxyWrapper<T> {
private T delegate; private T delegate;
private Class<?> type; private Class<?> inputType;
private Class<?> outputType;
public ProxyWrapper(ObjectMapper mapper) { public ProxyWrapper(ObjectMapper mapper) {
this.mapper = mapper; this.mapper = mapper;
@@ -277,20 +291,28 @@ abstract class ProxyWrapper<T> {
return delegate; return delegate;
} }
public void setType(Class<?> type) { public void setInputType(Class<?> inputType) {
this.type = type; this.inputType = inputType;
} }
public Class<?> getType() { public void setOutputType(Class<?> outputType) {
return this.type; this.outputType = outputType;
}
public Class<?> getInputType() {
return this.inputType;
}
public Class<?> getOutputType() {
return outputType;
} }
public Object fromJson(String value) { public Object fromJson(String value) {
if (getType().equals(String.class)) { if (getInputType().equals(String.class)) {
return value; return value;
} }
try { try {
return mapper.readValue(value, getType()); return mapper.readValue(value, getInputType());
} }
catch (Exception e) { catch (Exception e) {
throw new IllegalStateException("Cannot convert from JSON: " + value); throw new IllegalStateException("Cannot convert from JSON: " + value);
@@ -298,6 +320,9 @@ abstract class ProxyWrapper<T> {
} }
public String toJson(Object value) { public String toJson(Object value) {
if (String.class.equals(getOutputType()) && value instanceof String) {
return (String) value;
}
try { try {
return mapper.writeValueAsString(value); return mapper.writeValueAsString(value);
} }
@@ -308,7 +333,7 @@ abstract class ProxyWrapper<T> {
@Override @Override
public String toString() { public String toString() {
return "ProxyWrapper [delegate=" + delegate + ", type=" + type + "]"; return "ProxyWrapper [delegate=" + delegate + ", inputType=" + inputType + "]";
} }
} }

View File

@@ -79,4 +79,15 @@ public class FunctionController {
Flux<String> result = (Flux<String>) supplier.get(); Flux<String> result = (Flux<String>) supplier.get();
return debug ? result.log() : result; return debug ? result.log() : result;
} }
@GetMapping(path = "/{name}/{value}")
public Flux<String> value(@PathVariable String name, @PathVariable String value) {
Function<Flux<?>, Flux<?>> function = functions.lookupFunction(name);
if (function != null) {
@SuppressWarnings({ "unchecked" })
Flux<String> result = (Flux<String>) function.apply(Flux.just(value));
return debug ? result.log() : result;
}
throw new IllegalArgumentException("no such function: " + name);
}
} }

View File

@@ -150,6 +150,16 @@ public class RestApplicationTests {
.isEqualTo("[FOO][BAR]"); .isEqualTo("[FOO][BAR]");
} }
@Test
public void uppercaseGet() {
assertThat(rest.getForObject("/uppercase/foo", String.class)).isEqualTo("[FOO]");
}
@Test
public void convertGet() {
assertThat(rest.getForObject("/wrap/123", String.class)).isEqualTo("..123..");
}
@Test @Test
public void uppercaseJsonArray() throws Exception { public void uppercaseJsonArray() throws Exception {
assertThat(rest.exchange( assertThat(rest.exchange(
@@ -193,6 +203,11 @@ public class RestApplicationTests {
.map(value -> "[" + value.trim().toUpperCase() + "]"); .map(value -> "[" + value.trim().toUpperCase() + "]");
} }
@Bean
public Function<Flux<Integer>, Flux<String>> wrap() {
return flux -> flux.log().map(value -> ".." + value + "..");
}
@Bean @Bean
public Function<Flux<HashMap<String, String>>, Flux<Map<String, String>>> maps() { public Function<Flux<HashMap<String, String>>, Flux<Map<String, String>>> maps() {
return flux -> flux.map(value -> { return flux -> flux.map(value -> {