From 818e50f135157a5ee8a098e7008b9572edfc69b8 Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 17:24:36 -0500 Subject: [PATCH 1/8] Basic handling for undertow requests --- containers/pom.xml | 1 + containers/undertow-http/pom.xml | 99 +++++++++++++ .../src/main/java/UndertowHttpContainer.java | 139 ++++++++++++++++++ pom.xml | 10 +- 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 containers/undertow-http/pom.xml create mode 100644 containers/undertow-http/src/main/java/UndertowHttpContainer.java diff --git a/containers/pom.xml b/containers/pom.xml index e0fb55ae33..f42224e193 100644 --- a/containers/pom.xml +++ b/containers/pom.xml @@ -68,6 +68,7 @@ jetty-servlet netty-http simple-http + undertow-http diff --git a/containers/undertow-http/pom.xml b/containers/undertow-http/pom.xml new file mode 100644 index 0000000000..99e1851a01 --- /dev/null +++ b/containers/undertow-http/pom.xml @@ -0,0 +1,99 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.containers + project + 2.26 + + + jersey-container-undertow-http + jar + jersey-container-undertow-http + + Undertow Http Container + + + + io.undertow + undertow-core + + + + + + + com.sun.istack + maven-istack-commons-plugin + true + + + org.codehaus.mojo + build-helper-maven-plugin + true + + + org.apache.felix + maven-bundle-plugin + true + + + + + + ${basedir}/src/main/java + + META-INF/**/* + + + + ${basedir}/src/main/resources + true + + + + + diff --git a/containers/undertow-http/src/main/java/UndertowHttpContainer.java b/containers/undertow-http/src/main/java/UndertowHttpContainer.java new file mode 100644 index 0000000000..e7d818a8c9 --- /dev/null +++ b/containers/undertow-http/src/main/java/UndertowHttpContainer.java @@ -0,0 +1,139 @@ +import io.undertow.Undertow; +import io.undertow.security.idm.Account; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.internal.ContainerUtils; +import org.glassfish.jersey.server.spi.Container; + +import javax.ws.rs.core.SecurityContext; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; + +public class UndertowHttpContainer implements HttpHandler, Container { + private volatile ApplicationHandler appHandler; + + @Override + public ApplicationHandler getApplicationHandler() { + return appHandler; + } + + @Override + public ResourceConfig getConfiguration() { + return appHandler.getConfiguration(); + } + + @Override + public void reload() { + reload(getConfiguration()); + } + + @Override + public void reload(ResourceConfig configuration) { + appHandler.onShutdown(this); + + appHandler = new ApplicationHandler(configuration); + appHandler.onReload(this); + appHandler.onStartup(this); + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + // FIXME: base path is basically the mount point. How do we handle this with undertow? + // FIXME: possibilities: get it at creation time (is this part of the xml), or use the exchange + // FIXME: might be servlet only though? + + // TODO: include the host header + URI baseUri = getBaseUri(exchange); + ContainerRequest request = new ContainerRequest(baseUri, getRequestUri(exchange, baseUri), + exchange.getRequestMethod().toString(), new UndertowSecurityContext(exchange), + new MapPropertiesDelegate()); + } + + private URI getBaseUri(HttpServerExchange exchange) { + try { + return new URI(exchange.getRequestScheme(), null, exchange.getHostName(), + exchange.getHostPort(), "/", null, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + private URI getRequestUri(HttpServerExchange exchange, URI baseUri) { + String uri = getServerAddress(baseUri) + exchange.getRequestURI(); + String query = exchange.getQueryString(); + if (query != null && !query.isEmpty()) { + uri += "?" + ContainerUtils.encodeUnsafeCharacters(query); + } + + try { + return new URI(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + private String getServerAddress(URI baseUri) { + String serverAddress = baseUri.toString(); + if (serverAddress.charAt(serverAddress.length() - 1) == '/') { + return serverAddress.substring(0, serverAddress.length() - 1); + } + + return serverAddress; + } + + private static final class UndertowSecurityContext implements SecurityContext { + private HttpServerExchange exchange; + + UndertowSecurityContext(HttpServerExchange exchange) { + this.exchange = exchange; + } + + @Override + public Principal getUserPrincipal() { + Account account = getAccount(); + if (account != null) { + return account.getPrincipal(); + } + + return null; + } + + @Override + public boolean isUserInRole(String role) { + Account account = getAccount(); + return account != null && account.getRoles().contains(role); + } + + @Override + public boolean isSecure() { + return exchange.isSecure(); + } + + @Override + public String getAuthenticationScheme() { + return exchange.getSecurityContext().getMechanismName(); + } + + private Account getAccount() { + if (exchange.getSecurityContext() != null) { + return exchange.getSecurityContext().getAuthenticatedAccount(); + } + + return null; + } + } + + public static void main(String[] args) { + Undertow server = Undertow.builder() + .addHttpListener(8080, "localhost") + .setHandler(new UndertowHttpContainer()) + .build(); + + server.start(); + } +} diff --git a/pom.xml b/pom.xml index f42ccf6991..453bb39e50 100644 --- a/pom.xml +++ b/pom.xml @@ -1421,6 +1421,12 @@ ${simple.version} + + io.undertow + undertow-core + ${undertow.version} + + org.codehaus.jettison jettison @@ -1907,7 +1913,9 @@ 4.0.0-b07 6.0.1 1.7.12 - 4.3.4.RELEASE + 3.2.17.RELEASE + 1.4.21.Final + 1.1.0.Final 5.1.3.Final 2.2.14.Final 3.0.0.Final From 5e69b4522160ab35cea9cf4652eb4d5b225718a6 Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 18:31:16 -0500 Subject: [PATCH 2/8] Flesh out response writer --- .../src/main/java/UndertowHttpContainer.java | 74 ++++++------------- .../src/main/java/UndertowResponseWriter.java | 69 +++++++++++++++++ .../main/java/UndertowSecurityContext.java | 47 ++++++++++++ 3 files changed, 139 insertions(+), 51 deletions(-) create mode 100644 containers/undertow-http/src/main/java/UndertowResponseWriter.java create mode 100644 containers/undertow-http/src/main/java/UndertowSecurityContext.java diff --git a/containers/undertow-http/src/main/java/UndertowHttpContainer.java b/containers/undertow-http/src/main/java/UndertowHttpContainer.java index e7d818a8c9..7c9d313f8e 100644 --- a/containers/undertow-http/src/main/java/UndertowHttpContainer.java +++ b/containers/undertow-http/src/main/java/UndertowHttpContainer.java @@ -1,7 +1,7 @@ import io.undertow.Undertow; -import io.undertow.security.idm.Account; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; import org.glassfish.jersey.internal.MapPropertiesDelegate; import org.glassfish.jersey.server.ApplicationHandler; import org.glassfish.jersey.server.ContainerRequest; @@ -9,10 +9,8 @@ import org.glassfish.jersey.server.internal.ContainerUtils; import org.glassfish.jersey.server.spi.Container; -import javax.ws.rs.core.SecurityContext; import java.net.URI; import java.net.URISyntaxException; -import java.security.Principal; public class UndertowHttpContainer implements HttpHandler, Container { private volatile ApplicationHandler appHandler; @@ -33,7 +31,7 @@ public void reload() { } @Override - public void reload(ResourceConfig configuration) { + public void reload(final ResourceConfig configuration) { appHandler.onShutdown(this); appHandler = new ApplicationHandler(configuration); @@ -42,19 +40,35 @@ public void reload(ResourceConfig configuration) { } @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { + public void handleRequest(final HttpServerExchange exchange) throws Exception { // FIXME: base path is basically the mount point. How do we handle this with undertow? // FIXME: possibilities: get it at creation time (is this part of the xml), or use the exchange // FIXME: might be servlet only though? - // TODO: include the host header + // If we ware on the IO thread (where the raw HTTP request is processed), + // we want to dispatch to a worker. This is the preferred approach. + if (exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + + exchange.startBlocking(); URI baseUri = getBaseUri(exchange); ContainerRequest request = new ContainerRequest(baseUri, getRequestUri(exchange, baseUri), exchange.getRequestMethod().toString(), new UndertowSecurityContext(exchange), new MapPropertiesDelegate()); + + request.setEntityStream(exchange.getInputStream()); + request.setWriter(new UndertowResponseWriter(exchange)); + for (HeaderValues values : exchange.getRequestHeaders()) { + String name = values.getHeaderName().toString(); + request.headers(name, values.iterator()); + } + + appHandler.handle(request); } - private URI getBaseUri(HttpServerExchange exchange) { + private URI getBaseUri(final HttpServerExchange exchange) { try { return new URI(exchange.getRequestScheme(), null, exchange.getHostName(), exchange.getHostPort(), "/", null, null); @@ -63,7 +77,7 @@ private URI getBaseUri(HttpServerExchange exchange) { } } - private URI getRequestUri(HttpServerExchange exchange, URI baseUri) { + private URI getRequestUri(final HttpServerExchange exchange, final URI baseUri) { String uri = getServerAddress(baseUri) + exchange.getRequestURI(); String query = exchange.getQueryString(); if (query != null && !query.isEmpty()) { @@ -77,7 +91,7 @@ private URI getRequestUri(HttpServerExchange exchange, URI baseUri) { } } - private String getServerAddress(URI baseUri) { + private String getServerAddress(final URI baseUri) { String serverAddress = baseUri.toString(); if (serverAddress.charAt(serverAddress.length() - 1) == '/') { return serverAddress.substring(0, serverAddress.length() - 1); @@ -86,48 +100,6 @@ private String getServerAddress(URI baseUri) { return serverAddress; } - private static final class UndertowSecurityContext implements SecurityContext { - private HttpServerExchange exchange; - - UndertowSecurityContext(HttpServerExchange exchange) { - this.exchange = exchange; - } - - @Override - public Principal getUserPrincipal() { - Account account = getAccount(); - if (account != null) { - return account.getPrincipal(); - } - - return null; - } - - @Override - public boolean isUserInRole(String role) { - Account account = getAccount(); - return account != null && account.getRoles().contains(role); - } - - @Override - public boolean isSecure() { - return exchange.isSecure(); - } - - @Override - public String getAuthenticationScheme() { - return exchange.getSecurityContext().getMechanismName(); - } - - private Account getAccount() { - if (exchange.getSecurityContext() != null) { - return exchange.getSecurityContext().getAuthenticatedAccount(); - } - - return null; - } - } - public static void main(String[] args) { Undertow server = Undertow.builder() .addHttpListener(8080, "localhost") diff --git a/containers/undertow-http/src/main/java/UndertowResponseWriter.java b/containers/undertow-http/src/main/java/UndertowResponseWriter.java new file mode 100644 index 0000000000..bcc8fcca9c --- /dev/null +++ b/containers/undertow-http/src/main/java/UndertowResponseWriter.java @@ -0,0 +1,69 @@ +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +class UndertowResponseWriter implements ContainerResponseWriter { + private final HttpServerExchange exchange; + + UndertowResponseWriter(final HttpServerExchange exchange) { + this.exchange = exchange; + } + + @Override + public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context) throws ContainerException { + exchange.setStatusCode(context.getStatus()); + + Map> headers = context.getStringHeaders(); + for (Map.Entry> e : headers.entrySet()) { + HttpString name = new HttpString(e.getKey()); + exchange.getResponseHeaders().addAll(name, e.getValue()); + } + + return exchange.getOutputStream(); + } + + @Override + public boolean suspend(final long timeOut, final TimeUnit timeUnit, final TimeoutHandler timeoutHandler) { + return false; + } + + @Override + public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throws IllegalStateException { + } + + @Override + public void commit() { + exchange.endExchange(); + } + + @Override + public void failure(final Throwable error) { + if (!exchange.isResponseStarted()) { + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + exchange.getResponseSender().send("Internal Server Error"); + } + + commit(); + + // Rethrow the original exception as required by JAX-RS, 3.3.4. + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new ContainerException(error); + } + } + + @Override + public boolean enableResponseBuffering() { + return false; + } +} + diff --git a/containers/undertow-http/src/main/java/UndertowSecurityContext.java b/containers/undertow-http/src/main/java/UndertowSecurityContext.java new file mode 100644 index 0000000000..1132cef2d1 --- /dev/null +++ b/containers/undertow-http/src/main/java/UndertowSecurityContext.java @@ -0,0 +1,47 @@ +import io.undertow.security.idm.Account; +import io.undertow.server.HttpServerExchange; + +import javax.ws.rs.core.SecurityContext; +import java.security.Principal; + +class UndertowSecurityContext implements SecurityContext { + private final HttpServerExchange exchange; + + UndertowSecurityContext(final HttpServerExchange exchange) { + this.exchange = exchange; + } + + @Override + public Principal getUserPrincipal() { + Account account = getAccount(); + if (account != null) { + return account.getPrincipal(); + } + + return null; + } + + @Override + public boolean isUserInRole(final String role) { + Account account = getAccount(); + return account != null && account.getRoles().contains(role); + } + + @Override + public boolean isSecure() { + return exchange.isSecure(); + } + + @Override + public String getAuthenticationScheme() { + return exchange.getSecurityContext().getMechanismName(); + } + + private Account getAccount() { + if (exchange.getSecurityContext() != null) { + return exchange.getSecurityContext().getAuthenticatedAccount(); + } + + return null; + } +} From 8713f5f19fdc7a839748bd023929adab49f3b10d Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 18:53:09 -0500 Subject: [PATCH 3/8] Provider and Factory for container --- .../undertow}/UndertowHttpContainer.java | 22 ++++------ .../UndertowHttpContainerFactory.java | 31 +++++++++++++ .../UndertowHttpContainerProvider.java | 19 ++++++++ .../undertow}/UndertowResponseWriter.java | 2 + .../undertow}/UndertowSecurityContext.java | 2 + .../jersey/undertow/package-info.java | 44 +++++++++++++++++++ ...ssfish.jersey.server.spi.ContainerProvider | 1 + .../localization.properties | 41 +++++++++++++++++ 8 files changed, 148 insertions(+), 14 deletions(-) rename containers/undertow-http/src/main/java/{ => org/glassfish/jersey/undertow}/UndertowHttpContainer.java (86%) create mode 100644 containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java create mode 100644 containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java rename containers/undertow-http/src/main/java/{ => org/glassfish/jersey/undertow}/UndertowResponseWriter.java (98%) rename containers/undertow-http/src/main/java/{ => org/glassfish/jersey/undertow}/UndertowSecurityContext.java (96%) create mode 100644 containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/package-info.java create mode 100644 containers/undertow-http/src/main/resources/META-INF.services/org.glassfish.jersey.server.spi.ContainerProvider create mode 100644 containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties diff --git a/containers/undertow-http/src/main/java/UndertowHttpContainer.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java similarity index 86% rename from containers/undertow-http/src/main/java/UndertowHttpContainer.java rename to containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java index 7c9d313f8e..e053bc99d1 100644 --- a/containers/undertow-http/src/main/java/UndertowHttpContainer.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java @@ -1,4 +1,5 @@ -import io.undertow.Undertow; +package org.glassfish.jersey.undertow; + import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.HeaderValues; @@ -9,12 +10,17 @@ import org.glassfish.jersey.server.internal.ContainerUtils; import org.glassfish.jersey.server.spi.Container; +import javax.ws.rs.core.Application; import java.net.URI; import java.net.URISyntaxException; public class UndertowHttpContainer implements HttpHandler, Container { private volatile ApplicationHandler appHandler; + UndertowHttpContainer(final Application application) { + this.appHandler = new ApplicationHandler(application); + } + @Override public ApplicationHandler getApplicationHandler() { return appHandler; @@ -41,10 +47,6 @@ public void reload(final ResourceConfig configuration) { @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { - // FIXME: base path is basically the mount point. How do we handle this with undertow? - // FIXME: possibilities: get it at creation time (is this part of the xml), or use the exchange - // FIXME: might be servlet only though? - // If we ware on the IO thread (where the raw HTTP request is processed), // we want to dispatch to a worker. This is the preferred approach. if (exchange.isInIoThread()) { @@ -68,6 +70,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { appHandler.handle(request); } + // TODO: No easy way to get the mount point without using Undertow servlets library private URI getBaseUri(final HttpServerExchange exchange) { try { return new URI(exchange.getRequestScheme(), null, exchange.getHostName(), @@ -99,13 +102,4 @@ private String getServerAddress(final URI baseUri) { return serverAddress; } - - public static void main(String[] args) { - Undertow server = Undertow.builder() - .addHttpListener(8080, "localhost") - .setHandler(new UndertowHttpContainer()) - .build(); - - server.start(); - } } diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java new file mode 100644 index 0000000000..d14075b12d --- /dev/null +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java @@ -0,0 +1,31 @@ +package org.glassfish.jersey.undertow; + +import io.undertow.Undertow; +import org.glassfish.jersey.server.ResourceConfig; + +import java.net.URI; + +public class UndertowHttpContainerFactory { + public static Undertow createServer(URI uri, ResourceConfig config) { + return createServerInternal(uri, config, true); + } + + public static Undertow createServer(URI uri, ResourceConfig config, boolean start) { + return createServerInternal(uri, config, start); + } + + private static Undertow createServerInternal(URI uri, ResourceConfig config, boolean start) { + // TODO: check for null config + // TODO: check for null URI + Undertow server = Undertow.builder() + .addHttpListener(uri.getPort(), uri.getHost()) + .setHandler(new UndertowHttpContainer(config)) + .build(); + + if (start) { + server.start(); + } + + return server; + } +} diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java new file mode 100644 index 0000000000..492485fb78 --- /dev/null +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java @@ -0,0 +1,19 @@ +package org.glassfish.jersey.undertow; + +import io.undertow.server.HttpHandler; +import org.glassfish.jersey.server.spi.ContainerProvider; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; + +public final class UndertowHttpContainerProvider implements ContainerProvider { + + @Override + public T createContainer(final Class type, final Application application) throws ProcessingException { + if (HttpHandler.class == type || UndertowHttpContainer.class == type) { + return type.cast(new UndertowHttpContainer(application)); + } + + return null; + } +} diff --git a/containers/undertow-http/src/main/java/UndertowResponseWriter.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java similarity index 98% rename from containers/undertow-http/src/main/java/UndertowResponseWriter.java rename to containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java index bcc8fcca9c..db2aef9ba1 100644 --- a/containers/undertow-http/src/main/java/UndertowResponseWriter.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java @@ -1,3 +1,5 @@ +package org.glassfish.jersey.undertow; + import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; import io.undertow.util.StatusCodes; diff --git a/containers/undertow-http/src/main/java/UndertowSecurityContext.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowSecurityContext.java similarity index 96% rename from containers/undertow-http/src/main/java/UndertowSecurityContext.java rename to containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowSecurityContext.java index 1132cef2d1..5aec980128 100644 --- a/containers/undertow-http/src/main/java/UndertowSecurityContext.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowSecurityContext.java @@ -1,3 +1,5 @@ +package org.glassfish.jersey.undertow; + import io.undertow.security.idm.Account; import io.undertow.server.HttpServerExchange; diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/package-info.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/package-info.java new file mode 100644 index 0000000000..3795102dea --- /dev/null +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/package-info.java @@ -0,0 +1,44 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2011-2017 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://oss.oracle.com/licenses/CDDL+GPL-1.1 + * or LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ + +/** + * Jersey Undertow container classes. + */ +package org.glassfish.jersey.undertow; diff --git a/containers/undertow-http/src/main/resources/META-INF.services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/undertow-http/src/main/resources/META-INF.services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000000..ddcacdf3f5 --- /dev/null +++ b/containers/undertow-http/src/main/resources/META-INF.services/org.glassfish.jersey.server.spi.ContainerProvider @@ -0,0 +1 @@ +org.glassfish.jersey.undertow.UndertowHttpContainerProvider diff --git a/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties b/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties new file mode 100644 index 0000000000..223a75c56f --- /dev/null +++ b/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties @@ -0,0 +1,41 @@ +# +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. +# +# Copyright (c) 2013-2017 Oracle and/or its affiliates. All rights reserved. +# +# The contents of this file are subject to the terms of either the GNU +# General Public License Version 2 only ("GPL") or the Common Development +# and Distribution License("CDDL") (collectively, the "License"). You +# may not use this file except in compliance with the License. You can +# obtain a copy of the License at +# https://oss.oracle.com/licenses/CDDL+GPL-1.1 +# or LICENSE.txt. See the License for the specific +# language governing permissions and limitations under the License. +# +# When distributing the software, include this License Header Notice in each +# file and include the License file at LICENSE.txt. +# +# GPL Classpath Exception: +# Oracle designates this particular file as subject to the "Classpath" +# exception as provided by Oracle in the GPL Version 2 section of the License +# file that accompanied this code. +# +# Modifications: +# If applicable, add the following below the License Header, with the fields +# enclosed by brackets [] replaced by your own identifying information: +# "Portions Copyright [year] [name of copyright owner]" +# +# Contributor(s): +# If you wish your version of this file to be governed by only the CDDL or +# only the GPL Version 2, indicate your decision by adding "[Contributor] +# elects to include this software in this distribution under the [CDDL or GPL +# Version 2] license." If you don't indicate a single choice of license, a +# recipient has the option to distribute your version of this file under +# either the CDDL, the GPL Version 2 or to extend the choice of license to +# its licensees as provided above. However, if you add GPL Version 2 code +# and therefore, elected the GPL Version 2 license, then the option applies +# only if the new code is made subject to such option by the copyright +# holder. +# + +uri.cannot.be.null=The URI must not be null. From 761945c280639c1332848bd6c08847a59b808cb7 Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 20:57:35 -0500 Subject: [PATCH 4/8] Read headers into context properly --- .../org/glassfish/jersey/undertow/UndertowHttpContainer.java | 4 +++- .../glassfish/jersey/undertow/UndertowResponseWriter.java | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java index e053bc99d1..09bff2f283 100644 --- a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java @@ -64,7 +64,9 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { request.setWriter(new UndertowResponseWriter(exchange)); for (HeaderValues values : exchange.getRequestHeaders()) { String name = values.getHeaderName().toString(); - request.headers(name, values.iterator()); + for (String value : values) { + request.header(name, value); + } } appHandler.handle(request); diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java index db2aef9ba1..fdff705e1c 100644 --- a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowResponseWriter.java @@ -20,7 +20,10 @@ class UndertowResponseWriter implements ContainerResponseWriter { } @Override - public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context) throws ContainerException { + public OutputStream writeResponseStatusAndHeaders( + final long contentLength, + final ContainerResponse context) throws ContainerException { + exchange.setStatusCode(context.getStatus()); Map> headers = context.getStringHeaders(); From 1698bffba167f89091fa6e16556b2c7906b5348e Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 21:41:04 -0500 Subject: [PATCH 5/8] Tests for undertow container; copies some code from other container test suites --- .../undertow/UndertowHttpContainer.java | 6 +- .../AbstractUndertowServerTester.java | 86 +++++++++++++++++++ .../jersey/undertow/BasicRequestTest.java | 63 ++++++++++++++ .../undertow/LifecycleListenerTest.java | 54 ++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java create mode 100644 containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java create mode 100644 containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/LifecycleListenerTest.java diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java index 09bff2f283..f301b6266f 100644 --- a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java @@ -18,7 +18,11 @@ public class UndertowHttpContainer implements HttpHandler, Container { private volatile ApplicationHandler appHandler; UndertowHttpContainer(final Application application) { - this.appHandler = new ApplicationHandler(application); + appHandler = new ApplicationHandler(application); + + // No lifecycle hooks for Undertow's server, so we do this for + // completeness but not accuracy. + appHandler.onStartup(this); } @Override diff --git a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java new file mode 100644 index 0000000000..d9e788b98d --- /dev/null +++ b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java @@ -0,0 +1,86 @@ +package org.glassfish.jersey.undertow; + +import io.undertow.Undertow; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.After; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.security.AccessController; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class AbstractUndertowServerTester { + private static final Logger LOGGER = Logger.getLogger(AbstractUndertowServerTester.class.getName()); + private static final int DEFAULT_PORT = 9998; + + /** + * Get the port to be used for test application deployments. + * + * @return The HTTP port of the URI + */ + private int getPort() { + final String value = AccessController + .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port")); + if (value != null) { + + try { + final int i = Integer.parseInt(value); + if (i <= 0) { + throw new NumberFormatException("Value not positive."); + } + return i; + } catch (NumberFormatException e) { + LOGGER.log(Level.CONFIG, + "Value of 'jersey.config.test.container.port'" + + " property is not a valid positive integer [" + value + "]." + + " Reverting to default [" + DEFAULT_PORT + "].", + e); + } + } + return DEFAULT_PORT; + } + + private volatile Undertow server; + + UriBuilder getUri() { + return UriBuilder.fromUri("http://localhost").port(getPort()); + } + + void startServer(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final URI baseUri = getBaseUri(); + server = UndertowHttpContainerFactory.createServer(baseUri, config); + LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + baseUri); + } + + void startServer(ResourceConfig config) { + final URI baseUri = getBaseUri(); + server = UndertowHttpContainerFactory.createServer(baseUri, config); + LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + baseUri); + } + + private URI getBaseUri() { + return UriBuilder.fromUri("http://localhost/").port(getPort()).build(); + } + + private void stopServer() { + try { + server.stop(); + server = null; + LOGGER.log(Level.INFO, "Jetty-http server stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @After + public void tearDown() { + if (server != null) { + stopServer(); + } + } +} diff --git a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java new file mode 100644 index 0000000000..8c494659e8 --- /dev/null +++ b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java @@ -0,0 +1,63 @@ +package org.glassfish.jersey.undertow; + +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.*; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; + +public class BasicRequestTest extends AbstractUndertowServerTester { + + @Path("/") + @Produces(MediaType.TEXT_PLAIN) + public static class TestResource { + + @GET + @Path("/dummy") + public String dummyResponse(@QueryParam("answer") String answer) { + return answer; + } + + @GET + @Path("/exception/{status}") + public void exceptionResponse(@PathParam("status") int status) { + throw new WebApplicationException(status); + } + } + + @Before + public void setup() { + startServer(TestResource.class); + } + + @Test + public void test400StatusCode() { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(getUri().path("exception/400").build()); + assertEquals(400, target.request().get(Response.class).getStatus()); + } + + @Test + public void test500StatusCode() { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(getUri().path("exception/500").build()); + assertEquals(500, target.request().get(Response.class).getStatus()); + } + + @Test + public void testValidResponse() { + String answer = "Tested, working."; + Client client = ClientBuilder.newClient(); + URI uri = getUri().path("dummy").queryParam("answer", answer).build(); + WebTarget target = client.target(uri); + assertEquals(answer, target.request().get(String.class)); + } +} \ No newline at end of file diff --git a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/LifecycleListenerTest.java b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/LifecycleListenerTest.java new file mode 100644 index 0000000000..33ee7a841b --- /dev/null +++ b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/LifecycleListenerTest.java @@ -0,0 +1,54 @@ +package org.glassfish.jersey.undertow; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener; +import org.glassfish.jersey.server.spi.Container; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +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 static org.junit.Assert.assertEquals; + +public class LifecycleListenerTest extends AbstractUndertowServerTester { + + @Path("/") + public static class TestResource { + @GET + public void doNothing() { + } + } + + public static class Reloader extends AbstractContainerLifecycleListener { + Container container; + + public void reload(ResourceConfig newConfig) { + container.reload(newConfig); + } + + @Override + public void onStartup(Container container) { + this.container = container; + } + } + + @Test + public void testReload() throws Exception { + Reloader reloader = new Reloader(); + ResourceConfig config = new ResourceConfig(); + config.registerInstances(reloader); + + startServer(config); + + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(getUri().path("/").build()); + assertEquals(404, target.request().get(Response.class).getStatus()); + + reloader.reload(new ResourceConfig(TestResource.class)); + assertEquals(204, target.request().get(Response.class).getStatus()); + } +} From 5edd6d8352b05fec7a683f774e40362b6a72496c Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 22:24:56 -0500 Subject: [PATCH 6/8] Flesh out the factory; comments; localization --- .../undertow/UndertowHttpContainer.java | 5 ++ .../UndertowHttpContainerFactory.java | 63 ++++++++++++++++--- .../UndertowHttpContainerProvider.java | 5 ++ .../localization.properties | 2 + .../jersey/undertow/BasicRequestTest.java | 8 ++- pom.xml | 2 +- 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java index f301b6266f..f1ca12b647 100644 --- a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainer.java @@ -14,6 +14,11 @@ import java.net.URI; import java.net.URISyntaxException; +/** + * Undertow {@code Container} implementation based on Undertow's {@link io.undertow.server.HttpHandler}. + * + * @author Jonathan Como (jonathan.como at gmail.com) + */ public class UndertowHttpContainer implements HttpHandler, Container { private volatile ApplicationHandler appHandler; diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java index d14075b12d..fd931a6cba 100644 --- a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerFactory.java @@ -2,26 +2,71 @@ import io.undertow.Undertow; import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.undertow.internal.LocalizationMessages; +import javax.net.ssl.SSLContext; import java.net.URI; +/** + * Factory for creating and starting Undertow server handlers. This returns + * a handle to the started server as {@link Undertow} instances, which allows + * the server to be stopped by invoking the {@link Undertow#stop()} method. + *

+ * To start the server in HTTPS mode an {@link SSLContext} can be provided. + * This will be used to decrypt and encrypt information sent over the + * connected TCP socket channel. + * + * @author Jonathan Como (jonathan.como at gmail.com) + */ public class UndertowHttpContainerFactory { public static Undertow createServer(URI uri, ResourceConfig config) { - return createServerInternal(uri, config, true); + return createServer(uri, null, config, true); } public static Undertow createServer(URI uri, ResourceConfig config, boolean start) { - return createServerInternal(uri, config, start); + return createServer(uri, null, config, start); } - private static Undertow createServerInternal(URI uri, ResourceConfig config, boolean start) { - // TODO: check for null config - // TODO: check for null URI - Undertow server = Undertow.builder() - .addHttpListener(uri.getPort(), uri.getHost()) - .setHandler(new UndertowHttpContainer(config)) - .build(); + public static Undertow createServer(URI uri, SSLContext sslContext, ResourceConfig config) { + return createServer(uri, sslContext, config, true); + } + + public static Undertow createServer(URI uri, SSLContext sslContext, ResourceConfig config, boolean start) { + if (uri == null) { + throw new IllegalArgumentException(LocalizationMessages.URI_CANNOT_BE_NULL()); + } + + int defaultPort; + String scheme = uri.getScheme(); + + if (sslContext != null) { + defaultPort = Container.DEFAULT_HTTPS_PORT; + if (!"https".equals(scheme)) { + throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTPS()); + } + } else { + defaultPort = Container.DEFAULT_HTTP_PORT; + if (!"http".equals(scheme)) { + throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTP()); + } + } + + int port = uri.getPort(); + if (port == -1) { + port = defaultPort; + } + + Undertow.Builder builder = Undertow.builder() + .setHandler(new UndertowHttpContainer(config)); + + if (sslContext != null) { + builder.addHttpsListener(port, uri.getHost(), sslContext); + } else { + builder.addHttpListener(port, uri.getHost()); + } + Undertow server = builder.build(); if (start) { server.start(); } diff --git a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java index 492485fb78..c51ad64fd7 100644 --- a/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java +++ b/containers/undertow-http/src/main/java/org/glassfish/jersey/undertow/UndertowHttpContainerProvider.java @@ -6,6 +6,11 @@ import javax.ws.rs.ProcessingException; import javax.ws.rs.core.Application; +/** + * Container provider for containers based on Undertow Server {@link io.undertow.Undertow}. + * + * @author Jonathan Como (jonathan.como at gmail.com) + */ public final class UndertowHttpContainerProvider implements ContainerProvider { @Override diff --git a/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties b/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties index 223a75c56f..abba9ec2ea 100644 --- a/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties +++ b/containers/undertow-http/src/main/resources/org.glassfish.jersey.undertow.internal/localization.properties @@ -39,3 +39,5 @@ # uri.cannot.be.null=The URI must not be null. +wrong.scheme.when.using.http=The URI scheme should be 'http' when not using SSL. +wrong.scheme.when.using.https=The URI scheme should be 'https' when using SSL. diff --git a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java index 8c494659e8..b8c42cbf8f 100644 --- a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java +++ b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/BasicRequestTest.java @@ -3,13 +3,17 @@ import org.junit.Before; import org.junit.Test; -import javax.ws.rs.*; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - import java.net.URI; import static org.junit.Assert.assertEquals; diff --git a/pom.xml b/pom.xml index 453bb39e50..b59bb1195c 100644 --- a/pom.xml +++ b/pom.xml @@ -1913,7 +1913,7 @@ 4.0.0-b07 6.0.1 1.7.12 - 3.2.17.RELEASE + 4.3.4.RELEASE 1.4.21.Final 1.1.0.Final 5.1.3.Final From 94412b8987ed00b9a1305159649252c6dedfc4d8 Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Mon, 20 Nov 2017 22:27:44 -0500 Subject: [PATCH 7/8] Fix pom --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index b59bb1195c..991ab12ba4 100644 --- a/pom.xml +++ b/pom.xml @@ -1915,7 +1915,6 @@ 1.7.12 4.3.4.RELEASE 1.4.21.Final - 1.1.0.Final 5.1.3.Final 2.2.14.Final 3.0.0.Final From 033e64a4636e91a7d8b7f61a771ab24092726f70 Mon Sep 17 00:00:00 2001 From: Jonathan Como Date: Tue, 21 Nov 2017 12:53:21 -0500 Subject: [PATCH 8/8] Replace references to Jetty --- .../jersey/undertow/AbstractUndertowServerTester.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java index d9e788b98d..7114b63524 100644 --- a/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java +++ b/containers/undertow-http/src/test/java/org/glassfish/jersey/undertow/AbstractUndertowServerTester.java @@ -54,13 +54,13 @@ void startServer(Class... resources) { config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); final URI baseUri = getBaseUri(); server = UndertowHttpContainerFactory.createServer(baseUri, config); - LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + baseUri); + LOGGER.log(Level.INFO, "Undertow-http server started on base uri: " + baseUri); } void startServer(ResourceConfig config) { final URI baseUri = getBaseUri(); server = UndertowHttpContainerFactory.createServer(baseUri, config); - LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + baseUri); + LOGGER.log(Level.INFO, "Undertow-http server started on base uri: " + baseUri); } private URI getBaseUri() { @@ -71,7 +71,7 @@ private void stopServer() { try { server.stop(); server = null; - LOGGER.log(Level.INFO, "Jetty-http server stopped."); + LOGGER.log(Level.INFO, "Undertow-http server stopped."); } catch (Exception e) { throw new RuntimeException(e); }