diff --git a/pom.xml b/pom.xml
index eb71b1df5..d936aa831 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,7 @@
spring-cloud-function-context
spring-cloud-function-stream
spring-cloud-function-task
+ spring-cloud-function-web
spring-cloud-function-samples
spring-cloud-function-deployer
spring-cloud-function-adapters
diff --git a/spring-cloud-function-dependencies/pom.xml b/spring-cloud-function-dependencies/pom.xml
index 414ba1958..ad6010252 100644
--- a/spring-cloud-function-dependencies/pom.xml
+++ b/spring-cloud-function-dependencies/pom.xml
@@ -70,6 +70,11 @@
spring-cloud-function-adapter-openwhisk
${project.version}
+
+ org.springframework.cloud
+ spring-cloud-stream-binder-servlet
+ ${project.version}
+
diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml
index a5492101e..e5e25c304 100644
--- a/spring-cloud-function-deployer/pom.xml
+++ b/spring-cloud-function-deployer/pom.xml
@@ -15,8 +15,7 @@
- 1.0.8.RELEASE
- 1.0.0.BUILD-SNAPSHOT
+ 1.0.9.RELEASE
@@ -31,7 +30,6 @@
org.springframework.cloud
spring-cloud-stream-binder-servlet
- ${spring-cloud-stream-servlet.version}
org.springframework.boot
diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java
index c603aebf9..872d1ab72 100644
--- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java
+++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java
@@ -99,6 +99,7 @@ public class ApplicationRunner implements CommandLineRunner {
if (this.classLoader!=null && this.classLoader instanceof Closeable) {
((Closeable) this.classLoader).close();
}
+ this.classLoader = null;
}
private RuntimeException getError() {
diff --git a/spring-cloud-function-web/.jdk8 b/spring-cloud-function-web/.jdk8
new file mode 100644
index 000000000..e69de29bb
diff --git a/spring-cloud-function-web/pom.xml b/spring-cloud-function-web/pom.xml
new file mode 100644
index 000000000..259d217cb
--- /dev/null
+++ b/spring-cloud-function-web/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ spring-cloud-function-web
+ jar
+ Spring Cloud Function Web Support
+ Spring Cloud Function Web Support
+
+
+ org.springframework.cloud
+ spring-cloud-function-parent
+ 1.0.0.BUILD-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.cloud
+ spring-cloud-function-context
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.springframework.boot.experimental
+ spring-boot-thin-layout
+ ${wrapper.version}
+
+
+
+
+
+
+
diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestApplication.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestApplication.java
new file mode 100644
index 000000000..f59f2afe5
--- /dev/null
+++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestApplication.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016-2017 the original author or 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 org.springframework.cloud.function.web;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Mark Fisher
+ */
+@SpringBootApplication
+public class RestApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(RestApplication.class, args);
+ }
+}
+
diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java
new file mode 100644
index 000000000..b12816670
--- /dev/null
+++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016-2017 the original author or 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 org.springframework.cloud.function.web.flux;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.cloud.function.context.catalog.FunctionInspector;
+import org.springframework.cloud.function.web.flux.request.FluxRequest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author Dave Syer
+ * @author Mark Fisher
+ */
+@Component
+public class FunctionController {
+
+ private static Log logger = LogFactory.getLog(FunctionController.class);
+
+ private FunctionInspector inspector;
+
+ private boolean debug = false;
+
+ public FunctionController(FunctionInspector inspector) {
+ this.inspector = inspector;
+ }
+
+ public void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
+ @PostMapping(path = "/**")
+ @ResponseBody
+ public ResponseEntity> post(
+ @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.function") Function, Flux>> function,
+ @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.consumer") Consumer> consumer,
+ @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.input_single") Boolean single,
+ @RequestBody FluxRequest> body) {
+ if (function != null) {
+ Flux> flux = body.flux();
+ if (debug) {
+ flux = flux.log();
+ }
+ Flux> result = function.apply(flux);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Handled POST with function");
+ }
+ return ResponseEntity.ok().body(debug ? result.log() : result);
+ }
+ if (consumer != null) {
+ Flux> flux = body.flux().cache(); // send a copy back to the caller
+ if (debug) {
+ flux = flux.log();
+ }
+ consumer.accept(flux);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Handled POST with consumer");
+ }
+ return ResponseEntity.status(HttpStatus.ACCEPTED).body(flux);
+ }
+ throw new IllegalArgumentException("no such function");
+ }
+
+ @GetMapping(path = "/**")
+ @ResponseBody
+ public Object get(
+ @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.function") Function, Flux>> function,
+ @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.supplier") Supplier> supplier,
+ @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.argument") String argument) {
+ if (function != null) {
+ return value(function, argument);
+ }
+ return supplier(supplier);
+ }
+
+ private Flux> supplier(Supplier> supplier) {
+ Flux> result = supplier.get();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Handled GET with supplier");
+ }
+ return debug ? result.log() : result;
+ }
+
+ private Mono> value(Function, Flux>> function,
+ @PathVariable String value) {
+ Object input = inspector.convert(function, value);
+ Mono> result = Mono.from(function.apply(Flux.just(input)));
+ if (logger.isDebugEnabled()) {
+ logger.debug("Handled GET with function");
+ }
+ return debug ? result.log() : result;
+ }
+}
diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java
new file mode 100644
index 000000000..367806968
--- /dev/null
+++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2012-2015 the original author or 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 org.springframework.cloud.function.web.flux;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.cloud.function.context.catalog.FunctionInspector;
+import org.springframework.cloud.function.core.FunctionCatalog;
+import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.StringUtils;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+/**
+ * @author Dave Syer
+ *
+ */
+@Configuration
+@ConditionalOnClass(RequestMappingHandlerMapping.class)
+public class FunctionHandlerMapping extends RequestMappingHandlerMapping
+ implements InitializingBean {
+
+ private final FunctionCatalog functions;
+
+ private final FunctionController controller;
+
+ @Value("${spring.cloud.function.web.path:}")
+ private String prefix = "";
+
+ @Value("${debug:${DEBUG:false}}")
+ private String debug = "false";
+
+ @Autowired
+ public FunctionHandlerMapping(FunctionCatalog catalog, FunctionInspector inspector) {
+ this.functions = catalog;
+ logger.info("FunctionCatalog: " + catalog + ", FunctionInspector: " + inspector);
+ setOrder(super.getOrder() - 5);
+ this.controller = new FunctionController(inspector);
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ super.afterPropertiesSet();
+ this.controller.setDebug(!"false".equals(debug));
+ detectHandlerMethods(controller);
+ while (prefix.endsWith("/")) {
+ prefix = prefix.substring(0, prefix.length() - 1);
+ }
+ }
+
+ @Override
+ protected void initHandlerMethods() {
+ }
+
+ @Override
+ protected HandlerMethod getHandlerInternal(HttpServletRequest request)
+ throws Exception {
+ HandlerMethod handler = super.getHandlerInternal(request);
+ if (handler == null) {
+ return null;
+ }
+ String path = (String) request
+ .getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
+ if (StringUtils.hasText(prefix) && !path.startsWith(prefix)) {
+ return null;
+ }
+ if (path.startsWith(prefix)) {
+ path = path.substring(prefix.length());
+ }
+ if (path == null) {
+ return handler;
+ }
+ Object function = findFunctionForGet(request, path);
+ if (function != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Found function for GET: " + path);
+ }
+ request.setAttribute(WebRequestConstants.HANDLER, function);
+ return handler;
+ }
+ function = findFunctionForPost(request, path);
+ if (function != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Found function for POST: " + path);
+ }
+ request.setAttribute(WebRequestConstants.HANDLER, function);
+ return handler;
+ }
+ return null;
+ }
+
+ private Object findFunctionForPost(HttpServletRequest request, String path) {
+ if (!request.getMethod().equals("POST")) {
+ return null;
+ }
+ path = path.startsWith("/") ? path.substring(1) : path;
+ Consumer