MEECROWAVE-197 very trivial proxy module, more to come
git-svn-id: https://svn.apache.org/repos/asf/openwebbeans/meecrowave/trunk@1860778 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/meecrowave-doc/pom.xml b/meecrowave-doc/pom.xml
index 496e21c..348b665 100644
--- a/meecrowave-doc/pom.xml
+++ b/meecrowave-doc/pom.xml
@@ -102,6 +102,11 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.apache.meecrowave</groupId>
+ <artifactId>meecrowave-proxy</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.jbake</groupId>
<artifactId>jbake-core</artifactId>
<version>2.6.4</version>
diff --git a/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java
index 7dd8328..75801f9 100755
--- a/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java
+++ b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java
@@ -53,6 +53,7 @@
import org.apache.meecrowave.doc.generator.LetsEncryptConfiguration;
import org.apache.meecrowave.doc.generator.MavenConfiguration;
import org.apache.meecrowave.doc.generator.OAuth2Configuration;
+import org.apache.meecrowave.doc.generator.ProxyConfiguration;
import org.jbake.app.Oven;
import org.jbake.app.configuration.ConfigUtil;
import org.jbake.app.configuration.DefaultJBakeConfiguration;
@@ -83,6 +84,7 @@
new OAuth2Configuration().run();
new LetsEncryptConfiguration().run();
new GradleConfiguration().run();
+ new ProxyConfiguration().run();
if (updateDownloads) {
final ByteArrayOutputStream tableContent = new ByteArrayOutputStream();
diff --git a/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/generator/ProxyConfiguration.java b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/generator/ProxyConfiguration.java
new file mode 100644
index 0000000..b98a42c
--- /dev/null
+++ b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/generator/ProxyConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.doc.generator;
+
+import static java.util.stream.Collectors.joining;
+
+import java.lang.reflect.Field;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import org.apache.meecrowave.proxy.servlet.meecrowave.ProxyServletSetup;
+import org.apache.meecrowave.runner.cli.CliOption;
+
+public class ProxyConfiguration extends BaseGenerator {
+ @Override
+ protected String generate() {
+ return super.tableConfig() + "|===\n|Name|Description\n" +
+ Stream.of(ProxyServletSetup.Configuration.class.getDeclaredFields())
+ .filter(f -> f.isAnnotationPresent(CliOption.class))
+ .sorted(Comparator.comparing(Field::getName))
+ .map(f -> f.getAnnotation(CliOption.class))
+ .map(opt -> "|--" + opt.name() + "|" + opt.description())
+ .collect(joining("\n")) + "\n|===\n";
+ }
+}
diff --git a/meecrowave-doc/src/main/jbake/content/meecrowave-proxy/index.adoc b/meecrowave-doc/src/main/jbake/content/meecrowave-proxy/index.adoc
new file mode 100644
index 0000000..de62d4c
--- /dev/null
+++ b/meecrowave-doc/src/main/jbake/content/meecrowave-proxy/index.adoc
@@ -0,0 +1,33 @@
+= Meecrowave Proxy
+:jbake-date: 2019-06-07
+:jbake-type: page
+:jbake-status: published
+:jbake-meecrowavepdf:
+:jbake-meecrowavetitleicon: icon icon_puzzle_alt
+:jbake-meecrowavecolor: blue-green
+:icons: font
+
+Coordinates:
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.meecrowave</groupId>
+ <artifactId>meecrowave-proxy</artifactId>
+ <version>${meecrowave.version}</version>
+</dependency>
+----
+
+Simple proxy module using Meecrowave as backbone.
+It can be extended using CDI programming model and JAX-RS client.
+
+== Configuration
+
+include::../../../../../target/generated-doc/ProxyConfiguration.adoc[]
+
+TIP: you can use that servlet in a plain Servlet container (adding JAX-RS+JSON-B client).
+An integration example can be found in `org.apache.meecrowave.proxy.servlet.meecrowave.ProxyServletSetup#accept`.
+
+== Extend
+
+TBD
diff --git a/meecrowave-proxy/meecrowave-proxy.iml b/meecrowave-proxy/meecrowave-proxy.iml
new file mode 100644
index 0000000..0c9a3ce
--- /dev/null
+++ b/meecrowave-proxy/meecrowave-proxy.iml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+ <output url="file://$MODULE_DIR$/target/classes" />
+ <output-test url="file://$MODULE_DIR$/target/test-classes" />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+ <excludeFolder url="file://$MODULE_DIR$/target" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="meecrowave-specs-api" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-annotation_1.3_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-jcdi_2.0_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-atinject_1.0_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-interceptor_1.2_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-json_1.1_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-jsonb_1.0_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-jaxrs_2.1_spec:1.1" level="project" />
+ <orderEntry type="library" name="Maven: org.apache.tomcat:tomcat-servlet-api:9.0.20" level="project" />
+ <orderEntry type="module" module-name="meecrowave-core" scope="PROVIDED" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-jaspic-api:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.xbean:xbean-finder-shaded:4.14" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.xbean:xbean-asm7-shaded:4.14" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.xbean:xbean-reflect:4.14" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-web:2.0.11" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-impl:2.0.11" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-spi:2.0.11" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-el22:2.0.11" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-catalina:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-juli:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-api:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-jni:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-coyote:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-util:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-util-scan:9.0.20" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-frontend-jaxrs:3.3.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-core:3.3.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-transports-http:3.3.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-security:3.3.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-integration-cdi:3.3.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-rs-client:3.3.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-jaxrs:1.1.12" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-mapper:1.1.12" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-core:1.1.12" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-jsonb:1.1.12" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-api:2.11.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-core:2.11.2" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-jul:2.11.2" level="project" />
+ <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.13-beta-3" level="project" />
+ <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
+ <orderEntry type="module" module-name="meecrowave-junit" scope="TEST" />
+ </component>
+</module>
\ No newline at end of file
diff --git a/meecrowave-proxy/pom.xml b/meecrowave-proxy/pom.xml
new file mode 100644
index 0000000..75dc8a2
--- /dev/null
+++ b/meecrowave-proxy/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>meecrowave</artifactId>
+ <groupId>org.apache.meecrowave</groupId>
+ <version>1.2.9-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>meecrowave-proxy</artifactId>
+ <name>Meecrowave :: Proxy</name>
+
+ <properties>
+ <meecrowave.build.name>${project.groupId}.proxy</meecrowave.build.name>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.meecrowave</groupId>
+ <artifactId>meecrowave-specs-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.meecrowave</groupId>
+ <artifactId>meecrowave-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.meecrowave</groupId>
+ <artifactId>meecrowave-junit</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/configuration/Routes.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/configuration/Routes.java
new file mode 100644
index 0000000..9c0d71c
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/configuration/Routes.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.configuration;
+
+import java.util.Collection;
+
+public class Routes {
+ public Route defaultRoute;
+ public Collection<Route> routes;
+
+ @Override
+ public String toString() {
+ return "Routes{routes=" + routes + '}';
+ }
+
+ public static class Route {
+ public String id;
+ public RequestConfiguration requestConfiguration;
+ public ResponseConfiguration responseConfiguration;
+
+ @Override
+ public String toString() {
+ return "Route{id='" + id + "', requestConfiguration=" + requestConfiguration + ", responseConfiguration=" + responseConfiguration + '}';
+ }
+ }
+
+ public static class ResponseConfiguration {
+ public String target;
+ public Collection<String> skippedHeaders;
+ public Collection<String> skippedCookies;
+
+ @Override
+ public String toString() {
+ return "ResponseConfiguration{target='" + target + "'}";
+ }
+ }
+
+ public static class RequestConfiguration {
+ public String method;
+ public String prefix;
+ public Collection<String> skippedHeaders;
+ public Collection<String> skippedCookies;
+
+ @Override
+ public String toString() {
+ return "RequestConfiguration{method='" + method + "', prefix='" + prefix + "'}";
+ }
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/ProxyServlet.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/ProxyServlet.java
new file mode 100644
index 0000000..3eab49c
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/ProxyServlet.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front;
+
+import static java.util.Collections.list;
+import static java.util.Optional.empty;
+import static java.util.Optional.ofNullable;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.stream.Collectors.toMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.stream.Stream;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.CompletionStageRxInvoker;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+import org.apache.meecrowave.proxy.servlet.service.ConfigurationLoader;
+
+// IMPORTANT: don't make this class depending on meecrowave, cxf or our internals, use setup class
+public class ProxyServlet extends HttpServlet {
+ protected Routes routes;
+ protected Client client;
+ protected ExecutorService executor;
+ protected long awaitTimeout;
+ protected long asyncTimeout;
+ protected int prefixLength;
+
+ @Override
+ protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ final String prefix = req.getRequestURI().substring(prefixLength);
+ final Optional<Routes.Route> matchedRoute = findRoute(req, prefix);
+ if (!matchedRoute.isPresent()) {
+ super.service(req, resp);
+ } else {
+ doExecute(matchedRoute.orElseThrow(IllegalArgumentException::new), req, resp, prefix);
+ }
+ }
+
+ protected CompletionStage<HttpServletResponse> doExecute(final Routes.Route route, final HttpServletRequest req, final HttpServletResponse resp,
+ final String prefix) throws IOException {
+ final AsyncContext asyncContext = req.startAsync();
+ asyncContext.setTimeout(asyncTimeout);
+
+ WebTarget target = client.target(route.responseConfiguration.target);
+ target = target.path(prefix); // todo: query params, multipart, etc
+
+ final Map<String, String> queryParams = ofNullable(req.getQueryString())
+ .filter(it -> !it.isEmpty())
+ .map(queries -> Stream.of(queries.split("&"))
+ .map(it -> {
+ final int eq = it.indexOf('=');
+ if (eq > 0) {
+ return new AbstractMap.SimpleEntry<>(it.substring(0, eq), it.substring(eq + 1));
+ }
+ return new AbstractMap.SimpleEntry<>(it, "");
+ })
+ .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)))
+ .orElseGet(Collections::emptyMap);
+ for (final Map.Entry<String, String> q : queryParams.entrySet()) {
+ target = target.queryParam(q.getKey(), q.getValue());
+ }
+
+ final String type = req.getContentType();
+ final Invocation.Builder request = type != null ? target.request(type) : target.request();
+
+ final Enumeration<String> headerNames = req.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ final String name = headerNames.nextElement();
+ if (!filterHeader(route.requestConfiguration.skippedHeaders, name)) {
+ request.header(name, list(req.getHeaders(name)));
+ }
+ }
+
+ final Cookie[] cookies = req.getCookies();
+ if (cookies != null) {
+ Stream.of(cookies)
+ .filter(it -> filterCookie(route.requestConfiguration.skippedCookies, it.getName(), it.getValue()))
+ .forEach(cookie -> request.cookie(
+ new javax.ws.rs.core.Cookie(cookie.getName(), cookie.getValue(), cookie.getPath(), cookie.getDomain(), cookie.getVersion())));
+ }
+
+ final CompletionStageRxInvoker rx = request.rx();
+ final CompletionStage<Response> result;
+ if (isWrite(req)) {
+ result = rx.method(req.getMethod(), Entity.entity(req.getInputStream(), ofNullable(req.getContentType()).orElse(MediaType.WILDCARD)));
+ } else {
+ result = rx.method(req.getMethod());
+ }
+ return result.handle((response, error) -> {
+ try {
+ if (error != null) {
+ onError(route, resp, error);
+ } else {
+ try {
+ forwardResponse(route, response, resp);
+ } catch (final IOException e) {
+ onError(route, resp, e);
+ }
+ }
+ } catch (final IOException ioe) {
+ getServletContext().log("Error Proxying " + req.getMethod() + " " + req.getRequestURI() + ": " + ioe.getMessage(), ioe);
+ } finally {
+ asyncContext.complete();
+ }
+ return resp;
+ });
+ }
+
+ protected boolean isWrite(final HttpServletRequest req) {
+ return !HttpMethod.HEAD.equalsIgnoreCase(req.getMethod()) && !HttpMethod.GET.equalsIgnoreCase(req.getMethod());
+ }
+
+ protected void onError(final Routes.Route route, final HttpServletResponse resp, final Throwable error) throws IOException {
+ if (WebApplicationException.class.isInstance(error)) {
+ final WebApplicationException wae = WebApplicationException.class.cast(error);
+ if (wae.getResponse() != null) {
+ forwardResponse(route, wae.getResponse(), resp);
+ return;
+ }
+ }
+ onDefaultError(resp, error);
+ }
+
+ protected void onDefaultError(HttpServletResponse resp, Throwable error) throws IOException {
+ resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ error.printStackTrace(new PrintWriter(resp.getOutputStream()));
+ }
+
+ protected void forwardResponse(final Routes.Route route, final Response response, final HttpServletResponse resp) throws IOException {
+ final int status = response.getStatus();
+ resp.setStatus(status);
+ forwardHeaders(route, response, resp);
+ if (status == HttpServletResponse.SC_NOT_MODIFIED && resp.getHeader(HttpHeaders.CONTENT_LENGTH) == null) {
+ resp.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
+ }
+ forwardCookies(route, response, resp);
+ writeOutput(resp, response.readEntity(InputStream.class));
+ }
+
+ protected void forwardCookies(final Routes.Route route, final Response response, final HttpServletResponse resp) {
+ response.getCookies().entrySet().stream()
+ .filter(cookie -> filterCookie(route.requestConfiguration.skippedCookies, cookie.getKey(), cookie.getValue().getValue()))
+ .forEach(cookie -> addCookie(resp, cookie));
+ }
+
+ protected void addCookie(final HttpServletResponse resp, final Map.Entry<String, NewCookie> cookie) {
+ final NewCookie nc = cookie.getValue();
+ final Cookie servletCookie = new Cookie(cookie.getKey(), nc.getValue());
+ servletCookie.setComment(nc.getComment());
+ servletCookie.setDomain(nc.getDomain());
+ servletCookie.setHttpOnly(nc.isHttpOnly());
+ servletCookie.setSecure(nc.isSecure());
+ servletCookie.setMaxAge(nc.getMaxAge());
+ servletCookie.setPath(nc.getPath());
+ servletCookie.setVersion(nc.getVersion());
+ resp.addCookie(servletCookie);
+ }
+
+ protected void forwardHeaders(final Routes.Route route, final Response response, final HttpServletResponse resp) {
+ response.getHeaders().entrySet().stream()
+ .filter(header -> filterHeader(route.requestConfiguration.skippedHeaders, header.getKey()))
+ .flatMap(entry -> entry.getValue().stream().map(value -> new AbstractMap.SimpleEntry<>(entry.getKey(), String.valueOf(value))))
+ .forEach(header -> resp.addHeader(header.getKey(), header.getValue()));
+ }
+
+ protected boolean filterCookie(final Collection<String> blacklist, final String name, final String value) {
+ return value != null && (blacklist == null || blacklist.stream().anyMatch(it -> it.equalsIgnoreCase(name)));
+ }
+
+ protected boolean filterHeader(final Collection<String> blacklist, final String name) {
+ return blacklist == null || blacklist.stream().anyMatch(it -> it.equalsIgnoreCase(name));
+ }
+
+ private void writeOutput(final HttpServletResponse resp, final InputStream stream) throws IOException {
+ final int bufferSize = Math.max(1, Math.min(8192, stream.available()));
+ final byte[] buffer = new byte[bufferSize]; // todo: reusable (copier?)
+ final ServletOutputStream outputStream = resp.getOutputStream();
+ int read;
+ while ((read = stream.read(buffer)) >= 0) {
+ if (read > 0) {
+ outputStream.write(buffer, 0, read);
+ }
+ }
+ }
+
+ protected Optional<Routes.Route> findRoute(final HttpServletRequest req, final String prefix) {
+ return routes == null ? empty() : routes.routes.stream()
+ .filter(it -> it.requestConfiguration.method == null || it.requestConfiguration.method.equalsIgnoreCase(req.getMethod()))
+ .filter(it -> it.requestConfiguration.prefix == null || it.requestConfiguration.prefix.equalsIgnoreCase(prefix))
+ .findFirst();
+ }
+
+ protected Optional<Routes> loadConfiguration() {
+ return get("configuration").flatMap(path -> new ConfigurationLoader(path).load());
+ }
+
+ @Override
+ public void init(final ServletConfig config) throws ServletException {
+ super.init(config);
+
+ prefixLength = get("mapping")
+ .map(it -> it.endsWith("/*") ? it.substring(0, it.length() - "/*".length()) : it)
+ .orElse("").length() + config.getServletContext().getContextPath().length();
+
+ awaitTimeout = getLong("shutdown.timeout").orElse(1L);
+ asyncTimeout = getLong("async.timeout").orElse(30000L);
+
+ setupClient();
+
+ final Optional<Routes> configuration = loadConfiguration();
+ if (!configuration.isPresent()) {
+ return;
+ }
+ routes = configuration.orElseThrow(IllegalArgumentException::new);
+ }
+
+ @Override
+ public void destroy() {
+ if (executor != null) {
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(awaitTimeout, MILLISECONDS)) {
+ getServletContext().log("Can't shutdown the client executor in " + awaitTimeout + "ms");
+ }
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ client.close();
+ super.destroy();
+ }
+
+ protected void setupClient() {
+ executor = new ThreadPoolExecutor(
+ getInt("executor.core").orElse(64),
+ getInt("executor.max").orElse(512),
+ getLong("executor.keepAlive").orElse(60000L),
+ MILLISECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactory() {
+ private final SecurityManager sm = System.getSecurityManager();
+ private final ThreadGroup group = (sm != null) ? sm.getThreadGroup() : Thread.currentThread().getThreadGroup();
+
+ @Override
+ public Thread newThread(final Runnable r) {
+ final Thread newThread = new Thread(group, r, ProxyServlet.class.getName() + "_" + hashCode());
+ newThread.setDaemon(false);
+ newThread.setPriority(Thread.NORM_PRIORITY);
+ newThread.setContextClassLoader(getClass().getClassLoader());
+ return newThread;
+ }
+ },
+ (r, executor) -> getServletContext().log("Proxy rejected task: " + r));
+
+ final ClientBuilder clientBuilder = ClientBuilder.newBuilder();
+ clientBuilder.executorService(executor);
+ clientBuilder.readTimeout(getLong("read.timeout").orElse(30000L), MILLISECONDS);
+ clientBuilder.connectTimeout(getLong("connect.timeout").orElse(30000L), MILLISECONDS);
+ // todo: configure ssl
+ // clientBuilder.scheduledExecutorService(); // not used by cxf for instance so no need to overkill the conf
+ client = clientBuilder.build();
+ }
+
+ private Optional<Long> getLong(final String key) {
+ return get(key).map(Long::parseLong);
+ }
+
+ private Optional<Integer> getInt(final String key) {
+ return get(key).map(Integer::parseInt);
+ }
+
+ private Optional<String> get(final String key) {
+ return ofNullable(getServletConfig().getInitParameter(key));
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/CDIProxyServlet.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/CDIProxyServlet.java
new file mode 100644
index 0000000..44fb9ef
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/CDIProxyServlet.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front.cdi;
+
+import java.io.IOException;
+import java.util.concurrent.CompletionStage;
+
+import javax.enterprise.event.Event;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+import org.apache.meecrowave.proxy.servlet.front.ProxyServlet;
+import org.apache.meecrowave.proxy.servlet.front.cdi.event.AfterResponse;
+import org.apache.meecrowave.proxy.servlet.front.cdi.event.BeforeRequest;
+
+// IMPORTANT: don't make this class depending on meecrowave, cxf or our internals, use setup class
+public class CDIProxyServlet extends ProxyServlet {
+ @Inject
+ private Event<BeforeRequest> beforeRequestEvent;
+
+ @Inject
+ private Event<AfterResponse> afterResponseEvent;
+
+ @Override
+ protected CompletionStage<HttpServletResponse> doExecute(final Routes.Route route, final HttpServletRequest req, final HttpServletResponse resp,
+ final String prefix) throws IOException {
+ final BeforeRequest event = new BeforeRequest(req, resp);
+ event.setRoute(route);
+ event.setPrefix(prefix);
+ beforeRequestEvent.fire(event);
+ return super.doExecute(event.getRoute(), req, resp, event.getPrefix())
+ .handle((r, t) -> {
+ afterResponseEvent.fire(new AfterResponse(req, resp));
+ return r;
+ });
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/AfterResponse.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/AfterResponse.java
new file mode 100644
index 0000000..5bf7baf
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/AfterResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front.cdi.event;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+
+public class AfterResponse extends BaseEvent {
+ private Routes.Route route;
+ private String prefix;
+
+ public AfterResponse(final HttpServletRequest request, final HttpServletResponse response) {
+ super(request, response);
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ public Routes.Route getRoute() {
+ return route;
+ }
+
+ public void setRoute(final Routes.Route route) {
+ this.route = route;
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BaseEvent.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BaseEvent.java
new file mode 100644
index 0000000..c01b738
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BaseEvent.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (C) 2006-2019 Talend Inc. - www.talend.com
+ * <p>
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.apache.meecrowave.proxy.servlet.front.cdi.event;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class BaseEvent {
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+
+ protected BaseEvent(final HttpServletRequest request, final HttpServletResponse response) {
+ this.request = request;
+ this.response = response;
+ }
+
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BeforeRequest.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BeforeRequest.java
new file mode 100644
index 0000000..43fa33d
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BeforeRequest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front.cdi.event;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+
+public class BeforeRequest extends BaseEvent {
+ private Routes.Route route;
+ private String prefix;
+
+ public BeforeRequest(final HttpServletRequest request, final HttpServletResponse response) {
+ super(request, response);
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ public Routes.Route getRoute() {
+ return route;
+ }
+
+ public void setRoute(final Routes.Route route) {
+ this.route = route;
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/meecrowave/ProxyServletSetup.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/meecrowave/ProxyServletSetup.java
new file mode 100644
index 0000000..c581c97
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/meecrowave/ProxyServletSetup.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.meecrowave;
+
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ServletRegistration;
+
+import org.apache.catalina.Context;
+import org.apache.meecrowave.Meecrowave;
+import org.apache.meecrowave.proxy.servlet.front.cdi.CDIProxyServlet;
+import org.apache.meecrowave.runner.Cli;
+import org.apache.meecrowave.runner.cli.CliOption;
+
+// IMPORTANT: don't make this class depending on meecrowave, cxf or our internals, use setup class
+public class ProxyServletSetup implements Meecrowave.MeecrowaveAwareContextCustomizer {
+ private Meecrowave instance;
+
+ @Override
+ public void accept(final Context context) {
+ final Configuration config = instance.getConfiguration().getExtension(Configuration.class);
+ if (config.skip) {
+ return;
+ }
+ context.addServletContainerInitializer((c, ctx) -> {
+ final ServletRegistration.Dynamic servlet = ctx.addServlet("meecrowave-proxy-servlet", CDIProxyServlet.class);
+ servlet.setLoadOnStartup(1);
+ servlet.setAsyncSupported(true);
+ servlet.addMapping(config.mapping);
+ if (config.multipart) {
+ servlet.setMultipartConfig(new MultipartConfigElement(
+ config.multipartLocation,
+ config.multipartMaxFileSize,
+ config.multipartMaxRequestSize,
+ config.multipartFileSizeThreshold));
+ }
+ servlet.setInitParameter("mapping", config.mapping);
+ servlet.setInitParameter("configuration", config.configuration);
+ servlet.setInitParameter("read.timeout", config.readTimeout);
+ servlet.setInitParameter("connect.timeout", config.connectTimeout);
+ servlet.setInitParameter("executor.core", config.threadPoolCoreSize);
+ servlet.setInitParameter("executor.max", config.threadPoolMaxSize);
+ servlet.setInitParameter("executor.keepAlive", config.threadPoolKeepAlive);
+ servlet.setInitParameter("shutdown.timeout", config.shutdownTimeout);
+ servlet.setInitParameter("async.timeout", config.asyncTimeout);
+ }, null);
+ }
+
+ @Override
+ public void setMeecrowave(final Meecrowave meecrowave) {
+ this.instance = meecrowave;
+ }
+
+ public static class Configuration implements Cli.Options {
+ @CliOption(name = "proxy-skip", description = "Should default setup be ignored")
+ private boolean skip = false;
+
+ @CliOption(name = "proxy-mapping", description = "Where to bind the proxy (url pattern).")
+ private String mapping = "/*";
+
+ @CliOption(name = "proxy-multipart", description = "Is multipart explicit.")
+ private boolean multipart = true;
+
+ @CliOption(name = "proxy-multipart-maxfilesize", description = "Max file size for multipart requests.")
+ private long multipartMaxFileSize = -1;
+
+ @CliOption(name = "proxy-multipart-maxrequestsize", description = "Max request size for multipart requests.")
+ private long multipartMaxRequestSize = -1;
+
+ @CliOption(name = "proxy-multipart-maxfilesizethreshold", description = "Max file size threshold for multipart requests.")
+ private int multipartFileSizeThreshold = 0;
+
+ @CliOption(name = "proxy-multipart-location", description = "The multipart temporary folder.")
+ private String multipartLocation = "";
+
+ @CliOption(name = "proxy-configuration", description = "The route file.")
+ private String configuration = "conf/proxy.json";
+
+ @CliOption(name = "proxy-shutdown-timeout", description = "How long the shutdown will wait for in progress tasks (executor).")
+ private String shutdownTimeout = "30000";
+
+ @CliOption(name = "proxy-executor-core", description = "HTTP client thread pool core size.")
+ private String threadPoolCoreSize = "64";
+
+ @CliOption(name = "proxy-executor-max", description = "HTTP client thread pool max size.")
+ private String threadPoolMaxSize = "512";
+
+ @CliOption(name = "proxy-executor-max", description = "HTTP client thread pool keep alive duration (in ms).")
+ private String threadPoolKeepAlive = "60000";
+
+ @CliOption(name = "proxy-read-timeout", description = "HTTP client read timeout.")
+ private String readTimeout = "30000";
+
+ @CliOption(name = "proxy-connect-timeout", description = "HTTP client connect timeout.")
+ private String connectTimeout = "30000";
+
+ @CliOption(name = "proxy-async-timeout", description = "Asynchronous execution timeout.")
+ private String asyncTimeout = "30000";
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/ConfigurationLoader.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/ConfigurationLoader.java
new file mode 100644
index 0000000..bd3fd53
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/ConfigurationLoader.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.service;
+
+import static java.util.Collections.singletonList;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+import javax.json.spi.JsonProvider;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+
+public class ConfigurationLoader {
+ private final String path;
+
+ private Routes routes;
+
+ public ConfigurationLoader(final String path) {
+ this.path = path;
+ }
+
+ public Optional<Routes> load() {
+ final SimpleSubstitutor simpleSubstitutor = new SimpleSubstitutor(
+ System.getProperties().stringPropertyNames().stream().collect(toMap(identity(), System::getProperty)));
+ final Path routeFile = Paths.get(simpleSubstitutor.replace(path));
+ if (!Files.exists(routeFile)) {
+ throw new IllegalArgumentException("No routes configuration for the proxy servlet");
+ }
+
+ try (final InputStream stream = Files.newInputStream(routeFile);
+ final Jsonb jsonb = JsonbBuilder.newBuilder()
+ .withProvider(loadJsonpProvider())
+ .withConfig(new JsonbConfig().setProperty("org.apache.johnzon.supports-comments", true))
+ .build()) {
+ routes = jsonb.fromJson(stream, Routes.class);
+ } catch (final Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ final boolean hasRoutes = routes.routes != null && !routes.routes.isEmpty();
+ if (routes.defaultRoute == null && !hasRoutes) {
+ return Optional.empty();
+ }
+ if (routes.defaultRoute != null) {
+ onLoad(simpleSubstitutor, routes.defaultRoute);
+ if (routes.routes == null) { // no route were defined, consider it is the default route, /!\ empty means no route, don't default
+ routes.routes = singletonList(routes.defaultRoute);
+ }
+ if (hasRoutes) {
+ routes.routes.forEach(r -> merge(routes.defaultRoute, r));
+ }
+ }
+ if (hasRoutes) {
+ routes.routes.forEach(it -> onLoad(simpleSubstitutor, it));
+ }
+ return Optional.of(routes);
+ }
+
+ private void merge(final Routes.Route defaultRoute, final Routes.Route route) {
+ if (route.requestConfiguration == null) {
+ route.requestConfiguration = defaultRoute.requestConfiguration;
+ } else if (defaultRoute.requestConfiguration != null) {
+ if (route.requestConfiguration.method == null) {
+ route.requestConfiguration.method = defaultRoute.requestConfiguration.method;
+ }
+ if (route.requestConfiguration.prefix == null) {
+ route.requestConfiguration.prefix = defaultRoute.requestConfiguration.prefix;
+ }
+ if (route.requestConfiguration.skippedCookies == null) {
+ route.requestConfiguration.skippedCookies = defaultRoute.requestConfiguration.skippedCookies;
+ }
+ if (route.requestConfiguration.skippedHeaders == null) {
+ route.requestConfiguration.skippedHeaders = defaultRoute.requestConfiguration.skippedHeaders;
+ }
+ }
+
+ if (route.responseConfiguration == null) {
+ route.responseConfiguration = defaultRoute.responseConfiguration;
+ } else if (defaultRoute.responseConfiguration != null) {
+ if (route.responseConfiguration.target == null) {
+ route.responseConfiguration.target = defaultRoute.responseConfiguration.target;
+ }
+ if (route.responseConfiguration.skippedCookies == null) {
+ route.responseConfiguration.skippedCookies = defaultRoute.responseConfiguration.skippedCookies;
+ }
+ if (route.responseConfiguration.skippedHeaders == null) {
+ route.responseConfiguration.skippedHeaders = defaultRoute.responseConfiguration.skippedHeaders;
+ }
+ }
+ }
+
+ private JsonProvider loadJsonpProvider() {
+ try { // prefer johnzon to support comments
+ return JsonProvider.class.cast(Thread.currentThread().getContextClassLoader()
+ .loadClass("org.apache.johnzon.core.JsonProviderImpl")
+ .getConstructor().newInstance());
+ } catch (final InvocationTargetException | NoClassDefFoundError | InstantiationException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException err) {
+ return JsonProvider.provider();
+ }
+ }
+
+ // filter
+ private void onLoad(final SimpleSubstitutor simpleSubstitutor, final Routes.Route route) {
+ if (route.requestConfiguration != null && route.requestConfiguration.prefix != null) {
+ route.requestConfiguration.prefix = simpleSubstitutor.replace(route.requestConfiguration.prefix);
+ }
+ if (route.requestConfiguration != null && route.requestConfiguration.method != null) {
+ route.requestConfiguration.method = simpleSubstitutor.replace(route.requestConfiguration.method);
+ }
+ if (route.responseConfiguration != null && route.responseConfiguration.target != null) {
+ route.responseConfiguration.target = simpleSubstitutor.replace(route.responseConfiguration.target);
+ }
+ }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/SimpleSubstitutor.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/SimpleSubstitutor.java
new file mode 100644
index 0000000..4bda989
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/SimpleSubstitutor.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+// duplicated to not depend on core if this module is deployed in another servlet container
+public class SimpleSubstitutor {
+ private final char[] prefix = "${".toCharArray();
+ private final char[] suffix = "}".toCharArray();
+ private final char[] valueDelimiter = ":-".toCharArray();
+ private final Map<String, String> valueMap;
+
+ public SimpleSubstitutor(final Map<String, String> valueMap) {
+ this.valueMap = valueMap;
+ }
+
+ public String replace(final String source) {
+ if (source == null) {
+ return null;
+ }
+ final StringBuilder builder = new StringBuilder(source);
+ if (substitute(builder, 0, source.length(), null) <= 0) {
+ return source;
+ }
+ return replace(builder.toString());
+ }
+
+ private int substitute(final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
+ final boolean top = priorVariables == null;
+ boolean altered = false;
+ int lengthChange = 0;
+ char[] chars = buf.toString().toCharArray();
+ int bufEnd = offset + length;
+ int pos = offset;
+ while (pos < bufEnd) {
+ final int startMatchLen = isMatch(prefix, chars, pos, bufEnd);
+ if (startMatchLen == 0) {
+ pos++;
+ } else {
+ if (pos > offset && chars[pos - 1] == '$') {
+ buf.deleteCharAt(pos - 1);
+ chars = buf.toString().toCharArray();
+ lengthChange--;
+ altered = true;
+ bufEnd--;
+ } else {
+ final int startPos = pos;
+ pos += startMatchLen;
+ int endMatchLen;
+ while (pos < bufEnd) {
+ endMatchLen = isMatch(suffix, chars, pos, bufEnd);
+ if (endMatchLen == 0) {
+ pos++;
+ } else {
+ String varNameExpr = new String(chars, startPos
+ + startMatchLen, pos - startPos
+ - startMatchLen);
+ pos += endMatchLen;
+ final int endPos = pos;
+
+ String varName = varNameExpr;
+ String varDefaultValue = null;
+
+ final char[] varNameExprChars = varNameExpr.toCharArray();
+ for (int i = 0; i < varNameExprChars.length; i++) {
+ if (isMatch(prefix, varNameExprChars, i, varNameExprChars.length) != 0) {
+ break;
+ }
+ final int match = isMatch(valueDelimiter, varNameExprChars, i, varNameExprChars.length);
+ if (match != 0) {
+ varName = varNameExpr.substring(0, i);
+ varDefaultValue = varNameExpr.substring(i + match);
+ break;
+ }
+ }
+
+ if (priorVariables == null) {
+ priorVariables = new ArrayList<>();
+ priorVariables.add(new String(chars,
+ offset, length));
+ }
+
+ checkCyclicSubstitution(varName, priorVariables);
+ priorVariables.add(varName);
+
+ final String varValue = getOrDefault(varName, varDefaultValue);
+ if (varValue != null) {
+ final int varLen = varValue.length();
+ buf.replace(startPos, endPos, varValue);
+ altered = true;
+ int change = substitute(buf, startPos, varLen, priorVariables);
+ change = change + varLen - (endPos - startPos);
+ pos += change;
+ bufEnd += change;
+ lengthChange += change;
+ chars = buf.toString().toCharArray();
+ }
+
+ priorVariables.remove(priorVariables.size() - 1);
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (top) {
+ return altered ? 1 : 0;
+ }
+ return lengthChange;
+ }
+
+ protected String getOrDefault(final String varName, final String varDefaultValue) {
+ return valueMap.getOrDefault(varName, varDefaultValue);
+ }
+
+ private int isMatch(final char[] chars, final char[] buffer, int pos,
+ final int bufferEnd) {
+ final int len = chars.length;
+ if (pos + len > bufferEnd) {
+ return 0;
+ }
+ for (int i = 0; i < chars.length; i++, pos++) {
+ if (chars[i] != buffer[pos]) {
+ return 0;
+ }
+ }
+ return len;
+ }
+
+ private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
+ if (!priorVariables.contains(varName)) {
+ return;
+ }
+ final StringBuilder buf = new StringBuilder(256);
+ buf.append("Infinite loop in property interpolation of ");
+ buf.append(priorVariables.remove(0));
+ buf.append(": ");
+ appendWithSeparators(buf, priorVariables);
+ throw new IllegalStateException(buf.toString());
+ }
+
+ private void appendWithSeparators(final StringBuilder builder, final Collection<String> iterable) {
+ if (iterable != null && !iterable.isEmpty()) {
+ final Iterator<?> it = iterable.iterator();
+ while (it.hasNext()) {
+ builder.append(it.next());
+ if (it.hasNext()) {
+ builder.append("->");
+ }
+ }
+ }
+ }
+}
diff --git a/meecrowave-proxy/src/main/resources/META-INF/beans.xml b/meecrowave-proxy/src/main/resources/META-INF/beans.xml
new file mode 100644
index 0000000..e250b87
--- /dev/null
+++ b/meecrowave-proxy/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+-->
+<beans bean-discovery-mode="all" version="2.0"
+ xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ http://xmlns.jcp.org/xml/ns/javaee
+ http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd">
+
+ <trim/>
+</beans>
diff --git a/meecrowave-proxy/src/main/resources/META-INF/services/org.apache.meecrowave.Meecrowave$ContextCustomizer b/meecrowave-proxy/src/main/resources/META-INF/services/org.apache.meecrowave.Meecrowave$ContextCustomizer
new file mode 100644
index 0000000..f8a9cf2
--- /dev/null
+++ b/meecrowave-proxy/src/main/resources/META-INF/services/org.apache.meecrowave.Meecrowave$ContextCustomizer
@@ -0,0 +1 @@
+org.apache.meecrowave.proxy.servlet.meecrowave.ProxyServletSetup
diff --git a/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/ProxyServletTest.java b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/ProxyServletTest.java
new file mode 100644
index 0000000..6b9bab3
--- /dev/null
+++ b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/ProxyServletTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet;
+
+import static java.util.Optional.ofNullable;
+import static javax.ws.rs.client.Entity.entity;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import org.apache.meecrowave.Meecrowave;
+import org.apache.meecrowave.io.IO;
+import org.apache.meecrowave.junit.MeecrowaveRule;
+import org.apache.meecrowave.proxy.servlet.mock.FakeRemoteServer;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+public class ProxyServletTest {
+ @ClassRule(order = 1)
+ public static final TestRule FAKE_REMOTE_SERVER = new FakeRemoteServer()
+ .with(server -> server.createContext("/simple", exchange -> {
+ final byte[] out = ("{\"message\":\"" + ofNullable(exchange.getRequestBody()).map(it -> {
+ try {
+ return IO.toString(it);
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }).orElse("ok") + "\"}").getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Fake-Server", "true");
+ exchange.getResponseHeaders().add("Foo", ofNullable(exchange.getRequestURI().getQuery()).orElse("-"));
+ exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, out.length);
+ try (final OutputStream os = exchange.getResponseBody()) {
+ os.write(out);
+ }
+ }))
+ .with(server -> server.createContext("/data1", exchange -> {
+ final byte[] out = ("{\"message\":\"" + IO.toString(exchange.getRequestBody()) + "\"}")
+ .getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Fake-Server", "posted");
+ exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, out.length);
+ try (final OutputStream os = exchange.getResponseBody()) {
+ os.write(out);
+ }
+ }));
+
+ @ClassRule(order = 2)
+ public static final MeecrowaveRule MW = new MeecrowaveRule(new Meecrowave.Builder()
+ .property("proxy-configuration", "target/test-classes/routes.json"), "");
+
+ @Test
+ public void get() {
+ withClient(target -> {
+ final Response response = target.path("/simple").request().get();
+ assertEquals(HttpURLConnection.HTTP_OK, response.getStatus());
+ assertEquals("true", response.getHeaderString("Fake-Server"));
+ assertEquals("{\"message\":\"\"}", response.readEntity(String.class));
+ });
+ }
+
+ @Test
+ public void getWithQuery() {
+ withClient(target -> {
+ final Response response = target.path("/simple").queryParam("foo", "bar").request().get();
+ assertEquals(HttpURLConnection.HTTP_OK, response.getStatus());
+ assertEquals("foo=bar", response.getHeaderString("Foo"));
+ assertEquals("{\"message\":\"\"}", response.readEntity(String.class));
+ });
+ }
+
+ @Test
+ public void post() {
+ withClient(target -> {
+ final Response response = target.path("/data1").request().post(entity("data were sent", TEXT_PLAIN_TYPE));
+ assertEquals(HttpURLConnection.HTTP_OK, response.getStatus());
+ assertEquals("posted", response.getHeaderString("Fake-Server"));
+ assertEquals("{\"message\":\"data were sent\"}", response.readEntity(String.class));
+ });
+ }
+
+ private void withClient(final Consumer<WebTarget> withBase) {
+ final Client client = ClientBuilder.newClient();
+ try {
+ withBase.accept(client.target("http://localhost:" + MW.getConfiguration().getHttpPort()));
+ } finally {
+ client.close();
+ }
+ }
+}
diff --git a/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/mock/FakeRemoteServer.java b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/mock/FakeRemoteServer.java
new file mode 100644
index 0000000..8f18c6e
--- /dev/null
+++ b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/mock/FakeRemoteServer.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.mock;
+
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Consumer;
+
+import com.sun.net.httpserver.HttpServer;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class FakeRemoteServer implements TestRule {
+ private HttpServer server;
+ private final Collection<Consumer<HttpServer>> configurers = new ArrayList<>();
+
+ public HttpServer getServer() {
+ return server;
+ }
+
+ public FakeRemoteServer with(final Consumer<HttpServer> configurer) {
+ configurers.add(configurer);
+ return this;
+ }
+
+ @Override
+ public Statement apply(final Statement statement, final Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ configurers.forEach(it -> it.accept(server));
+ server.start();
+ System.setProperty("fake.server.port", Integer.toString(server.getAddress().getPort()));
+ statement.evaluate();
+ } finally {
+ server.stop(0);
+ server = null;
+ System.clearProperty("fake.server.port");
+ }
+ }
+ };
+ }
+}
diff --git a/meecrowave-proxy/src/test/resources/routes.json b/meecrowave-proxy/src/test/resources/routes.json
new file mode 100644
index 0000000..13a9a5d
--- /dev/null
+++ b/meecrowave-proxy/src/test/resources/routes.json
@@ -0,0 +1,28 @@
+{
+ "defaultRoute": {
+ "responseConfiguration": {
+ // configure our global fake server for all the endpoints
+ "target": "http://localhost:${fake.server.port}"
+ }
+ },
+ "routes": [
+ {
+ // used to test a plain simple static endpoint - simplest case
+ "id": "get-simple",
+ "requestConfiguration": {
+ "method": "GET",
+ "prefix": "/simple"
+ }
+ },
+ {
+ /**
+ * used to test a very trivial post without any query param or anything else
+ */
+ "id": "post-simple",
+ "requestConfiguration": {
+ "method": "POST",
+ "prefix": "/data1"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ac4c16b..e276109 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<meecrowave.build.name>${project.groupId}.${project.artifactId}</meecrowave.build.name>
- <junit.version>4.12</junit.version>
+ <junit.version>4.13-beta-3</junit.version>
<tomcat.version>9.0.20</tomcat.version>
<openwebbeans.version>2.0.11</openwebbeans.version>
<cxf.version>3.3.2</cxf.version>
@@ -81,6 +81,7 @@
<module>integration-tests</module>
<module>meecrowave-oauth2</module>
<module>meecrowave-letsencrypt</module>
+ <module>meecrowave-proxy</module>
</modules>
<dependencyManagement>