From 302cd1ec01513a32796a10b33562f91571e069cd Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 7 Jan 2020 16:00:28 -0500 Subject: [PATCH 01/35] Initial integration of Tyrus integration POC into 2.0 branch. --- .../io/helidon/common/http/DataChunk.java | 31 +++ webserver/pom.xml | 1 + webserver/tyrus/pom.xml | 104 ++++++++++ .../tyrus/TyrusReaderSubscriber.java | 81 ++++++++ .../helidon/webserver/tyrus/TyrusSupport.java | 192 ++++++++++++++++++ .../webserver/tyrus/TyrusWriterPublisher.java | 156 ++++++++++++++ .../helidon/webserver/tyrus/package-info.java | 20 ++ .../tyrus/src/main/java/module-info.java | 35 ++++ .../helidon/webserver/tyrus/EchoEndpoint.java | 57 ++++++ .../webserver/tyrus/ServerConfigurator.java | 35 ++++ .../webserver/tyrus/TyrusExampleMain.java | 83 ++++++++ .../webserver/tyrus/TyrusSupportTest.java | 108 ++++++++++ .../webserver/tyrus/UppercaseCodec.java | 54 +++++ .../test/resources/logging-test.properties | 28 +++ .../helidon/webserver/ForwardingHandler.java | 64 +++++- 15 files changed, 1046 insertions(+), 3 deletions(-) create mode 100644 webserver/tyrus/pom.xml create mode 100644 webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java create mode 100644 webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java create mode 100644 webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java create mode 100644 webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/package-info.java create mode 100644 webserver/tyrus/src/main/java/module-info.java create mode 100644 webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java create mode 100644 webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/ServerConfigurator.java create mode 100644 webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusExampleMain.java create mode 100644 webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusSupportTest.java create mode 100644 webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/UppercaseCodec.java create mode 100644 webserver/tyrus/src/test/resources/logging-test.properties diff --git a/common/http/src/main/java/io/helidon/common/http/DataChunk.java b/common/http/src/main/java/io/helidon/common/http/DataChunk.java index 719c42a81cc..41baa14b010 100644 --- a/common/http/src/main/java/io/helidon/common/http/DataChunk.java +++ b/common/http/src/main/java/io/helidon/common/http/DataChunk.java @@ -17,6 +17,8 @@ package io.helidon.common.http; import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * The DataChunk represents a part of the HTTP body content. @@ -105,6 +107,7 @@ static DataChunk create(boolean flush, ByteBuffer data, Runnable releaseCallback static DataChunk create(boolean flush, ByteBuffer data, Runnable releaseCallback, boolean readOnly) { return new DataChunk() { private boolean isReleased = false; + private CompletableFuture writeFuture; @Override public ByteBuffer data() { @@ -131,6 +134,16 @@ public boolean isReleased() { public boolean isReadOnly() { return readOnly; } + + @Override + public void writeFuture(CompletableFuture writeFuture) { + this.writeFuture = writeFuture; + } + + @Override + public Optional> writeFuture() { + return Optional.ofNullable(writeFuture); + } }; } @@ -259,4 +272,22 @@ default boolean isReadOnly() { default boolean isFlushChunk() { return flush() && data().limit() == 0; } + + /** + * Set a write future that will complete when data chunk has been + * written to a connection. + * + * @param writeFuture Write future. + */ + default void writeFuture(CompletableFuture writeFuture) { + } + + /** + * Returns a write future associated with this chunk. + * + * @return Write future if one has ben set. + */ + default Optional> writeFuture() { + return Optional.empty(); + } } diff --git a/webserver/pom.xml b/webserver/pom.xml index 11e6d196b7b..65a32d506f1 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -36,5 +36,6 @@ jersey test-support access-log + tyrus diff --git a/webserver/tyrus/pom.xml b/webserver/tyrus/pom.xml new file mode 100644 index 00000000000..f82213d7476 --- /dev/null +++ b/webserver/tyrus/pom.xml @@ -0,0 +1,104 @@ + + + + + 4.0.0 + + io.helidon.webserver + helidon-webserver-project + 2.0-SNAPSHOT + + + helidon-webserver-tyrus + + + 1.1.1 + 1.15 + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-reactive + + + jakarta.websocket + jakarta.websocket-api + ${websocket-api-version} + + + org.glassfish.tyrus + tyrus-core + ${tyrus-version} + + + org.glassfish.tyrus + tyrus-client + ${tyrus-version} + + + org.glassfish.tyrus + tyrus-server + ${tyrus-version} + + + io.helidon.webserver + helidon-webserver-test-support + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + + maven-surefire-plugin + + + + ${project.build.testOutputDirectory}/logging-test.properties + + + + + + + + + \ No newline at end of file diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java new file mode 100644 index 00000000000..da5d22892a0 --- /dev/null +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import javax.websocket.CloseReason; +import java.nio.ByteBuffer; +import java.util.concurrent.Flow; +import java.util.logging.Logger; + +import io.helidon.common.http.DataChunk; +import org.glassfish.tyrus.spi.Connection; + +import static javax.websocket.CloseReason.CloseCodes.NORMAL_CLOSURE; +import static javax.websocket.CloseReason.CloseCodes.UNEXPECTED_CONDITION; + +/** + * Class TyrusReaderSubscriber. + */ +public class TyrusReaderSubscriber implements Flow.Subscriber { + private static final Logger LOGGER = Logger.getLogger(TyrusSupport.class.getName()); + + private static final int MAX_RETRIES = 3; + private static final CloseReason CONNECTION_CLOSED = new CloseReason(NORMAL_CLOSURE, "Connection closed"); + + private final Connection connection; + + TyrusReaderSubscriber(Connection connection) { + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + this.connection = connection; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(DataChunk item) { + // Send data to Tyrus + ByteBuffer data = item.data(); + connection.getReadHandler().handle(data); + + // Retry a few times if Tyrus did not consume all data + int retries = MAX_RETRIES; + while (data.remaining() > 0 && retries-- > 0) { + LOGGER.warning("Tyrus did not consume all data buffer"); + connection.getReadHandler().handle(data); + } + + // Report error if data is still unconsumed + if (retries == 0) { + throw new RuntimeException("Tyrus unable to consume data buffer"); + } + } + + @Override + public void onError(Throwable throwable) { + connection.close(new CloseReason(UNEXPECTED_CONDITION, throwable.getMessage())); + } + + @Override + public void onComplete() { + connection.close(CONNECTION_CLOSED); + } +} diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java new file mode 100644 index 00000000000..75d12fb2c57 --- /dev/null +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import javax.websocket.DeploymentException; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +import io.helidon.webserver.Handler; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; +import org.glassfish.tyrus.core.RequestContext; +import org.glassfish.tyrus.core.TyrusUpgradeResponse; +import org.glassfish.tyrus.core.TyrusWebSocketEngine; +import org.glassfish.tyrus.server.TyrusServerContainer; +import org.glassfish.tyrus.spi.Connection; +import org.glassfish.tyrus.spi.WebSocketEngine; + +/** + * Class TyrusSupport implemented as a Helidon service. + */ +public class TyrusSupport implements Service { + private static final Logger LOGGER = Logger.getLogger(TyrusSupport.class.getName()); + + /** + * A zero-length buffer indicates a connection flush to Helidon. + */ + private static final ByteBuffer FLUSH_BUFFER = ByteBuffer.allocateDirect(0); + + private final WebSocketEngine engine; + private final TyrusHandler handler = new TyrusHandler(); + + TyrusSupport(WebSocketEngine engine) { + this.engine = engine; + } + + /** + * Register our WebSocket handler for all routes. Once a request is received, + * it will be forwarded to the next handler if not a protocol upgrade request. + * + * @param routingRules Routing rules to update. + */ + @Override + public void update(Routing.Rules routingRules) { + LOGGER.info("Updating TyrusSupport routing routes"); + routingRules.any(handler); + } + + /** + * Creates a builder for this class. + * + * @return A builder for this class. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for convenient way to create {@link TyrusSupport}. + */ + public static final class Builder implements io.helidon.common.Builder { + + private Set> endpointClasses = new HashSet<>(); + + private Builder() { + } + + Builder register(Class endpointClass) { + endpointClasses.add(endpointClass); + return this; + } + + @Override + public TyrusSupport build() { + // Create container and WebSocket engine + TyrusServerContainer serverContainer = new TyrusServerContainer(endpointClasses) { + private final WebSocketEngine engine = + TyrusWebSocketEngine.builder(this).build(); + + @Override + public void register(Class endpointClass) { + throw new UnsupportedOperationException("Use TyrusWebSocketEngine for registration"); + } + + @Override + public void register(ServerEndpointConfig serverEndpointConfig) { + throw new UnsupportedOperationException("Use TyrusWebSocketEngine for registration"); + } + + @Override + public WebSocketEngine getWebSocketEngine() { + return engine; + } + }; + + // Register classes with context path "/" + WebSocketEngine engine = serverContainer.getWebSocketEngine(); + endpointClasses.forEach(c -> { + try { + // Context path handled by Helidon based on app's routes + engine.register(c, "/"); + } catch (DeploymentException e) { + throw new RuntimeException(e); + } + }); + + // Create TyrusSupport using WebSocket engine + return new TyrusSupport(serverContainer.getWebSocketEngine()); + } + } + + /** + * A Helidon handler that integrates with Tyrus and can process WebSocket + * upgrade requests. + */ + private class TyrusHandler implements Handler { + + /** + * Process a server request/response. + * + * @param req an HTTP server request. + * @param res an HTTP server response. + */ + @Override + public void accept(ServerRequest req, ServerResponse res) { + // Skip this handler if not an upgrade request + Optional secWebSocketKey = req.headers().value(HandshakeRequest.SEC_WEBSOCKET_KEY); + if (!secWebSocketKey.isPresent()) { + req.next(); + return; + } + + LOGGER.fine("Initiating WebSocket handshake ..."); + + // Create Tyrus request context and copy request headers + RequestContext requestContext = RequestContext.Builder.create() + .requestURI(URI.create(req.path().toString())) // excludes context path + .build(); + req.headers().toMap().entrySet().forEach(e -> + requestContext.getHeaders().put(e.getKey(), e.getValue())); + + // Use Tyrus to process a WebSocket upgrade request + final TyrusUpgradeResponse upgradeResponse = new TyrusUpgradeResponse(); + final WebSocketEngine.UpgradeInfo upgradeInfo = engine.upgrade(requestContext, upgradeResponse); + + // Respond to upgrade request using response from Tyrus + res.status(upgradeResponse.getStatus()); + upgradeResponse.getHeaders().entrySet().forEach(e -> + res.headers().add(e.getKey(), e.getValue())); + TyrusWriterPublisher publisherWriter = new TyrusWriterPublisher(); + res.send(publisherWriter); + + // Write reason for failure if not successful + if (upgradeInfo.getStatus() != WebSocketEngine.UpgradeStatus.SUCCESS) { + publisherWriter.write(ByteBuffer.wrap(upgradeResponse.getReasonPhrase().getBytes()), null); + } + + // Flush upgrade response + publisherWriter.write(FLUSH_BUFFER, null); + + // Setup the WebSocket connection and internally the ReaderHandler + Connection connection = upgradeInfo.createConnection(publisherWriter, + closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason)); + + // Set up reader to pass data back to Tyrus + TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection); + req.content().subscribe(subscriber); + } + } +} diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java new file mode 100644 index 00000000000..dbef6b97f3d --- /dev/null +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicLong; + +import io.helidon.common.http.DataChunk; +import org.glassfish.tyrus.spi.CompletionHandler; +import org.glassfish.tyrus.spi.Writer; + +/** + * Class TyrusWriterProducer. + */ +public class TyrusWriterPublisher extends Writer implements Flow.Publisher { + + private Flow.Subscriber subscriber; + + private final Queue queue = new ConcurrentLinkedQueue<>(); + + private final AtomicLong requested = new AtomicLong(0L); + + private static class QueuedBuffer { + private final CompletionHandler completionHandler; + private final ByteBuffer byteBuffer; + + QueuedBuffer(ByteBuffer byteBuffer, CompletionHandler completionHandler) { + this.byteBuffer = byteBuffer; + this.completionHandler = completionHandler; + } + + CompletionHandler completionHandler() { + return completionHandler; + } + + ByteBuffer byteBuffer() { + return byteBuffer; + } + } + + // -- Writer -------------------------------------------------------------- + + @Override + public void write(ByteBuffer byteBuffer, CompletionHandler handler) { + // No queueing if there is no subscriber + if (subscriber == null) { + return; + } + + // Nothing requested yet, just queue buffer + if (requested.get() <= 0) { + queue.add(new QueuedBuffer(byteBuffer, handler)); + return; + } + + // Write queued buffers first + while (!queue.isEmpty() && requested.get() > 0) { + QueuedBuffer queuedBuffer = queue.remove(); + writeNext(queuedBuffer.byteBuffer(), queuedBuffer.completionHandler()); + decrement(requested); + } + + // Process current buffer + if (requested.get() > 0) { + writeNext(byteBuffer, handler); + decrement(requested); + } else { + queue.add(new QueuedBuffer(byteBuffer, handler)); + } + } + + @Override + public void close() throws IOException { + if (subscriber != null) { + subscriber.onComplete(); + } + } + + // -- Publisher ----------------------------------------------------------- + + @Override + public void subscribe(Flow.Subscriber newSubscriber) { + if (subscriber != null) { + throw new IllegalStateException("Only one subscriber is allowed"); + } + subscriber = newSubscriber; + subscriber.onSubscribe(new Flow.Subscription() { + @Override + public void request(long n) { + if (n == Long.MAX_VALUE) { + requested.set(Long.MAX_VALUE); + } else { + requested.getAndAdd(n); + } + } + + @Override + public void cancel() { + requested.set(0L); + } + }); + } + + // -- Utility methods ----------------------------------------------------- + + private static synchronized long decrement(AtomicLong requested) { + long value = requested.get(); + return value == Long.MAX_VALUE ? requested.get() : requested.decrementAndGet(); + } + + private void writeNext(ByteBuffer byteBuffer, CompletionHandler handler) { + DataChunk dataChunk = DataChunk.create(true, byteBuffer, true); + if (handler != null) { + dataChunk.writeFuture(fromCompletionHandler(handler)); + } + subscriber.onNext(dataChunk); + } + + /** + * Wraps {@code CompletionHandler} into a {@code CompletableFuture} so that + * when the latter succeeds or fails so does the former. + * + * @param handler Handler to wrap. + * @return Wrapped handler. + */ + private static CompletableFuture fromCompletionHandler(CompletionHandler handler) { + CompletableFuture future = new CompletableFuture<>(); + future.whenComplete((chunk, throwable) -> { + if (throwable == null) { + handler.completed(chunk.data()); + } else { + handler.failed(throwable); + } + }); + return future; + } +} diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/package-info.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/package-info.java new file mode 100644 index 00000000000..31d8af20dfa --- /dev/null +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tyrus integration. + */ +package io.helidon.webserver.tyrus; diff --git a/webserver/tyrus/src/main/java/module-info.java b/webserver/tyrus/src/main/java/module-info.java new file mode 100644 index 00000000000..09a0e97fc8a --- /dev/null +++ b/webserver/tyrus/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tyrus integration. + */ +module io.helidon.webserver.tyrus { + requires java.logging; + requires transitive jakarta.websocket.api; + requires transitive java.annotation; + requires transitive io.helidon.webserver; + + requires io.helidon.common.context; + requires io.helidon.common.mapper; + requires io.helidon.common.reactive; + + requires tyrus.core; + requires tyrus.server; + requires tyrus.spi; + + exports io.helidon.webserver.tyrus; +} diff --git a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java new file mode 100644 index 00000000000..dedfb399ca0 --- /dev/null +++ b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.logging.Logger; + +@ServerEndpoint( + value = "/echo", + encoders = { UppercaseCodec.class }, + decoders = { UppercaseCodec.class }, + configurator = ServerConfigurator.class +) +public class EchoEndpoint { + private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); + + @OnOpen + public void onOpen(Session session) throws IOException { + LOGGER.info("OnOpen called"); + } + + @OnMessage + public void echo(Session session, String message) throws IOException { + LOGGER.info("OnMessage called with '" + message + "'"); + session.getBasicRemote().sendText(message); + } + + @OnError + public void onError(Throwable t) { + LOGGER.info("OnError called"); + } + + @OnClose + public void onClose(Session session) { + LOGGER.info("OnClose called"); + } +} diff --git a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/ServerConfigurator.java b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/ServerConfigurator.java new file mode 100644 index 00000000000..d3dcfe924c2 --- /dev/null +++ b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/ServerConfigurator.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import java.util.logging.Logger; + +/** + * Class ServerConfigurator. + */ +public class ServerConfigurator extends ServerEndpointConfig.Configurator { + private static final Logger LOGGER = Logger.getLogger(ServerConfigurator.class.getName()); + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + super.modifyHandshake(sec, request, response); + LOGGER.info("ServerConfigurator called during handshake"); + } +} diff --git a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusExampleMain.java b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusExampleMain.java new file mode 100644 index 00000000000..d395489883a --- /dev/null +++ b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusExampleMain.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import java.net.InetAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testsupport.LoggingTestUtils; + +/** + * The TyrusExampleMain. + */ +public final class TyrusExampleMain { + + public static final TyrusExampleMain INSTANCE = new TyrusExampleMain(); + + private TyrusExampleMain() { + } + + private volatile WebServer webServer; + + public static void main(String... args) throws InterruptedException, ExecutionException, TimeoutException { + LoggingTestUtils.initializeLogging(); + INSTANCE.webServer(false); + } + + synchronized WebServer webServer(boolean testing) throws InterruptedException, TimeoutException, ExecutionException { + if (webServer != null) { + return webServer; + } + + ServerConfiguration.Builder builder = + ServerConfiguration.builder().bindAddress(InetAddress.getLoopbackAddress()); + + if (!testing) { + // in case we're running as main an not in test, run on a fixed port + builder.port(8080); + } + + webServer = WebServer.create( + builder.build(), + Routing.builder().register( + "/tyrus", + TyrusSupport.builder().register(EchoEndpoint.class).build())); + + webServer.start().toCompletableFuture().get(10, TimeUnit.SECONDS); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + webServer.shutdown().toCompletableFuture().get(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new IllegalStateException(e); + } + })); + + if (!testing) { + System.out.println("WebServer Tyrus application started."); + System.out.println("Hit CTRL+C to stop."); + Thread.currentThread().join(); + } + + return webServer; + } +} diff --git a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusSupportTest.java b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusSupportTest.java new file mode 100644 index 00000000000..f4cd2370639 --- /dev/null +++ b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/TyrusSupportTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import io.helidon.webserver.WebServer; +import org.glassfish.tyrus.client.ClientManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Class TyrusSupportTest. + */ +public class TyrusSupportTest { + + private static WebServer webServer; + + @BeforeAll + public static void startServer() throws Exception { + webServer = TyrusExampleMain.INSTANCE.webServer(true); + } + + @AfterAll + public static void stopServer() { + webServer.shutdown(); + } + + @Test + public void testEcho() { + CompletableFuture openFuture = new CompletableFuture<>(); + CompletableFuture messageFuture = new CompletableFuture<>(); + CompletableFuture closeFuture = new CompletableFuture<>(); + + try { + URI uri = URI.create("ws://localhost:" + webServer.port() + "/tyrus/echo"); + ClientManager client = ClientManager.createClient(); + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + client.connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig EndpointConfig) { + openFuture.complete(null); + + try { + // Register message handler. Tyrus has problems with lambdas here + // so an inner class with an onMessage method is required. + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String message) { + assertTrue(message.equals("HI")); + messageFuture.complete(null); + try { + session.close(); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + }); + + // Send message to Echo service + session.getBasicRemote().sendText("hi"); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + closeFuture.complete(null); + } + }, config, uri); + + openFuture.get(10, TimeUnit.SECONDS); + messageFuture.get(10, TimeUnit.SECONDS); + closeFuture.get(10, TimeUnit.SECONDS); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + } +} diff --git a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/UppercaseCodec.java b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/UppercaseCodec.java new file mode 100644 index 00000000000..d5095ec38f5 --- /dev/null +++ b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/UppercaseCodec.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tyrus; + +import javax.websocket.Decoder; +import javax.websocket.Encoder; +import javax.websocket.EndpointConfig; +import java.util.logging.Logger; + +/** + * Class UppercaseCodec. + */ +public class UppercaseCodec implements Decoder.Text, Encoder.Text { + private static final Logger LOGGER = Logger.getLogger(UppercaseCodec.class.getName()); + + @Override + public String decode(String s) { + LOGGER.info("UppercaseCodec decode called"); + return s.toUpperCase(); + } + + @Override + public boolean willDecode(String s) { + return true; + } + + @Override + public void init(EndpointConfig config) { + } + + @Override + public void destroy() { + } + + @Override + public String encode(String s) { + LOGGER.info("UppercaseCodec encode called"); + return s.toUpperCase(); + } +} diff --git a/webserver/tyrus/src/test/resources/logging-test.properties b/webserver/tyrus/src/test/resources/logging-test.properties new file mode 100644 index 00000000000..aa55aa774dc --- /dev/null +++ b/webserver/tyrus/src/test/resources/logging-test.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +#All attributes details +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=WARNING + +io.helidon.webserver.level=FINE +org.glassfish.jersey.internal.Errors.level=SEVERE diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java index 48af371ca60..c8087596560 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java @@ -16,17 +16,21 @@ package io.helidon.webserver; +import javax.net.ssl.SSLEngine; + import java.nio.charset.StandardCharsets; + +import java.util.Iterator; +import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; -import javax.net.ssl.SSLEngine; - import io.helidon.common.http.DataChunk; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; @@ -35,8 +39,10 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; @@ -57,19 +63,24 @@ public class ForwardingHandler extends SimpleChannelInboundHandler { private final NettyWebServer webServer; private final SSLEngine sslEngine; private final Queue> queues; + private final HttpRequestDecoder httpRequestDecoder; // this field is always accessed by the very same thread; as such, it doesn't need to be // concurrency aware private RequestContext requestContext; + private boolean isWebSocketUpgrade = false; + ForwardingHandler(Routing routing, NettyWebServer webServer, SSLEngine sslEngine, - Queue> queues) { + Queue> queues, + HttpRequestDecoder httpRequestDecoder) { this.routing = routing; this.webServer = webServer; this.sslEngine = sslEngine; this.queues = queues; + this.httpRequestDecoder = httpRequestDecoder; } @Override @@ -145,6 +156,16 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { send400BadRequest(ctx, e.getMessage()); return; } + + // If WebSockets upgrade, re-arrange pipeline and drop HTTP decoder + if (bareResponse.isWebSocketUpgrade()) { + LOGGER.fine("Replacing HttpRequestDecoder by WebSocketServerProtocolHandler"); + ctx.pipeline().replace(httpRequestDecoder, "webSocketsHandler", + new WebSocketServerProtocolHandler(bareRequest.uri().getPath(), null, true)); + removeHandshakeHandler(ctx); // already done by Tyrus + isWebSocketUpgrade = true; + return; + } } if (msg instanceof HttpContent) { @@ -180,11 +201,48 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { if (msg instanceof LastHttpContent) { requestContext.publisher().complete(); requestContext = null; // just to be sure that current http req/res session doesn't interfere with other ones + if (!isWebSocketUpgrade) { + requestContext.publisher().complete(); + requestContext = null; // just to be sure that current http req/res session doesn't interfere with other ones + } } else if (!content.isReadable()) { // this is here to handle the case when the content is not readable but we didn't // exceptionally complete the publisher and close the connection throw new IllegalStateException("It is not expected to not have readable content."); } + + // We receive a raw bytebuf if connection was upgraded to WebSockets + if (msg instanceof ByteBuf) { + if (!isWebSocketUpgrade) { + throw new IllegalStateException("Received ByteBuf without upgrading to WebSockets"); + } + // Simply forward raw bytebuf to Tyrus for processing + LOGGER.fine("Received ByteBuf of WebSockets connection" + msg); + requestContext.publisher().submit((ByteBuf) msg); + } + } + } + + /** + * Find and remove the WebSockets handshake handler. Note that the handler's implementation + * class is package private, so we look for it by name. Handshake is done in Helidon using + * Tyrus' code instead of here. + * + * @param ctx Channel handler context. + */ + private static void removeHandshakeHandler(ChannelHandlerContext ctx) { + ChannelHandler handshakeHandler = null; + for (Iterator> it = ctx.pipeline().iterator(); it.hasNext(); ) { + ChannelHandler handler = it.next().getValue(); + if (handler.getClass().getName().endsWith("WebSocketServerProtocolHandshakeHandler")) { + handshakeHandler = handler; + break; + } + } + if (handshakeHandler != null) { + ctx.pipeline().remove(handshakeHandler); + } else { + LOGGER.warning("Unable to remove WebSockets handshake handler from pipeline"); } } From 423a91e00393b34df7174395988abec4ccd62778 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 8 Jan 2020 09:24:21 -0500 Subject: [PATCH 02/35] Moved new dependencies to parent POM, fixed copyright and checkstyle problems. Signed-off-by: Santiago Pericas-Geertsen --- dependencies/pom.xml | 28 +++++++++-- webserver/tyrus/pom.xml | 10 +--- .../tyrus/TyrusReaderSubscriber.java | 4 +- .../helidon/webserver/tyrus/TyrusSupport.java | 8 ++-- .../webserver/tyrus/TyrusWriterPublisher.java | 1 + .../test/resources/logging-test.properties | 2 +- .../helidon/webserver/BareResponseImpl.java | 48 ++++++++++++++++--- .../io/helidon/webserver/HttpInitializer.java | 5 +- 8 files changed, 80 insertions(+), 26 deletions(-) diff --git a/dependencies/pom.xml b/dependencies/pom.xml index b375ff3e9dd..a2a27c6373a 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -45,6 +45,7 @@ 3.0.0 3.0.0 2.17.0 + 2.3.1 1.30.5 19.2.0 1.25.0 @@ -53,6 +54,7 @@ 1.3 5.4.4.Final 3.4.1 + 1.5.18 1 2.10.0 1.1.0 @@ -95,13 +97,13 @@ 1.24 1.2 1.4.0 + 1.15 19.3.0.0 2.0.1.Final + 1.1.1 3.1.1.Final - 2.12.0 - 1.5.18 - 2.3.1 1.0.3 + 2.12.0 @@ -340,6 +342,26 @@ reactive-streams ${version.lib.reactivestreams} + + jakarta.websocket + jakarta.websocket-api + ${version.lib.websockets-api} + + + org.glassfish.tyrus + tyrus-core + ${version.lib.tyrus} + + + org.glassfish.tyrus + tyrus-client + ${version.lib.tyrus} + + + org.glassfish.tyrus + tyrus-server + ${version.lib.tyrus} + diff --git a/webserver/tyrus/pom.xml b/webserver/tyrus/pom.xml index f82213d7476..afad9bb2ce2 100644 --- a/webserver/tyrus/pom.xml +++ b/webserver/tyrus/pom.xml @@ -28,11 +28,7 @@ helidon-webserver-tyrus - - - 1.1.1 - 1.15 - + Helidon WebServer Tyrus @@ -50,22 +46,18 @@ jakarta.websocket jakarta.websocket-api - ${websocket-api-version} org.glassfish.tyrus tyrus-core - ${tyrus-version} org.glassfish.tyrus tyrus-client - ${tyrus-version} org.glassfish.tyrus tyrus-server - ${tyrus-version} io.helidon.webserver diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java index da5d22892a0..8a01e75d593 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java @@ -16,12 +16,14 @@ package io.helidon.webserver.tyrus; -import javax.websocket.CloseReason; import java.nio.ByteBuffer; import java.util.concurrent.Flow; import java.util.logging.Logger; +import javax.websocket.CloseReason; + import io.helidon.common.http.DataChunk; + import org.glassfish.tyrus.spi.Connection; import static javax.websocket.CloseReason.CloseCodes.NORMAL_CLOSURE; diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index 75d12fb2c57..7a7d87f3dfc 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -16,9 +16,6 @@ package io.helidon.webserver.tyrus; -import javax.websocket.DeploymentException; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpointConfig; import java.net.URI; import java.nio.ByteBuffer; import java.util.HashSet; @@ -26,11 +23,16 @@ import java.util.Set; import java.util.logging.Logger; +import javax.websocket.DeploymentException; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; + import io.helidon.webserver.Handler; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; + import org.glassfish.tyrus.core.RequestContext; import org.glassfish.tyrus.core.TyrusUpgradeResponse; import org.glassfish.tyrus.core.TyrusWebSocketEngine; diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java index dbef6b97f3d..0d5c6fa3501 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusWriterPublisher.java @@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicLong; import io.helidon.common.http.DataChunk; + import org.glassfish.tyrus.spi.CompletionHandler; import org.glassfish.tyrus.spi.Writer; diff --git a/webserver/tyrus/src/test/resources/logging-test.properties b/webserver/tyrus/src/test/resources/logging-test.properties index aa55aa774dc..4aa62b27a15 100644 --- a/webserver/tyrus/src/test/resources/logging-test.properties +++ b/webserver/tyrus/src/test/resources/logging-test.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java b/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java index b8ff5fe9235..53c2908a8b1 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java @@ -78,6 +78,7 @@ class BareResponseImpl implements BareResponse { private volatile DataChunk firstChunk; private volatile DefaultHttpResponse response; private volatile boolean lengthOptimization; + private volatile Boolean isWebSocketUpgrade; /** * @param ctx the channel handler context @@ -143,17 +144,24 @@ public void writeStatusAndHeaders(Http.ResponseStatus status, Map header.startsWith(HTTP_2_HEADER_PREFIX)) .forEach(header -> response.headers().add(header, requestHeaders.get(header))); - // Set chunked if length not set, may switch to length later - boolean lengthSet = HttpUtil.isContentLengthSet(response); - if (!lengthSet) { - lengthOptimization = status.code() == Http.Status.OK_200.code() - && !HttpUtil.isTransferEncodingChunked(response); - HttpUtil.setTransferEncodingChunked(response, true); + // Check if WebSocket upgrade + boolean isUpgrade = isWebSocketUpgrade(status, headers); + if (isUpgrade) { + isWebSocketUpgrade = true; + } else { + // Set chunked if length not set, may switch to length later + boolean lengthSet = HttpUtil.isContentLengthSet(response); + if (!lengthSet) { + lengthOptimization = status.code() == Http.Status.OK_200.code() + && !HttpUtil.isTransferEncodingChunked(response); + HttpUtil.setTransferEncodingChunked(response, true); + } } // Add keep alive header as per: // http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection - if (keepAlive) { + // If already set (e.g. WebSocket upgrade), do not override + if (keepAlive && !headers.containsKey(HttpHeaderNames.CONNECTION.toString())) { response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } @@ -164,6 +172,24 @@ public void writeStatusAndHeaders(Http.ResponseStatus status, Map> headers) { + return status.code() == 101 && headers.containsKey("Upgrade") + && headers.get("Upgrade").contains("websocket"); + } + + /** + * Determines if response is a WebSockets upgrade. + * + * @return Outcome of test. + * @throws IllegalStateException If headers not written yet. + */ + boolean isWebSocketUpgrade() { + if (isWebSocketUpgrade == null) { + throw new IllegalStateException("Status and response headers not written"); + } + return isWebSocketUpgrade; + } + /** * Completes {@code responseFuture} instance to signal that this response is done. * Prefer to use {@link #completeInternal(Throwable)} to cover whole completion process. @@ -318,6 +344,14 @@ private ChannelFuture sendData(DataChunk data) { return channelFuture .addListener(future -> { + data.writeFuture().ifPresent(writeFuture -> { + // Complete write future based con channel future + if (future.isSuccess()) { + writeFuture.complete(data); + } else { + writeFuture.completeExceptionally(future.cause()); + } + }); data.release(); LOGGER.finest(() -> log("Data chunk sent with result: " + future.isSuccess())); }) diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java b/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java index 33c629d76bf..8774509e7b7 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java @@ -83,6 +83,7 @@ public void initChannel(SocketChannel ch) { // Set up HTTP/2 pipeline if feature is enabled ServerConfiguration serverConfig = webServer.configuration(); + HttpRequestDecoder requestDecoder = new HttpRequestDecoder(); if (serverConfig.isHttp2Enabled()) { ExperimentalConfiguration experimental = serverConfig.experimental(); Http2Configuration http2Config = experimental.http2(); @@ -100,7 +101,7 @@ public void initChannel(SocketChannel ch) { p.addLast(cleartextHttp2ServerUpgradeHandler); p.addLast(new HelidonEventLogger()); } else { - p.addLast(new HttpRequestDecoder()); + p.addLast(requestDecoder); // Uncomment the following line if you don't want to handle HttpChunks. // p.addLast(new HttpObjectAggregator(1048576)); p.addLast(new HttpResponseEncoder()); @@ -109,7 +110,7 @@ public void initChannel(SocketChannel ch) { } // Helidon's forwarding handler - p.addLast(new ForwardingHandler(routing, webServer, sslEngine, queues)); + p.addLast(new ForwardingHandler(routing, webServer, sslEngine, queues, requestDecoder)); // Cleanup queues as part of event loop ch.eventLoop().execute(this::clearQueues); From 0b717818ed3d86e8f59c6294dc1ee1ee94fbff33 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 8 Jan 2020 11:25:38 -0500 Subject: [PATCH 03/35] Some changes to the webserver engine to handle websocket connections. Signed-off-by: Santiago Pericas-Geertsen --- .../io/helidon/webserver/BareResponseImpl.java | 5 +---- .../helidon/webserver/ForwardingHandler.java | 18 ++++++++---------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java b/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java index 53c2908a8b1..3a5ecadb644 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java @@ -78,7 +78,7 @@ class BareResponseImpl implements BareResponse { private volatile DataChunk firstChunk; private volatile DefaultHttpResponse response; private volatile boolean lengthOptimization; - private volatile Boolean isWebSocketUpgrade; + private volatile boolean isWebSocketUpgrade = false; /** * @param ctx the channel handler context @@ -184,9 +184,6 @@ private boolean isWebSocketUpgrade(Http.ResponseStatus status, Map Date: Wed, 8 Jan 2020 11:42:04 -0500 Subject: [PATCH 04/35] Fixed copyright and checkstyle. Signed-off-by: Santiago Pericas-Geertsen --- .../main/java/io/helidon/webserver/BareResponseImpl.java | 2 +- .../java/io/helidon/webserver/ForwardingHandler.java | 9 ++++----- .../main/java/io/helidon/webserver/HttpInitializer.java | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java b/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java index 3a5ecadb644..89367fe2fb2 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/BareResponseImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java index c7aae776d50..18fd9ffb46c 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,15 @@ package io.helidon.webserver; -import javax.net.ssl.SSLEngine; - import java.nio.charset.StandardCharsets; - import java.util.Iterator; import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; +import javax.net.ssl.SSLEngine; + import io.helidon.common.http.DataChunk; import io.netty.buffer.ByteBuf; @@ -230,7 +229,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { */ private static void removeHandshakeHandler(ChannelHandlerContext ctx) { ChannelHandler handshakeHandler = null; - for (Iterator> it = ctx.pipeline().iterator(); it.hasNext(); ) { + for (Iterator> it = ctx.pipeline().iterator(); it.hasNext();) { ChannelHandler handler = it.next().getValue(); if (handler.getClass().getName().endsWith("WebSocketServerProtocolHandshakeHandler")) { handshakeHandler = handler; diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java b/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java index 8774509e7b7..189398080d6 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/HttpInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From c34dce5495edeb056850c8a08aaadaff0d61ddf4 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 8 Jan 2020 12:50:14 -0500 Subject: [PATCH 05/35] A few more copyright fixes. Signed-off-by: Santiago Pericas-Geertsen --- common/http/src/main/java/io/helidon/common/http/DataChunk.java | 2 +- webserver/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/http/src/main/java/io/helidon/common/http/DataChunk.java b/common/http/src/main/java/io/helidon/common/http/DataChunk.java index 41baa14b010..ab42e31ad8b 100644 --- a/common/http/src/main/java/io/helidon/common/http/DataChunk.java +++ b/common/http/src/main/java/io/helidon/common/http/DataChunk.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/webserver/pom.xml b/webserver/pom.xml index 65a32d506f1..b4976ae1fde 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -1,7 +1,7 @@ io.helidon.jersey @@ -421,6 +426,11 @@ helidon-microprofile-access-log ${helidon.version} + + io.helidon.microprofile.tyrus + helidon-microprofile-tyrus + ${helidon.version} + io.helidon.metrics diff --git a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java index c168b33e118..4bf4a46d19e 100644 --- a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java +++ b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java @@ -16,8 +16,10 @@ package io.helidon.microprofile.cdi; import java.lang.annotation.Annotation; +import java.net.URL; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.ServiceLoader; import java.util.UUID; @@ -138,7 +140,13 @@ private HelidonContainerImpl init() { addHelidonBeanDefiningAnnotations(); - ResourceLoader resourceLoader = new WeldResourceLoader(); + ResourceLoader resourceLoader = new WeldResourceLoader() { + @Override + public Collection getResources(String name) { + Collection resources = super.getResources(name); + return new HashSet<>(resources); // drops duplicates when using patch-module + } + }; setResourceLoader(resourceLoader); Config config = (Config) ConfigProvider.getConfig(); diff --git a/microprofile/fault-tolerance/src/main/java/module-info.java b/microprofile/fault-tolerance/src/main/java/module-info.java index 6d0a0ded113..637b3ce4408 100644 --- a/microprofile/fault-tolerance/src/main/java/module-info.java +++ b/microprofile/fault-tolerance/src/main/java/module-info.java @@ -39,5 +39,7 @@ requires microprofile.metrics.api; requires microprofile.fault.tolerance.api; + exports io.helidon.microprofile.faulttolerance; + provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.faulttolerance.FaultToleranceExtension; } diff --git a/microprofile/pom.xml b/microprofile/pom.xml index 99b25c51b0e..abba9f7a396 100644 --- a/microprofile/pom.xml +++ b/microprofile/pom.xml @@ -49,5 +49,6 @@ grpc cdi weld + tyrus diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index 97d9764afee..26fad883bf0 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -52,6 +52,10 @@ io.helidon.common helidon-common-service-loader + + io.helidon.microprofile.tyrus + helidon-microprofile-tyrus + javax.interceptor javax.interceptor-api diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index 6e9ca8f86c7..21f5668be92 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -49,6 +49,8 @@ import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.microprofile.cdi.RuntimeStart; +import io.helidon.microprofile.tyrus.WebSocketApplication; +import io.helidon.microprofile.tyrus.WebSocketCdiExtension; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; import io.helidon.webserver.Service; @@ -56,6 +58,7 @@ import io.helidon.webserver.StaticContentSupport; import io.helidon.webserver.WebServer; import io.helidon.webserver.jersey.JerseySupport; +import io.helidon.webserver.tyrus.TyrusSupport; import org.eclipse.microprofile.config.ConfigProvider; @@ -117,6 +120,9 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( // register static content if configured registerStaticContent(); + // register websocket endpoints + registerWebSockets(beanManager, serverConfig); + // start the webserver WebServer.Builder wsBuilder = WebServer.builder(routingBuilder.build()); wsBuilder.config(serverConfig); @@ -194,6 +200,20 @@ private void registerPathStaticContent(Config config) { } } + private void registerWebSockets(BeanManager beanManager, ServerConfiguration serverConfig) { + try { + WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class); + WebSocketApplication app = extension.toWebSocketApplication().build(); + TyrusSupport.Builder builder = TyrusSupport.builder(); + app.endpointClasses().forEach(builder::register); + app.endpointConfigs().forEach(builder::register); + Routing.Builder routing = serverRoutingBuilder(); + routing.register("/websocket", builder.build()); + } catch (IllegalArgumentException e) { + LOGGER.fine("Unable to load WebSocket extension"); + } + } + private void registerClasspathStaticContent(Config config) { Config context = config.get("context"); diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index 38415c5c6bf..bc37e5d4af1 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -37,6 +37,8 @@ // there is now a hardcoded dependency on Weld, to configure additional bean defining annotation requires java.management; + requires io.helidon.microprofile.tyrus; + requires io.helidon.webserver.tyrus; exports io.helidon.microprofile.server; diff --git a/microprofile/tyrus/pom.xml b/microprofile/tyrus/pom.xml new file mode 100644 index 00000000000..bb877951467 --- /dev/null +++ b/microprofile/tyrus/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.microprofile + helidon-microprofile-project + 2.0-SNAPSHOT + + + io.helidon.microprofile.tyrus + helidon-microprofile-tyrus + Helidon MicroProfile Tyrus + + Helidon MP integration with Tyrus + + + + io.helidon.webserver + helidon-webserver-tyrus + + + io.helidon.common + helidon-common + + + jakarta.websocket + jakarta.websocket-api + + + javax.enterprise + cdi-api + provided + + + io.helidon.microprofile.bundles + internal-test-libs + test + + + io.helidon.microprofile.cdi + helidon-microprofile-cdi + test + + + diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java new file mode 100644 index 00000000000..a781ca2bb3e --- /dev/null +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import java.util.ArrayList; +import java.util.List; + +import javax.websocket.server.ServerEndpoint; +import javax.websocket.server.ServerEndpointConfig; + +/** + * Represents a websocket application with class and config endpoints. + */ +public final class WebSocketApplication { + + private List> endpointClasses; + private List> endpointConfigs; + + /** + * Creates a websocket application from classes and configs. + * + * @param classesOrConfigs Classes and configs. + * @return A websocket application. + */ + @SuppressWarnings("unchecked") + public static WebSocketApplication create(Class... classesOrConfigs) { + Builder builder = new Builder(); + for (Class c : classesOrConfigs) { + if (ServerEndpointConfig.class.isAssignableFrom(c)) { + builder.endpointConfig((Class) c); + } else if (c.isAnnotationPresent(ServerEndpoint.class)) { + builder.endpointClass(c); + } else { + throw new IllegalArgumentException("Unable to create WebSocket application using " + c); + } + } + return builder.build(); + } + + private WebSocketApplication(Builder builder) { + this.endpointConfigs = builder.endpointConfigs; + this.endpointClasses = builder.endpointClasses; + } + + /** + * A new fluent API builder to create a customized {@link WebSocketApplication}. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Get list of config endpoints. + * + * @return List of config endpoints. + */ + public List> endpointConfigs() { + return endpointConfigs; + } + + /** + * Get list of endpoint classes. + * + * @return List of endpoint classes. + */ + public List> endpointClasses() { + return endpointClasses; + } + + /** + * Fluent API builder to create {@link WebSocketApplication} instances. + */ + public static class Builder { + + private List> endpointClasses = new ArrayList<>(); + private List> endpointConfigs = new ArrayList<>(); + + /** + * Add single config endpoint. + * + * @param endpointConfig Endpoint config. + * @return The builder. + */ + public Builder endpointConfig(Class endpointConfig) { + endpointConfigs.add(endpointConfig); + return this; + } + + /** + * Add single endpoint class. + * + * @param endpointClass Endpoint class. + * @return The builder. + */ + public Builder endpointClass(Class endpointClass) { + endpointClasses.add(endpointClass); + return this; + } + + /** + * Builds application. + * + * @return The application. + */ + public WebSocketApplication build() { + return new WebSocketApplication(this); + } + } +} diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java new file mode 100644 index 00000000000..a7d39728aa3 --- /dev/null +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import java.util.logging.Logger; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.inject.spi.WithAnnotations; +import javax.websocket.server.ServerEndpoint; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; + +/** + * Configure Tyrus related things. + */ +public class WebSocketCdiExtension implements Extension { + private static final Logger LOGGER = Logger.getLogger(WebSocketCdiExtension.class.getName()); + + static { + HelidonFeatures.register(HelidonFlavor.MP, "WebSocket"); + } + + private WebSocketApplication.Builder applicationBuilder = WebSocketApplication.builder(); + + private void endpoints(@Observes @WithAnnotations(ServerEndpoint.class) ProcessAnnotatedType applicationType) { + LOGGER.info(() -> "Annotated endpoint found " + applicationType.getAnnotatedType().getJavaClass()); + applicationBuilder.endpointClass(applicationType.getAnnotatedType().getJavaClass()); + } + + /** + * Provides access to websocket application builder. + * + * @return Application builder. + */ + public WebSocketApplication.Builder toWebSocketApplication() { + return applicationBuilder; + } +} diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/package-info.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/package-info.java new file mode 100644 index 00000000000..beb6cc51008 --- /dev/null +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Helidon MP integration with Tyrus. + */ +package io.helidon.microprofile.tyrus; diff --git a/microprofile/tyrus/src/main/java/module-info.java b/microprofile/tyrus/src/main/java/module-info.java new file mode 100644 index 00000000000..957de671eaf --- /dev/null +++ b/microprofile/tyrus/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * MP Tyrus Integration + */ +module io.helidon.microprofile.tyrus { + requires java.logging; + requires java.annotation; + requires javax.inject; + requires javax.interceptor.api; + + requires cdi.api; + requires transitive jakarta.websocket.api; + + requires io.helidon.common; + + exports io.helidon.microprofile.tyrus; +} diff --git a/microprofile/tyrus/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/microprofile/tyrus/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..8015de12bc5 --- /dev/null +++ b/microprofile/tyrus/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.microprofile.tyrus.WebSocketCdiExtension \ No newline at end of file diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java new file mode 100644 index 00000000000..040f4fa60c6 --- /dev/null +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.logging.Logger; + +import org.glassfish.tyrus.client.ClientManager; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Class EchoClient. + */ +public class EchoClient { + private static final Logger LOGGER = Logger.getLogger(EchoClient.class.getName()); + + private static final ClientManager client = ClientManager.createClient(); + private static final long TIMEOUT_SECONDS = 10; + + private final URI uri; + private final BiFunction equals; + + public EchoClient(URI uri) { + this(uri, String::equals); + } + + public EchoClient(URI uri, BiFunction equals) { + this.uri = uri; + this.equals = equals; + } + + /** + * Sends each message one by one and compares echoed value ignoring cases. + * + * @param messages Messages to send. + * @throws Exception If an error occurs. + */ + public void echo(String... messages) throws Exception { + CountDownLatch messageLatch = new CountDownLatch(messages.length); + CompletableFuture openFuture = new CompletableFuture<>(); + CompletableFuture closeFuture = new CompletableFuture<>(); + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + client.connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig EndpointConfig) { + openFuture.complete(null); + try { + // Register message handler. Tyrus has problems with lambdas here + // so an inner class with an onMessage method is required. + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String message) { + LOGGER.info("Client OnMessage called '" + message + "'"); + + int index = messages.length - (int) messageLatch.getCount(); + assertTrue(equals.apply(messages[index], message)); + + messageLatch.countDown(); + if (messageLatch.getCount() == 0) { + try { + session.close(); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + } + }); + + // Send message to Echo service + for (String msg : messages) { + session.getBasicRemote().sendText(msg); + } + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + closeFuture.complete(null); + } + }, config, uri); + + openFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + closeFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!messageLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + fail("Timeout expired without receiving echo of all messages"); + } + } +} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java new file mode 100644 index 00000000000..bac07170139 --- /dev/null +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import javax.enterprise.context.Dependent; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +import static io.helidon.microprofile.tyrus.UppercaseCodec.isDecoded; + +/** + * Class EchoEndpoint. Only one instance of this endpoint should be used at + * a time. See static {@code EchoEndpoint#modifyHandshakeCalled}. + */ +@Dependent +@ServerEndpoint( + value = "/echo", + encoders = { UppercaseCodec.class }, + decoders = { UppercaseCodec.class }, + configurator = ServerConfigurator.class +) +public class EchoEndpoint { + private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); + + static AtomicBoolean modifyHandshakeCalled = new AtomicBoolean(false); + + @OnOpen + public void onOpen(Session session) throws IOException { + LOGGER.info("OnOpen called"); + if (!modifyHandshakeCalled.get()) { + session.close(); // unexpected + } + } + + @OnMessage + public void echo(Session session, String message) throws Exception { + LOGGER.info("Endpoint OnMessage called '" + message + "'"); + if (!isDecoded(message)) { + throw new InternalError("Message has not been decoded"); + } + session.getBasicRemote().sendObject(message); // calls encoder + } + + @OnError + public void onError(Throwable t) { + LOGGER.info("OnError called"); + modifyHandshakeCalled.set(false); + } + + @OnClose + public void onClose(Session session) { + LOGGER.info("OnClose called"); + modifyHandshakeCalled.set(false); + } +} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java new file mode 100644 index 00000000000..f47f0dd254e --- /dev/null +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import javax.enterprise.context.Dependent; +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import java.util.logging.Logger; + +/** + * Class ServerConfigurator. + */ +@Dependent +public class ServerConfigurator extends ServerEndpointConfig.Configurator { + private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + LOGGER.info("ServerConfigurator called during handshake"); + super.modifyHandshake(sec, request, response); + EchoEndpoint.modifyHandshakeCalled.set(true); + } +} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java new file mode 100644 index 00000000000..bbcb409d6bb --- /dev/null +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import javax.enterprise.context.Dependent; +import javax.websocket.Decoder; +import javax.websocket.Encoder; +import javax.websocket.EndpointConfig; +import java.util.logging.Logger; + +/** + * Class UppercaseCodec. + */ +@Dependent +public class UppercaseCodec implements Decoder.Text, Encoder.Text { + private static final Logger LOGGER = Logger.getLogger(UppercaseCodec.class.getName()); + + private static final String ENCODING_PREFIX = "\0\0"; + + public UppercaseCodec() { + LOGGER.info("UppercaseCodec instance created"); + } + + @Override + public String decode(String s) { + LOGGER.info("UppercaseCodec decode called"); + return ENCODING_PREFIX + s; + } + + @Override + public boolean willDecode(String s) { + return true; + } + + @Override + public void init(EndpointConfig config) { + } + + @Override + public void destroy() { + } + + @Override + public String encode(String s) { + LOGGER.info("UppercaseCodec encode called"); + return s.replace(ENCODING_PREFIX, ""); + } + + public static boolean isDecoded(String s) { + return s.startsWith(ENCODING_PREFIX); + } +} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java new file mode 100644 index 00000000000..ae9a5c6893b --- /dev/null +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.spi.BeanManager; + +import io.helidon.microprofile.cdi.HelidonContainer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; + +/** + * Class WebSocketExtensionTest. + */ +public class WebSocketCdiExtensionTest { + + private static SeContainer cdiContainer; + + @BeforeAll + public static void startCdiContainer() { + cdiContainer = HelidonContainer.instance().start(); + } + + @AfterAll + public static void shutDownCdiContainer() { + if (cdiContainer != null) { + cdiContainer.close(); + } + } + + private WebSocketApplication webSocketApplication() { + BeanManager beanManager = cdiContainer.getBeanManager(); + WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class); + return extension.toWebSocketApplication().build(); + } + + @Test + public void testExtension() { + WebSocketApplication application = webSocketApplication(); + assertThat(application.endpointClasses().size(), is(greaterThan(0))); + assertThat(application.endpointConfigs().size(), is(0)); + } +} diff --git a/microprofile/tyrus/src/test/resources/META-INF/beans.xml b/microprofile/tyrus/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..74564b22ff3 --- /dev/null +++ b/microprofile/tyrus/src/test/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/microprofile/tyrus/src/test/resources/logging.properties b/microprofile/tyrus/src/test/resources/logging.properties new file mode 100644 index 00000000000..2951b7c71be --- /dev/null +++ b/microprofile/tyrus/src/test/resources/logging.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +.level=INFO + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index e4b3c141ead..602aa03c6d8 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -18,6 +18,7 @@ import java.net.URI; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -55,9 +56,13 @@ public class TyrusSupport implements Service { private final WebSocketEngine engine; private final TyrusHandler handler = new TyrusHandler(); + private Set> endpointClasses; + private Set endpointConfigs; - TyrusSupport(WebSocketEngine engine) { + TyrusSupport(WebSocketEngine engine, Set> endpointClasses, Set endpointConfigs) { this.engine = engine; + this.endpointClasses = endpointClasses; + this.endpointConfigs = endpointConfigs; } /** @@ -72,6 +77,24 @@ public void update(Routing.Rules routingRules) { routingRules.any(handler); } + /** + * Access to endpoint classes. + * + * @return Immutable set of end endpoint classes. + */ + public Set> endpointClasses() { + return Collections.unmodifiableSet(endpointClasses); + } + + /** + * Access to endpoint configs. + * + * @return Immutable set of end endpoint configs. + */ + public Set endpointConfigs() { + return Collections.unmodifiableSet(endpointConfigs); + } + /** * Creates a builder for this class. * @@ -92,12 +115,24 @@ public static final class Builder implements io.helidon.common.Builder endpointClass) { + /** + * Register an endpoint class. + * + * @param endpointClass The class. + * @return The builder. + */ + public Builder register(Class endpointClass) { endpointClasses.add(endpointClass); return this; } - Builder register(ServerEndpointConfig endpointConfig) { + /** + * Register an endpoint config. + * + * @param endpointConfig The endpoint config. + * @return The builder. + */ + public Builder register(ServerEndpointConfig endpointConfig) { endpointConfigs.add(endpointConfig); return this; } @@ -145,7 +180,7 @@ public WebSocketEngine getWebSocketEngine() { }); // Create TyrusSupport using WebSocket engine - return new TyrusSupport(serverContainer.getWebSocketEngine()); + return new TyrusSupport(serverContainer.getWebSocketEngine(), endpointClasses, endpointConfigs); } } From ccbda1eb35cc155741251e253676f3535c606f18 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 22 Jan 2020 11:45:59 -0500 Subject: [PATCH 13/35] Tyrus component provider to integrate with CDI. Changes to JerseySupport to ignore websocket handshakes. MP server test. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/server/pom.xml | 2 +- .../server/ServerCdiExtension.java | 1 + .../server/src/main/java/module-info.java | 12 +- .../microprofile/server/WebSocketTest.java | 188 ++++++++++++++++++ .../src/test/resources/META-INF/beans.xml | 26 +++ microprofile/tyrus/pom.xml | 13 +- .../tyrus/HelidonComponentProvider.java | 50 +++++ .../tyrus/WebSocketCdiExtension.java | 15 +- .../tyrus/src/main/java/module-info.java | 3 +- ...org.glassfish.tyrus.core.ComponentProvider | 17 ++ .../microprofile/tyrus/EchoClient.java | 118 ----------- .../microprofile/tyrus/EchoEndpoint.java | 45 +---- .../tyrus/ServerConfigurator.java | 38 ---- .../microprofile/tyrus/UppercaseCodec.java | 66 ------ .../webserver/jersey/JerseySupport.java | 9 + 15 files changed, 321 insertions(+), 282 deletions(-) create mode 100644 microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java create mode 100644 microprofile/server/src/test/resources/META-INF/beans.xml create mode 100644 microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java create mode 100644 microprofile/tyrus/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index 26fad883bf0..51696df8c60 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -59,7 +59,7 @@ javax.interceptor javax.interceptor-api - provided> + provided javax.ws.rs diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index 21f5668be92..94c04866b59 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -300,6 +300,7 @@ private void addApplication(ServerConfiguration serverConfig, JaxRsCdiExtension } } + @SuppressWarnings("unchecked") private void registerWebServerServices(BeanManager beanManager, ServerConfiguration serverConfig) { List> beans = prioritySort(beanManager.getBeans(Service.class)); diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index bc37e5d4af1..cd480313101 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -14,8 +14,6 @@ * limitations under the License. */ -import io.helidon.microprofile.server.JaxRsCdiExtension; - /** * Implementation of a layer that binds microprofile components together and * runs an HTTP server. @@ -28,8 +26,8 @@ requires transitive io.helidon.microprofile.cdi; - requires transitive cdi.api; - requires transitive java.ws.rs; + requires cdi.api; + requires java.ws.rs; requires javax.interceptor.api; requires java.logging; @@ -37,12 +35,16 @@ // there is now a hardcoded dependency on Weld, to configure additional bean defining annotation requires java.management; + requires io.helidon.microprofile.tyrus; requires io.helidon.webserver.tyrus; + requires jakarta.websocket.api; exports io.helidon.microprofile.server; - provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.server.ServerCdiExtension, JaxRsCdiExtension; + provides javax.enterprise.inject.spi.Extension with + io.helidon.microprofile.server.ServerCdiExtension, + io.helidon.microprofile.server.JaxRsCdiExtension; // needed when running with modules - to make private methods accessible opens io.helidon.microprofile.server to weld.core.impl; diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java new file mode 100644 index 00000000000..52f705e278e --- /dev/null +++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.server; + +import javax.enterprise.context.Dependent; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.logging.Logger; + +import org.glassfish.tyrus.client.ClientManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Class EchoEndpointTest. + */ +public class WebSocketTest { + + private static Server server; + + @BeforeAll + static void initClass() { + server = Server.create(); + server.start(); + } + + @AfterAll + static void destroyClass() { + server.stop(); + } + + @Test + public void testEcho() throws Exception { + URI echoUri = URI.create("ws://localhost:" + server.port() + "/websocket/echo"); + EchoClient echoClient = new EchoClient(echoUri); + echoClient.echo("hi", "how are you?"); + } + + @Dependent + @ServerEndpoint("/echo") + public static class EchoEndpoint { + private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); + + @OnOpen + public void onOpen(Session session) throws IOException { + LOGGER.info("OnOpen called"); + } + + @OnMessage + public void echo(Session session, String message) throws Exception { + LOGGER.info("Endpoint OnMessage called '" + message + "'"); + session.getBasicRemote().sendObject(message); + } + + @OnError + public void onError(Throwable t) { + LOGGER.info("OnError called"); + } + + @OnClose + public void onClose(Session session) { + LOGGER.info("OnClose called"); + } + } + + /** + * Class EchoClient. + */ + static class EchoClient { + private static final Logger LOGGER = Logger.getLogger(EchoClient.class.getName()); + + private static final ClientManager client = ClientManager.createClient(); + private static final long TIMEOUT_SECONDS = 10; + + private final URI uri; + private final BiFunction equals; + + public EchoClient(URI uri) { + this(uri, String::equals); + } + + public EchoClient(URI uri, BiFunction equals) { + this.uri = uri; + this.equals = equals; + } + + /** + * Sends each message one by one and compares echoed value ignoring cases. + * + * @param messages Messages to send. + * @throws Exception If an error occurs. + */ + public void echo(String... messages) throws Exception { + CountDownLatch messageLatch = new CountDownLatch(messages.length); + CompletableFuture openFuture = new CompletableFuture<>(); + CompletableFuture closeFuture = new CompletableFuture<>(); + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + client.connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig EndpointConfig) { + openFuture.complete(null); + try { + // Register message handler. Tyrus has problems with lambdas here + // so an inner class with an onMessage method is required. + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String message) { + LOGGER.info("Client OnMessage called '" + message + "'"); + + int index = messages.length - (int) messageLatch.getCount(); + assertTrue(equals.apply(messages[index], message)); + + messageLatch.countDown(); + if (messageLatch.getCount() == 0) { + try { + session.close(); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + } + }); + + // Send message to Echo service + for (String msg : messages) { + LOGGER.info("Client sending message '" + msg + "'"); + session.getBasicRemote().sendText(msg); + } + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + LOGGER.info("Client OnClose called '" + closeReason + "'"); + closeFuture.complete(null); + } + + @Override + public void onError(Session session, Throwable thr) { + LOGGER.info("Client OnError called '" + thr + "'"); + + } + }, config, uri); + + openFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + closeFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!messageLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + fail("Timeout expired without receiving echo of all messages"); + } + } + } +} diff --git a/microprofile/server/src/test/resources/META-INF/beans.xml b/microprofile/server/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..74564b22ff3 --- /dev/null +++ b/microprofile/server/src/test/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/microprofile/tyrus/pom.xml b/microprofile/tyrus/pom.xml index bb877951467..4f4aaeed970 100644 --- a/microprofile/tyrus/pom.xml +++ b/microprofile/tyrus/pom.xml @@ -32,6 +32,10 @@ Helidon MP integration with Tyrus + + org.glassfish.tyrus + tyrus-core + io.helidon.webserver helidon-webserver-tyrus @@ -40,6 +44,10 @@ io.helidon.common helidon-common + + io.helidon.microprofile.cdi + helidon-microprofile-cdi + jakarta.websocket jakarta.websocket-api @@ -54,10 +62,5 @@ internal-test-libs test - - io.helidon.microprofile.cdi - helidon-microprofile-cdi - test - diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java new file mode 100644 index 00000000000..54f83177902 --- /dev/null +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; + +import org.glassfish.tyrus.core.ComponentProvider; + +/** + * Class HelidonComponentProvider. A service provider for Tyrus to create and destroy + * beans using CDI. + */ +public class HelidonComponentProvider extends ComponentProvider { + + @Override + public boolean isApplicable(Class c) { + BeanManager beanManager = CDI.current().getBeanManager(); + return beanManager.getBeans(c).size() > 0; + } + + @Override + public Object create(Class c) { + return CDI.current().select(c).get(); + } + + @Override + public boolean destroy(Object o) { + try { + CDI.current().destroy(o); + } catch (UnsupportedOperationException e) { + return false; + } + return true; + } +} diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java index a7d39728aa3..0a01ef3fc63 100644 --- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java @@ -37,11 +37,16 @@ public class WebSocketCdiExtension implements Extension { HelidonFeatures.register(HelidonFlavor.MP, "WebSocket"); } - private WebSocketApplication.Builder applicationBuilder = WebSocketApplication.builder(); + private WebSocketApplication.Builder appBuilder = WebSocketApplication.builder(); - private void endpoints(@Observes @WithAnnotations(ServerEndpoint.class) ProcessAnnotatedType applicationType) { - LOGGER.info(() -> "Annotated endpoint found " + applicationType.getAnnotatedType().getJavaClass()); - applicationBuilder.endpointClass(applicationType.getAnnotatedType().getJavaClass()); + /** + * Collects endpoints annotated with {@code ServerEndpoint}. + * + * @param endpoint Type of endpoint. + */ + private void endpoints(@Observes @WithAnnotations(ServerEndpoint.class) ProcessAnnotatedType endpoint) { + LOGGER.info(() -> "Annotated endpoint found " + endpoint.getAnnotatedType().getJavaClass()); + appBuilder.endpointClass(endpoint.getAnnotatedType().getJavaClass()); } /** @@ -50,6 +55,6 @@ private void endpoints(@Observes @WithAnnotations(ServerEndpoint.class) ProcessA * @return Application builder. */ public WebSocketApplication.Builder toWebSocketApplication() { - return applicationBuilder; + return appBuilder; } } diff --git a/microprofile/tyrus/src/main/java/module-info.java b/microprofile/tyrus/src/main/java/module-info.java index 957de671eaf..5bbc225b754 100644 --- a/microprofile/tyrus/src/main/java/module-info.java +++ b/microprofile/tyrus/src/main/java/module-info.java @@ -19,7 +19,6 @@ */ module io.helidon.microprofile.tyrus { requires java.logging; - requires java.annotation; requires javax.inject; requires javax.interceptor.api; @@ -27,6 +26,8 @@ requires transitive jakarta.websocket.api; requires io.helidon.common; + requires tyrus.core; exports io.helidon.microprofile.tyrus; + provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.tyrus.WebSocketCdiExtension; } diff --git a/microprofile/tyrus/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider b/microprofile/tyrus/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider new file mode 100644 index 00000000000..e67036c00c6 --- /dev/null +++ b/microprofile/tyrus/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.microprofile.tyrus.HelidonComponentProvider \ No newline at end of file diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java deleted file mode 100644 index 040f4fa60c6..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.websocket.ClientEndpointConfig; -import javax.websocket.CloseReason; -import javax.websocket.Endpoint; -import javax.websocket.EndpointConfig; -import javax.websocket.MessageHandler; -import javax.websocket.Session; -import java.io.IOException; -import java.net.URI; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.logging.Logger; - -import org.glassfish.tyrus.client.ClientManager; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * Class EchoClient. - */ -public class EchoClient { - private static final Logger LOGGER = Logger.getLogger(EchoClient.class.getName()); - - private static final ClientManager client = ClientManager.createClient(); - private static final long TIMEOUT_SECONDS = 10; - - private final URI uri; - private final BiFunction equals; - - public EchoClient(URI uri) { - this(uri, String::equals); - } - - public EchoClient(URI uri, BiFunction equals) { - this.uri = uri; - this.equals = equals; - } - - /** - * Sends each message one by one and compares echoed value ignoring cases. - * - * @param messages Messages to send. - * @throws Exception If an error occurs. - */ - public void echo(String... messages) throws Exception { - CountDownLatch messageLatch = new CountDownLatch(messages.length); - CompletableFuture openFuture = new CompletableFuture<>(); - CompletableFuture closeFuture = new CompletableFuture<>(); - ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); - - client.connectToServer(new Endpoint() { - @Override - public void onOpen(Session session, EndpointConfig EndpointConfig) { - openFuture.complete(null); - try { - // Register message handler. Tyrus has problems with lambdas here - // so an inner class with an onMessage method is required. - session.addMessageHandler(new MessageHandler.Whole() { - @Override - public void onMessage(String message) { - LOGGER.info("Client OnMessage called '" + message + "'"); - - int index = messages.length - (int) messageLatch.getCount(); - assertTrue(equals.apply(messages[index], message)); - - messageLatch.countDown(); - if (messageLatch.getCount() == 0) { - try { - session.close(); - } catch (IOException e) { - fail("Unexpected exception " + e); - } - } - } - }); - - // Send message to Echo service - for (String msg : messages) { - session.getBasicRemote().sendText(msg); - } - } catch (IOException e) { - fail("Unexpected exception " + e); - } - } - - @Override - public void onClose(Session session, CloseReason closeReason) { - closeFuture.complete(null); - } - }, config, uri); - - openFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - closeFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!messageLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - fail("Timeout expired without receiving echo of all messages"); - } - } -} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java index bac07170139..a7ce0ab4b33 100644 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java @@ -17,60 +17,19 @@ package io.helidon.microprofile.tyrus; import javax.enterprise.context.Dependent; -import javax.websocket.OnClose; -import javax.websocket.OnError; import javax.websocket.OnMessage; -import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Logger; - -import static io.helidon.microprofile.tyrus.UppercaseCodec.isDecoded; /** - * Class EchoEndpoint. Only one instance of this endpoint should be used at - * a time. See static {@code EchoEndpoint#modifyHandshakeCalled}. + * Class EchoEndpoint. */ @Dependent -@ServerEndpoint( - value = "/echo", - encoders = { UppercaseCodec.class }, - decoders = { UppercaseCodec.class }, - configurator = ServerConfigurator.class -) +@ServerEndpoint("/echo") public class EchoEndpoint { - private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); - - static AtomicBoolean modifyHandshakeCalled = new AtomicBoolean(false); - - @OnOpen - public void onOpen(Session session) throws IOException { - LOGGER.info("OnOpen called"); - if (!modifyHandshakeCalled.get()) { - session.close(); // unexpected - } - } @OnMessage public void echo(Session session, String message) throws Exception { - LOGGER.info("Endpoint OnMessage called '" + message + "'"); - if (!isDecoded(message)) { - throw new InternalError("Message has not been decoded"); - } session.getBasicRemote().sendObject(message); // calls encoder } - - @OnError - public void onError(Throwable t) { - LOGGER.info("OnError called"); - modifyHandshakeCalled.set(false); - } - - @OnClose - public void onClose(Session session) { - LOGGER.info("OnClose called"); - modifyHandshakeCalled.set(false); - } } diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java deleted file mode 100644 index f47f0dd254e..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/ServerConfigurator.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.enterprise.context.Dependent; -import javax.websocket.HandshakeResponse; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpointConfig; -import java.util.logging.Logger; - -/** - * Class ServerConfigurator. - */ -@Dependent -public class ServerConfigurator extends ServerEndpointConfig.Configurator { - private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); - - @Override - public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { - LOGGER.info("ServerConfigurator called during handshake"); - super.modifyHandshake(sec, request, response); - EchoEndpoint.modifyHandshakeCalled.set(true); - } -} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java deleted file mode 100644 index bbcb409d6bb..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/UppercaseCodec.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.enterprise.context.Dependent; -import javax.websocket.Decoder; -import javax.websocket.Encoder; -import javax.websocket.EndpointConfig; -import java.util.logging.Logger; - -/** - * Class UppercaseCodec. - */ -@Dependent -public class UppercaseCodec implements Decoder.Text, Encoder.Text { - private static final Logger LOGGER = Logger.getLogger(UppercaseCodec.class.getName()); - - private static final String ENCODING_PREFIX = "\0\0"; - - public UppercaseCodec() { - LOGGER.info("UppercaseCodec instance created"); - } - - @Override - public String decode(String s) { - LOGGER.info("UppercaseCodec decode called"); - return ENCODING_PREFIX + s; - } - - @Override - public boolean willDecode(String s) { - return true; - } - - @Override - public void init(EndpointConfig config) { - } - - @Override - public void destroy() { - } - - @Override - public String encode(String s) { - LOGGER.info("UppercaseCodec encode called"); - return s.replace(ENCODING_PREFIX, ""); - } - - public static boolean isDecoded(String s) { - return s.startsWith(ENCODING_PREFIX); - } -} diff --git a/webserver/jersey/src/main/java/io/helidon/webserver/jersey/JerseySupport.java b/webserver/jersey/src/main/java/io/helidon/webserver/jersey/JerseySupport.java index 7e0f7b95cff..482643ec858 100644 --- a/webserver/jersey/src/main/java/io/helidon/webserver/jersey/JerseySupport.java +++ b/webserver/jersey/src/main/java/io/helidon/webserver/jersey/JerseySupport.java @@ -83,6 +83,8 @@ */ public class JerseySupport implements Service { + private static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + /** * The request scoped span qualifier that can be injected into a Jersey resource. *

@@ -231,6 +233,13 @@ private class JerseyHandler implements Handler {
 
         @Override
         public void accept(ServerRequest req, ServerResponse res) {
+            // Skip this handler if a WebSocket upgrade request
+            Optional secWebSocketKey = req.headers().value(SEC_WEBSOCKET_KEY);
+            if (secWebSocketKey.isPresent()) {
+                req.next();
+                return;
+            }
+
             // create a new context for jersey, so we do not modify webserver's internals
             Context parent = Contexts.context()
                     .orElseThrow(() -> new IllegalStateException("Context must be propagated from server"));

From 507c2df2889ddc995c350621c75cee738f442b44 Mon Sep 17 00:00:00 2001
From: Santiago Pericas-Geertsen 
Date: Thu, 23 Jan 2020 11:03:08 -0500
Subject: [PATCH 14/35] Support for scanning programmatic endpoints and
 applications. Allow @ApplicationPath annotation on subclasses of
 ServerApplicationConfig. Some test changes.

---
 .../helidon/microprofile/server/Server.java   | 14 +++
 .../server/ServerCdiExtension.java            | 47 ++++++++-
 .../tyrus/WebSocketApplication.java           | 98 ++++++++++---------
 .../tyrus/WebSocketCdiExtension.java          | 38 +++++--
 .../microprofile/tyrus/EchoEndpoint.java      |  2 +-
 .../microprofile/tyrus/EchoEndpointProg.java  | 43 ++++++++
 .../tyrus/EndpointApplication.java            | 40 ++++++++
 .../tyrus/WebSocketCdiExtensionTest.java      | 18 ++--
 8 files changed, 231 insertions(+), 69 deletions(-)
 create mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java
 create mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java

diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java
index 11b4d97003f..c04b7486416 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java
@@ -30,6 +30,7 @@
 import io.helidon.common.configurable.ServerThreadPoolSupplier;
 import io.helidon.common.context.Contexts;
 import io.helidon.microprofile.cdi.HelidonContainer;
+import io.helidon.microprofile.tyrus.WebSocketApplication;
 
 import org.eclipse.microprofile.config.Config;
 import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
@@ -136,6 +137,7 @@ final class Builder {
 
         private final List> resourceClasses = new LinkedList<>();
         private final List applications = new LinkedList<>();
+        private final List wsApplications = new LinkedList<>();
         private Config config;
         private String host;
         private String basePath;
@@ -366,6 +368,18 @@ public Builder addApplication(Application application) {
             return this;
         }
 
+        /**
+         * Adds a WebSocket application to the server. If more than one application is added, they
+         * must be registered on different paths.
+         *
+         * @param wsApplication websocket application
+         * @return modified builder
+         */
+        public Builder addApplication(WebSocketApplication wsApplication) {
+            this.wsApplications.add(wsApplication);
+            return this;
+        }
+
         /**
          * If any application or resource class is added through this builder, applications discovered by CDI are ignored.
          * You can change this behavior by setting the retain discovered applications to {@code true}.
diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
index 94c04866b59..8b82717b1b7 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
@@ -39,8 +39,12 @@
 import javax.enterprise.event.Observes;
 import javax.enterprise.inject.spi.Bean;
 import javax.enterprise.inject.spi.BeanManager;
+import javax.enterprise.inject.spi.CDI;
 import javax.enterprise.inject.spi.DeploymentException;
 import javax.enterprise.inject.spi.Extension;
+import javax.websocket.server.ServerApplicationConfig;
+import javax.websocket.server.ServerEndpointConfig;
+import javax.ws.rs.ApplicationPath;
 
 import io.helidon.common.HelidonFeatures;
 import io.helidon.common.HelidonFlavor;
@@ -75,6 +79,8 @@ public class ServerCdiExtension implements Extension {
 
     private static final Logger LOGGER = Logger.getLogger(ServerCdiExtension.class.getName());
 
+    private static final String DEFAULT_WEBSOCKET_PATH = "/websocket";
+
     // build time
     private ServerConfiguration.Builder serverConfigBuilder = ServerConfiguration.builder()
             .port(7001);
@@ -203,14 +209,45 @@ private void registerPathStaticContent(Config config) {
     private void registerWebSockets(BeanManager beanManager, ServerConfiguration serverConfig) {
         try {
             WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class);
-            WebSocketApplication app = extension.toWebSocketApplication().build();
+            WebSocketApplication app = extension.toWebSocketApplication();
+
+            // If application present call its methods
+            String path = DEFAULT_WEBSOCKET_PATH;
             TyrusSupport.Builder builder = TyrusSupport.builder();
-            app.endpointClasses().forEach(builder::register);
-            app.endpointConfigs().forEach(builder::register);
+            Optional> applicationClass = app.applicationClass();
+            if (applicationClass.isPresent()) {
+                Class c = applicationClass.get();
+                ServerApplicationConfig instance = CDI.current().select(c).get();
+                if (instance == null) {
+                    try {
+                        instance = c.getDeclaredConstructor().newInstance();
+                    } catch (Exception e) {
+                        throw new RuntimeException("Unable to instantiate websocket application " + c, e);
+                    }
+                }
+                Set endpointConfigs = instance.getEndpointConfigs(app.programmaticEndpoints());
+                Set> endpointClasses = instance.getAnnotatedEndpointClasses(app.annotatedEndpoints());
+
+                // Helidon extension - allow @ApplicationPath class for WebSockets
+                ApplicationPath appPath = c.getAnnotation(ApplicationPath.class);
+                if (appPath != null) {
+                    path = appPath.value();
+                }
+
+                // Register classes and configs
+                endpointClasses.forEach(builder::register);
+                endpointConfigs.forEach(builder::register);
+            } else {
+                // Direct registration without calling application class
+                app.annotatedEndpoints().forEach(builder::register);
+                app.programmaticEndpoints().forEach(builder::register);
+            }
+
+            // Finally register WebSockets in Helidon routing
             Routing.Builder routing = serverRoutingBuilder();
-            routing.register("/websocket", builder.build());
+            routing.register(path, builder.build());
         } catch (IllegalArgumentException e) {
-            LOGGER.fine("Unable to load WebSocket extension");
+            throw new RuntimeException("Unable to load WebSocket extension", e);
         }
     }
 
diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
index a781ca2bb3e..0ccb7100b07 100644
--- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
+++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
@@ -16,44 +16,26 @@
 
 package io.helidon.microprofile.tyrus;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
 
-import javax.websocket.server.ServerEndpoint;
-import javax.websocket.server.ServerEndpointConfig;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerApplicationConfig;
 
 /**
  * Represents a websocket application with class and config endpoints.
  */
 public final class WebSocketApplication {
 
-    private List> endpointClasses;
-    private List> endpointConfigs;
-
-    /**
-     * Creates a websocket application from classes and configs.
-     *
-     * @param classesOrConfigs Classes and configs.
-     * @return A websocket application.
-     */
-    @SuppressWarnings("unchecked")
-    public static WebSocketApplication create(Class... classesOrConfigs) {
-        Builder builder = new Builder();
-        for (Class c : classesOrConfigs) {
-            if (ServerEndpointConfig.class.isAssignableFrom(c)) {
-                builder.endpointConfig((Class) c);
-            } else if (c.isAnnotationPresent(ServerEndpoint.class)) {
-                builder.endpointClass(c);
-            } else {
-                throw new IllegalArgumentException("Unable to create WebSocket application using " + c);
-            }
-        }
-        return builder.build();
-    }
+    private Class applicationClass;
+    private Set> annotatedEndpoints;
+    private Set> programmaticEndpoints;
 
     private WebSocketApplication(Builder builder) {
-        this.endpointConfigs = builder.endpointConfigs;
-        this.endpointClasses = builder.endpointClasses;
+        this.applicationClass = builder.applicationClass;
+        this.annotatedEndpoints = builder.annotatedEndpoints;
+        this.programmaticEndpoints = builder.programmaticEndpoints;
     }
 
     /**
@@ -66,21 +48,30 @@ public static Builder builder() {
     }
 
     /**
-     * Get list of config endpoints.
+     * Get access to application class, if present.
+     *
+     * @return Application class optional.
+     */
+    public Optional> applicationClass() {
+        return Optional.ofNullable(applicationClass);
+    }
+
+    /**
+     * Get list of programmatic endpoints.
      *
      * @return List of config endpoints.
      */
-    public List> endpointConfigs() {
-        return endpointConfigs;
+    public Set> programmaticEndpoints() {
+        return programmaticEndpoints;
     }
 
     /**
-     * Get list of endpoint classes.
+     * Get list of annotated endpoints.
      *
-     * @return List of endpoint classes.
+     * @return List of annotated endpoint.
      */
-    public List> endpointClasses() {
-        return endpointClasses;
+    public Set> annotatedEndpoints() {
+        return annotatedEndpoints;
     }
 
     /**
@@ -88,28 +79,43 @@ public List> endpointClasses() {
      */
     public static class Builder {
 
-        private List> endpointClasses = new ArrayList<>();
-        private List> endpointConfigs = new ArrayList<>();
+        private Class applicationClass;
+        private Set> annotatedEndpoints = new HashSet<>();
+        private Set> programmaticEndpoints = new HashSet<>();
+
+        /**
+         * Set an application class in the builder.
+         *
+         * @param applicationClass The application class.
+         * @return The builder.
+         */
+        public Builder applicationClass(Class applicationClass) {
+            if (this.applicationClass != null) {
+                throw new IllegalStateException("At most one subclass of ServerApplicationConfig is permitted");
+            }
+            this.applicationClass = applicationClass;
+            return this;
+        }
 
         /**
-         * Add single config endpoint.
+         * Add single programmatic endpoint.
          *
-         * @param endpointConfig Endpoint config.
+         * @param programmaticEndpoint Programmatic endpoint.
          * @return The builder.
          */
-        public Builder endpointConfig(Class endpointConfig) {
-            endpointConfigs.add(endpointConfig);
+        public Builder programmaticEndpoint(Class programmaticEndpoint) {
+            programmaticEndpoints.add(programmaticEndpoint);
             return this;
         }
 
         /**
-         * Add single endpoint class.
+         * Add single annotated endpoint.
          *
-         * @param endpointClass Endpoint class.
+         * @param annotatedEndpoint Annotated endpoint.
          * @return The builder.
          */
-        public Builder endpointClass(Class endpointClass) {
-            endpointClasses.add(endpointClass);
+        public Builder annotatedEndpoint(Class annotatedEndpoint) {
+            annotatedEndpoints.add(annotatedEndpoint);
             return this;
         }
 
diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java
index 0a01ef3fc63..7b0f42f758a 100644
--- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java
+++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java
@@ -22,6 +22,8 @@
 import javax.enterprise.inject.spi.Extension;
 import javax.enterprise.inject.spi.ProcessAnnotatedType;
 import javax.enterprise.inject.spi.WithAnnotations;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerApplicationConfig;
 import javax.websocket.server.ServerEndpoint;
 
 import io.helidon.common.HelidonFeatures;
@@ -40,21 +42,41 @@ public class WebSocketCdiExtension implements Extension {
     private WebSocketApplication.Builder appBuilder = WebSocketApplication.builder();
 
     /**
-     * Collects endpoints annotated with {@code ServerEndpoint}.
+     * Collect application class extending {@code ServerApplicationConfig}.
      *
-     * @param endpoint Type of endpoint.
+     * @param applicationClass Application class.
      */
-    private void endpoints(@Observes @WithAnnotations(ServerEndpoint.class) ProcessAnnotatedType endpoint) {
+    private void applicationClass(@Observes ProcessAnnotatedType applicationClass) {
+        LOGGER.info(() -> "Application class found " + applicationClass.getAnnotatedType().getJavaClass());
+        appBuilder.applicationClass(applicationClass.getAnnotatedType().getJavaClass());
+    }
+
+    /**
+     * Collect annotated endpoints.
+     *
+     * @param endpoint The endpoint.
+     */
+    private void endpointClasses(@Observes @WithAnnotations(ServerEndpoint.class) ProcessAnnotatedType endpoint) {
         LOGGER.info(() -> "Annotated endpoint found " + endpoint.getAnnotatedType().getJavaClass());
-        appBuilder.endpointClass(endpoint.getAnnotatedType().getJavaClass());
+        appBuilder.annotatedEndpoint(endpoint.getAnnotatedType().getJavaClass());
+    }
+
+    /**
+     * Collects programmatic endpoints .
+     *
+     * @param endpoint The endpoint.
+     */
+    private void endpointConfig(@Observes ProcessAnnotatedType endpoint) {
+        LOGGER.info(() -> "Programmatic endpoint found " + endpoint.getAnnotatedType().getJavaClass());
+        appBuilder.programmaticEndpoint(endpoint.getAnnotatedType().getJavaClass());
     }
 
     /**
-     * Provides access to websocket application builder.
+     * Provides access to websocket application.
      *
-     * @return Application builder.
+     * @return Application.
      */
-    public WebSocketApplication.Builder toWebSocketApplication() {
-        return appBuilder;
+    public WebSocketApplication toWebSocketApplication() {
+        return appBuilder.build();
     }
 }
diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java
index a7ce0ab4b33..cc13e9c830e 100644
--- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java
+++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java
@@ -30,6 +30,6 @@ public class EchoEndpoint {
 
     @OnMessage
     public void echo(Session session, String message) throws Exception {
-        session.getBasicRemote().sendObject(message);       // calls encoder
+        session.getBasicRemote().sendObject(message);
     }
 }
diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java
new file mode 100644
index 00000000000..d1849d287e3
--- /dev/null
+++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.tyrus;
+
+import javax.enterprise.context.Dependent;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+/**
+ * Class EchoEndpointProg. Using WebSocket programmatic API.
+ */
+@Dependent
+public class EchoEndpointProg extends Endpoint {
+
+    @Override
+    public void onOpen(Session session, EndpointConfig endpointConfig) {
+        session.addMessageHandler(new MessageHandler.Whole() {
+            @Override
+            public void onMessage(String message) {
+                try {
+                    session.getBasicRemote().sendObject(message);       // calls encoder
+                } catch (Exception e) {
+                }
+            }
+        });
+    }
+}
diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java
new file mode 100644
index 00000000000..5f1f7597d42
--- /dev/null
+++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.tyrus;
+
+import javax.enterprise.context.Dependent;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerApplicationConfig;
+import javax.websocket.server.ServerEndpointConfig;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Class EndpointApplication.
+ */
+@Dependent
+public class EndpointApplication implements ServerApplicationConfig {
+    @Override
+    public Set getEndpointConfigs(Set> endpointClasses) {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public Set> getAnnotatedEndpointClasses(Set> scanned) {
+        return Collections.emptySet();
+    }
+}
diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java
index ae9a5c6893b..dce9f932ff3 100644
--- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java
+++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java
@@ -26,7 +26,6 @@
 
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.greaterThan;
 
 /**
  * Class WebSocketExtensionTest.
@@ -47,16 +46,17 @@ public static void shutDownCdiContainer() {
         }
     }
 
-    private WebSocketApplication webSocketApplication() {
-        BeanManager beanManager = cdiContainer.getBeanManager();
-        WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class);
-        return extension.toWebSocketApplication().build();
-    }
-
     @Test
     public void testExtension() {
         WebSocketApplication application = webSocketApplication();
-        assertThat(application.endpointClasses().size(), is(greaterThan(0)));
-        assertThat(application.endpointConfigs().size(), is(0));
+        assertThat(application.applicationClass().isPresent(), is(true));
+        assertThat(application.annotatedEndpoints().size(), is(1));
+        assertThat(application.programmaticEndpoints().size(), is(1));
+    }
+
+    private WebSocketApplication webSocketApplication() {
+        BeanManager beanManager = cdiContainer.getBeanManager();
+        WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class);
+        return extension.toWebSocketApplication();
     }
 }

From 89c7f697e631c7e2a968c6bf463c265cd255d51b Mon Sep 17 00:00:00 2001
From: Santiago Pericas-Geertsen 
Date: Thu, 23 Jan 2020 14:48:19 -0500
Subject: [PATCH 15/35] Testing websocket applications and programmatic
 endpoints.

---
 .../server/ServerCdiExtension.java            |   1 +
 .../microprofile/server/EchoClient.java       | 126 +++++++++++++++
 .../microprofile/server/WebSocketAppTest.java | 148 ++++++++++++++++++
 .../microprofile/server/WebSocketTest.java    | 104 +-----------
 4 files changed, 276 insertions(+), 103 deletions(-)
 create mode 100644 microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java
 create mode 100644 microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java

diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
index 8b82717b1b7..ed9396ccb02 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
@@ -244,6 +244,7 @@ private void registerWebSockets(BeanManager beanManager, ServerConfiguration ser
             }
 
             // Finally register WebSockets in Helidon routing
+            LOGGER.info("Registering websocket application at " + path);
             Routing.Builder routing = serverRoutingBuilder();
             routing.register(path, builder.build());
         } catch (IllegalArgumentException e) {
diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java
new file mode 100644
index 00000000000..29224aacd44
--- /dev/null
+++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.server;
+
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.logging.Logger;
+
+import org.glassfish.tyrus.client.ClientManager;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Class EchoClient.
+ */
+class EchoClient {
+    private static final Logger LOGGER = Logger.getLogger(EchoClient.class.getName());
+
+    private static final ClientManager client = ClientManager.createClient();
+    private static final long TIMEOUT_SECONDS = 10;
+
+    private final URI uri;
+    private final BiFunction equals;
+
+    public EchoClient(URI uri) {
+        this(uri, String::equals);
+    }
+
+    public EchoClient(URI uri, BiFunction equals) {
+        this.uri = uri;
+        this.equals = equals;
+    }
+
+    /**
+     * Sends each message one by one and compares echoed value ignoring cases.
+     *
+     * @param messages Messages to send.
+     * @throws Exception If an error occurs.
+     */
+    public void echo(String... messages) throws Exception {
+        CountDownLatch messageLatch = new CountDownLatch(messages.length);
+        CompletableFuture openFuture = new CompletableFuture<>();
+        CompletableFuture closeFuture = new CompletableFuture<>();
+        ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build();
+
+        client.connectToServer(new Endpoint() {
+            @Override
+            public void onOpen(Session session, EndpointConfig EndpointConfig) {
+                openFuture.complete(null);
+                try {
+                    // Register message handler. Tyrus has problems with lambdas here
+                    // so an inner class with an onMessage method is required.
+                    session.addMessageHandler(new MessageHandler.Whole() {
+                        @Override
+                        public void onMessage(String message) {
+                            LOGGER.info("Client OnMessage called '" + message + "'");
+
+                            int index = messages.length - (int) messageLatch.getCount();
+                            assertTrue(equals.apply(messages[index], message));
+
+                            messageLatch.countDown();
+                            if (messageLatch.getCount() == 0) {
+                                try {
+                                    session.close();
+                                } catch (IOException e) {
+                                    fail("Unexpected exception " + e);
+                                }
+                            }
+                        }
+                    });
+
+                    // Send message to Echo service
+                    for (String msg : messages) {
+                        LOGGER.info("Client sending message '" + msg + "'");
+                        session.getBasicRemote().sendText(msg);
+                    }
+                } catch (IOException e) {
+                    fail("Unexpected exception " + e);
+                }
+            }
+
+            @Override
+            public void onClose(Session session, CloseReason closeReason) {
+                LOGGER.info("Client OnClose called '" + closeReason + "'");
+                closeFuture.complete(null);
+            }
+
+            @Override
+            public void onError(Session session, Throwable thr) {
+                LOGGER.info("Client OnError called '" + thr + "'");
+
+            }
+        }, config, uri);
+
+        openFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        closeFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        if (!messageLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+            fail("Timeout expired without receiving echo of all messages");
+        }
+    }
+}
diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java
new file mode 100644
index 00000000000..9be34322c3f
--- /dev/null
+++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.server;
+
+import javax.enterprise.context.Dependent;
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerApplicationConfig;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+import javax.ws.rs.ApplicationPath;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Class WebSocketAppTest.
+ */
+public class WebSocketAppTest {
+
+    private static Server server;
+
+    @BeforeAll
+    static void initClass() {
+        server = Server.create();
+        server.start();
+    }
+
+    @AfterAll
+    static void destroyClass() {
+        server.stop();
+    }
+
+    @Test
+    public void testEchoAnnot() throws Exception {
+        URI echoUri = URI.create("ws://localhost:" + server.port() + "/web/echoAnnot");
+        EchoClient echoClient = new EchoClient(echoUri);
+        echoClient.echo("hi", "how are you?");
+    }
+
+    @Test
+    public void testEchoProg() throws Exception {
+        URI echoUri = URI.create("ws://localhost:" + server.port() + "/web/echoProg");
+        EchoClient echoClient = new EchoClient(echoUri);
+        echoClient.echo("hi", "how are you?");
+    }
+
+    @Dependent      // scanned by CDI
+    @ApplicationPath("/web")
+    public static class EndpointApplication implements ServerApplicationConfig {
+        @Override
+        public Set getEndpointConfigs(Set> endpoints) {
+            ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(
+                    EchoEndpointProg.class, "/echoProg");
+            return Collections.singleton(builder.build());
+        }
+
+        @Override
+        public Set> getAnnotatedEndpointClasses(Set> endpoints) {
+            return Collections.singleton(EchoEndpointAnnot.class);
+        }
+    }
+
+    @ServerEndpoint("/echoAnnot")
+    public static class EchoEndpointAnnot {
+        private static final Logger LOGGER = Logger.getLogger(EchoEndpointAnnot.class.getName());
+
+        @OnOpen
+        public void onOpen(Session session) throws IOException {
+            LOGGER.info("OnOpen called");
+        }
+
+        @OnMessage
+        public void echo(Session session, String message) throws Exception {
+            LOGGER.info("OnMessage called '" + message + "'");
+            session.getBasicRemote().sendObject(message);
+        }
+
+        @OnError
+        public void onError(Throwable t) {
+            LOGGER.info("OnError called");
+        }
+
+        @OnClose
+        public void onClose(Session session) {
+            LOGGER.info("OnClose called");
+        }
+    }
+
+    public static class EchoEndpointProg extends Endpoint {
+        private static final Logger LOGGER = Logger.getLogger(EchoEndpointProg.class.getName());
+
+        @Override
+        public void onOpen(Session session, EndpointConfig endpointConfig) {
+            LOGGER.info("OnOpen called");
+            session.addMessageHandler(new MessageHandler.Whole() {
+                @Override
+                public void onMessage(String message) {
+                    LOGGER.info("OnMessage called '" + message + "'");
+                    try {
+                        session.getBasicRemote().sendObject(message);
+                    } catch (Exception e) {
+                        LOGGER.info(e.getMessage());
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onError(Session session, Throwable thr) {
+            LOGGER.info("OnError called");
+            super.onError(session, thr);
+        }
+
+        @Override
+        public void onClose(Session session, CloseReason closeReason) {
+            LOGGER.info("OnClose called");
+            super.onClose(session, closeReason);
+        }
+    }
+}
diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java
index 52f705e278e..d9b1706a42b 100644
--- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java
+++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java
@@ -17,33 +17,21 @@
 package io.helidon.microprofile.server;
 
 import javax.enterprise.context.Dependent;
-import javax.websocket.ClientEndpointConfig;
-import javax.websocket.CloseReason;
-import javax.websocket.Endpoint;
-import javax.websocket.EndpointConfig;
-import javax.websocket.MessageHandler;
 import javax.websocket.OnClose;
 import javax.websocket.OnError;
 import javax.websocket.OnMessage;
 import javax.websocket.OnOpen;
 import javax.websocket.Session;
 import javax.websocket.server.ServerEndpoint;
+
 import java.io.IOException;
 import java.net.URI;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.function.BiFunction;
 import java.util.logging.Logger;
 
-import org.glassfish.tyrus.client.ClientManager;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
 /**
  * Class EchoEndpointTest.
  */
@@ -95,94 +83,4 @@ public void onClose(Session session) {
             LOGGER.info("OnClose called");
         }
     }
-
-    /**
-     * Class EchoClient.
-     */
-    static class EchoClient {
-        private static final Logger LOGGER = Logger.getLogger(EchoClient.class.getName());
-
-        private static final ClientManager client = ClientManager.createClient();
-        private static final long TIMEOUT_SECONDS = 10;
-
-        private final URI uri;
-        private final BiFunction equals;
-
-        public EchoClient(URI uri) {
-            this(uri, String::equals);
-        }
-
-        public EchoClient(URI uri, BiFunction equals) {
-            this.uri = uri;
-            this.equals = equals;
-        }
-
-        /**
-         * Sends each message one by one and compares echoed value ignoring cases.
-         *
-         * @param messages Messages to send.
-         * @throws Exception If an error occurs.
-         */
-        public void echo(String... messages) throws Exception {
-            CountDownLatch messageLatch = new CountDownLatch(messages.length);
-            CompletableFuture openFuture = new CompletableFuture<>();
-            CompletableFuture closeFuture = new CompletableFuture<>();
-            ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build();
-
-            client.connectToServer(new Endpoint() {
-                @Override
-                public void onOpen(Session session, EndpointConfig EndpointConfig) {
-                    openFuture.complete(null);
-                    try {
-                        // Register message handler. Tyrus has problems with lambdas here
-                        // so an inner class with an onMessage method is required.
-                        session.addMessageHandler(new MessageHandler.Whole() {
-                            @Override
-                            public void onMessage(String message) {
-                                LOGGER.info("Client OnMessage called '" + message + "'");
-
-                                int index = messages.length - (int) messageLatch.getCount();
-                                assertTrue(equals.apply(messages[index], message));
-
-                                messageLatch.countDown();
-                                if (messageLatch.getCount() == 0) {
-                                    try {
-                                        session.close();
-                                    } catch (IOException e) {
-                                        fail("Unexpected exception " + e);
-                                    }
-                                }
-                            }
-                        });
-
-                        // Send message to Echo service
-                        for (String msg : messages) {
-                            LOGGER.info("Client sending message '" + msg + "'");
-                            session.getBasicRemote().sendText(msg);
-                        }
-                    } catch (IOException e) {
-                        fail("Unexpected exception " + e);
-                    }
-                }
-
-                @Override
-                public void onClose(Session session, CloseReason closeReason) {
-                    LOGGER.info("Client OnClose called '" + closeReason + "'");
-                    closeFuture.complete(null);
-                }
-
-                @Override
-                public void onError(Session session, Throwable thr) {
-                    LOGGER.info("Client OnError called '" + thr + "'");
-
-                }
-            }, config, uri);
-
-            openFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
-            closeFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
-            if (!messageLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
-                fail("Timeout expired without receiving echo of all messages");
-            }
-        }
-    }
 }

From 7345fc9112aee02cede40c9b1890eb32537410f9 Mon Sep 17 00:00:00 2001
From: Santiago Pericas-Geertsen 
Date: Thu, 23 Jan 2020 15:31:25 -0500
Subject: [PATCH 16/35] Extended server builder to allow manually setting a
 websocket application. Updated corresponding test.

---
 .../helidon/microprofile/server/Server.java   | 19 +++++++++++++------
 .../server/ServerCdiExtension.java            | 12 +++++++++++-
 .../microprofile/server/ServerImpl.java       |  5 +++++
 .../microprofile/server/WebSocketAppTest.java |  5 +++--
 .../tyrus/WebSocketApplication.java           | 13 +++++++++++++
 .../tyrus/WebSocketCdiExtension.java          | 10 ++++++++++
 6 files changed, 55 insertions(+), 9 deletions(-)

diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java
index c04b7486416..2670721be4a 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java
@@ -19,18 +19,19 @@
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Supplier;
 import java.util.logging.Logger;
 
 import javax.enterprise.inject.spi.CDI;
+import javax.websocket.server.ServerApplicationConfig;
 import javax.ws.rs.core.Application;
 
 import io.helidon.common.configurable.ServerThreadPoolSupplier;
 import io.helidon.common.context.Contexts;
 import io.helidon.microprofile.cdi.HelidonContainer;
-import io.helidon.microprofile.tyrus.WebSocketApplication;
 
 import org.eclipse.microprofile.config.Config;
 import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
@@ -137,7 +138,6 @@ final class Builder {
 
         private final List> resourceClasses = new LinkedList<>();
         private final List applications = new LinkedList<>();
-        private final List wsApplications = new LinkedList<>();
         private Config config;
         private String host;
         private String basePath;
@@ -145,6 +145,7 @@ final class Builder {
         private Supplier defaultExecutorService;
         private JaxRsCdiExtension jaxRs;
         private boolean retainDiscovered = false;
+        private Class wsApplication;
 
         private Builder() {
             if (!IN_PROGRESS_OR_RUNNING.compareAndSet(false, true)) {
@@ -369,14 +370,16 @@ public Builder addApplication(Application application) {
         }
 
         /**
-         * Adds a WebSocket application to the server. If more than one application is added, they
-         * must be registered on different paths.
+         * Registers a WebSocket application in the server. At most one application can be registered.
          *
          * @param wsApplication websocket application
          * @return modified builder
          */
-        public Builder addApplication(WebSocketApplication wsApplication) {
-            this.wsApplications.add(wsApplication);
+        public Builder websocketApplication(Class wsApplication) {
+            if (this.wsApplication != null) {
+                throw new IllegalStateException("Cannot register more than one websocket application");
+            }
+            this.wsApplication = wsApplication;
             return this;
         }
 
@@ -505,5 +508,9 @@ String host() {
         int port() {
             return port;
         }
+
+        Optional> websocketApplication() {
+            return Optional.ofNullable(wsApplication);
+        }
     }
 }
diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
index ed9396ccb02..26bb8690c75 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java
@@ -37,6 +37,7 @@
 import javax.enterprise.context.Initialized;
 import javax.enterprise.context.spi.CreationalContext;
 import javax.enterprise.event.Observes;
+import javax.enterprise.inject.UnsatisfiedResolutionException;
 import javax.enterprise.inject.spi.Bean;
 import javax.enterprise.inject.spi.BeanManager;
 import javax.enterprise.inject.spi.CDI;
@@ -217,7 +218,16 @@ private void registerWebSockets(BeanManager beanManager, ServerConfiguration ser
             Optional> applicationClass = app.applicationClass();
             if (applicationClass.isPresent()) {
                 Class c = applicationClass.get();
-                ServerApplicationConfig instance = CDI.current().select(c).get();
+
+                // Attempt to instantiate via CDI
+                ServerApplicationConfig instance = null;
+                try {
+                    instance = CDI.current().select(c).get();
+                } catch (UnsatisfiedResolutionException e) {
+                    // falls through
+                }
+
+                // Otherwise, we create instance directly
                 if (instance == null) {
                     try {
                         instance = c.getDeclaredConstructor().newInstance();
diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java
index 040b416c7b2..e19ee22522a 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java
@@ -25,6 +25,7 @@
 import javax.enterprise.inject.spi.CDI;
 
 import io.helidon.microprofile.cdi.HelidonContainer;
+import io.helidon.microprofile.tyrus.WebSocketCdiExtension;
 
 import static io.helidon.microprofile.server.Server.Builder.IN_PROGRESS_OR_RUNNING;
 
@@ -68,6 +69,10 @@ public class ServerImpl implements Server {
 
         serverExtension.listenHost(this.host);
 
+        // Update extension with manually configured application -- overrides scanning
+        WebSocketCdiExtension wsExtension = beanManager.getExtension(WebSocketCdiExtension.class);
+        builder.websocketApplication().ifPresent(wsExtension::applicationClass);
+
         STARTUP_LOGGER.finest("Builders ready");
 
         STARTUP_LOGGER.finest("Static classpath");
diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java
index 9be34322c3f..0e0b229bb35 100644
--- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java
+++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java
@@ -49,7 +49,9 @@ public class WebSocketAppTest {
 
     @BeforeAll
     static void initClass() {
-        server = Server.create();
+        Server.Builder builder = Server.builder();
+        builder.websocketApplication(EndpointApplication.class);
+        server = builder.build();
         server.start();
     }
 
@@ -72,7 +74,6 @@ public void testEchoProg() throws Exception {
         echoClient.echo("hi", "how are you?");
     }
 
-    @Dependent      // scanned by CDI
     @ApplicationPath("/web")
     public static class EndpointApplication implements ServerApplicationConfig {
         @Override
diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
index 0ccb7100b07..1c6bd882e15 100644
--- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
+++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
@@ -83,6 +83,19 @@ public static class Builder {
         private Set> annotatedEndpoints = new HashSet<>();
         private Set> programmaticEndpoints = new HashSet<>();
 
+        /**
+         * Updates an application class in the builder. Clears all results from scanning.
+         *
+         * @param applicationClass The application class.
+         * @return The builder.
+         */
+        Builder updateApplicationClass(Class applicationClass) {
+            this.applicationClass = applicationClass;
+            annotatedEndpoints.clear();
+            programmaticEndpoints.clear();
+            return this;
+        }
+
         /**
          * Set an application class in the builder.
          *
diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java
index 7b0f42f758a..2d136e7b3f9 100644
--- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java
+++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java
@@ -51,6 +51,16 @@ private void applicationClass(@Observes ProcessAnnotatedType applicationClass) {
+        LOGGER.info(() -> "Using manually set application class  " + applicationClass);
+        appBuilder.updateApplicationClass(applicationClass);
+    }
+
     /**
      * Collect annotated endpoints.
      *

From af077c6255fad9746c1ed588be8f15ef231935bf Mon Sep 17 00:00:00 2001
From: Santiago Pericas-Geertsen 
Date: Fri, 24 Jan 2020 13:41:29 -0500
Subject: [PATCH 17/35] New MP example that uses REST and WebSockets. Some
 other minor changes.

---
 examples/microprofile/pom.xml                 |   1 +
 examples/microprofile/websocket/README.md     |  13 ++
 examples/microprofile/websocket/pom.xml       |  82 ++++++++++++
 .../websocket/MessageBoardEndpoint.java       | 116 ++++++++++++++++
 .../example/websocket/MessageQueue.java       |  67 ++++++++++
 .../websocket/MessageQueueResource.java       |  46 +++++++
 .../example/websocket/package-info.java       |  20 +++
 .../src/main/resources/META-INF/beans.xml     |  26 ++++
 .../src/main/resources/application.yaml       |  18 +++
 .../src/main/resources/logging.properties     |  20 +++
 .../example/websocket/MessageBoardTest.java   | 126 ++++++++++++++++++
 .../META-INF/microprofile-config.properties   |  17 +++
 .../server/JaxRsCdiExtension.java             |   2 +-
 .../tyrus/HelidonComponentProvider.java       |   2 +-
 .../tyrus/WebSocketApplication.java           |   7 +-
 .../helidon/webserver/tyrus/TyrusSupport.java |  12 +-
 16 files changed, 567 insertions(+), 8 deletions(-)
 create mode 100644 examples/microprofile/websocket/README.md
 create mode 100644 examples/microprofile/websocket/pom.xml
 create mode 100644 examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java
 create mode 100644 examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java
 create mode 100644 examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java
 create mode 100644 examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java
 create mode 100644 examples/microprofile/websocket/src/main/resources/META-INF/beans.xml
 create mode 100644 examples/microprofile/websocket/src/main/resources/application.yaml
 create mode 100644 examples/microprofile/websocket/src/main/resources/logging.properties
 create mode 100644 examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java
 create mode 100644 examples/microprofile/websocket/src/test/resources/META-INF/microprofile-config.properties

diff --git a/examples/microprofile/pom.xml b/examples/microprofile/pom.xml
index c5a565b748e..3d8c40acd73 100644
--- a/examples/microprofile/pom.xml
+++ b/examples/microprofile/pom.xml
@@ -38,5 +38,6 @@
         mp1_1-security
         idcs
         openapi-basic
+        websocket
     
 
diff --git a/examples/microprofile/websocket/README.md b/examples/microprofile/websocket/README.md
new file mode 100644
index 00000000000..3e83747a507
--- /dev/null
+++ b/examples/microprofile/websocket/README.md
@@ -0,0 +1,13 @@
+# Helidon MP WebSocket Example
+
+This examples shows a simple application written using Helidon MP
+that combines REST resources and WebSocket endpoints.
+
+## Build and run
+
+With JDK8+
+```bash
+mvn package
+java -jar target/helidon-examples-microprofile-websocket.jar
+```
+
diff --git a/examples/microprofile/websocket/pom.xml b/examples/microprofile/websocket/pom.xml
new file mode 100644
index 00000000000..a343394b202
--- /dev/null
+++ b/examples/microprofile/websocket/pom.xml
@@ -0,0 +1,82 @@
+
+
+
+
+    4.0.0
+    
+        io.helidon.applications
+        helidon-mp
+        2.0-SNAPSHOT
+        ../../../applications/mp/pom.xml
+    
+    io.helidon.examples.microprofile
+    helidon-examples-microprofile-websocket
+    Helidon Microprofile Examples WebSocket
+
+    
+        Microprofile example that uses websockets
+    
+
+    
+        
+            io.helidon.microprofile.bundles
+            helidon-microprofile
+        
+        
+            io.helidon.microprofile.bundles
+            internal-test-libs
+            test
+        
+        
+            org.jboss
+            jandex
+            runtime
+            true
+        
+        
+            javax.activation
+            javax.activation-api
+        
+    
+
+    
+        
+            
+                org.apache.maven.plugins
+                maven-dependency-plugin
+                
+                    
+                        copy-libs
+                    
+                
+            
+            
+                org.jboss.jandex
+                jandex-maven-plugin
+                
+                    
+                        make-index
+                    
+                
+            
+        
+    
+
diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java
new file mode 100644
index 00000000000..f00d0bd868d
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.example.websocket;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.enterprise.context.Dependent;
+import javax.inject.Inject;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+/**
+ * Class MessageBoardEndpoint.
+ */
+@Dependent
+@ServerEndpoint(
+        value = "/board",
+        encoders = { MessageBoardEndpoint.UppercaseEncoder.class }
+)
+public class MessageBoardEndpoint {
+    private static final Logger LOGGER = Logger.getLogger(MessageBoardEndpoint.class.getName());
+
+    @Inject
+    private MessageQueue messageQueue;
+
+    /**
+     * OnOpen call.
+     *
+     * @param session The websocket session.
+     * @throws IOException If error occurs.
+     */
+    @OnOpen
+    public void onOpen(Session session) throws IOException {
+        LOGGER.info("OnOpen called");
+    }
+
+    /**
+     * OnMessage call.
+     *
+     * @param session The websocket session.
+     * @param message The message received.
+     * @throws Exception If error occurs.
+     */
+    @OnMessage
+    public void onMessage(Session session, String message) throws Exception {
+        LOGGER.info("OnMessage called '" + message + "'");
+
+        // Send all messages in the queue
+        if (message.equals("SEND")) {
+            while (!messageQueue.isEmpty()) {
+                session.getBasicRemote().sendObject(messageQueue.pop());
+            }
+        }
+    }
+
+    /**
+     * OnError call.
+     *
+     * @param t The throwable.
+     */
+    @OnError
+    public void onError(Throwable t) {
+        LOGGER.info("OnError called");
+    }
+
+    /**
+     * OnError call.
+     *
+     * @param session The websocket session.
+     */
+    @OnClose
+    public void onClose(Session session) {
+        LOGGER.info("OnClose called");
+    }
+
+    /**
+     * Uppercase encoder.
+     */
+    public static class UppercaseEncoder implements Encoder.Text {
+
+        @Override
+        public String encode(String s) {
+            LOGGER.info("UppercaseEncoder encode called");
+            return s.toUpperCase();
+        }
+
+        @Override
+        public void init(EndpointConfig config) {
+        }
+
+        @Override
+        public void destroy() {
+        }
+    }
+}
diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java
new file mode 100644
index 00000000000..dedd8270f15
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.example.websocket;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ * Class MessageQueue.
+ */
+@ApplicationScoped
+public class MessageQueue {
+
+    private Queue queue = new ConcurrentLinkedQueue<>();
+
+    /**
+     * Push string on stack.
+     *
+     * @param s String to push.
+     */
+    public void push(String s) {
+        queue.add(s);
+    }
+
+    /**
+     * Pop string from stack.
+     *
+     * @return The string or {@code null}.
+     */
+    public String pop() {
+        return queue.poll();
+    }
+
+    /**
+     * Check if stack is empty.
+     *
+     * @return Outcome of test.
+     */
+    public boolean isEmpty() {
+        return queue.isEmpty();
+    }
+
+    /**
+     * Peek at stack without changing it.
+     *
+     * @return String peeked or {@code null}.
+     */
+    public String peek() {
+        return queue.peek();
+    }
+}
diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java
new file mode 100644
index 00000000000..8d44811436f
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.example.websocket;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+
+/**
+ * Class MessageQueueResource.
+ */
+@Path("rest")
+@RequestScoped
+public class MessageQueueResource {
+
+    @Inject
+    private MessageQueue messageQueue;
+
+    /**
+     * Resource to push string into queue.
+     *
+     * @param s The string.
+     */
+    @POST
+    @Path("board")
+    @Consumes("text/plain")
+    public void push(String s) {
+        messageQueue.push(s);
+    }
+}
diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java
new file mode 100644
index 00000000000..0da0f7e29e4
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Helidon WebSocket example.
+ */
+package io.helidon.microprofile.example.websocket;
diff --git a/examples/microprofile/websocket/src/main/resources/META-INF/beans.xml b/examples/microprofile/websocket/src/main/resources/META-INF/beans.xml
new file mode 100644
index 00000000000..8a67108b992
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/examples/microprofile/websocket/src/main/resources/application.yaml b/examples/microprofile/websocket/src/main/resources/application.yaml
new file mode 100644
index 00000000000..5120c1a6d92
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/resources/application.yaml
@@ -0,0 +1,18 @@
+#
+# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+server.port: 7001
+
diff --git a/examples/microprofile/websocket/src/main/resources/logging.properties b/examples/microprofile/websocket/src/main/resources/logging.properties
new file mode 100644
index 00000000000..07cdd016ba3
--- /dev/null
+++ b/examples/microprofile/websocket/src/main/resources/logging.properties
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+handlers=io.helidon.common.HelidonConsoleHandler
+java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n
+.level=INFO
+io.helidon.microprofile.server.level=INFO
diff --git a/examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java b/examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java
new file mode 100644
index 00000000000..0229f4e6347
--- /dev/null
+++ b/examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.example.websocket;
+
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.CloseReason;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import io.helidon.microprofile.server.Server;
+import org.glassfish.tyrus.client.ClientManager;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Class MessageBoardTest.
+ */
+public class MessageBoardTest {
+    private static final Logger LOGGER = Logger.getLogger(MessageBoardTest.class.getName());
+
+    private static Client restClient = ClientBuilder.newClient();
+    private static ClientManager websocketClient = ClientManager.createClient();
+    private static Server server;
+
+    private String[] messages = { "Whisky", "Tango", "Foxtrot" };
+
+    @BeforeAll
+    static void initClass() {
+        server = Server.create();
+        server.start();
+    }
+
+    @AfterAll
+    static void destroyClass() {
+        server.stop();
+    }
+
+    @Test
+    public void testBoard() throws IOException, DeploymentException, InterruptedException {
+        // Post messages using REST resource
+        URI restUri = URI.create("http://localhost:" + server.port() + "/rest/board");
+        for (String message : messages) {
+            Response res = restClient.target(restUri).request().post(Entity.text(message));
+            assertThat(res.getStatus(), is(204));
+        }
+
+        // Now connect to message board using WS and them back
+        URI websocketUri = URI.create("ws://localhost:" + server.port() + "/websocket/board");
+        CountDownLatch messageLatch = new CountDownLatch(messages.length);
+        ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build();
+
+        websocketClient.connectToServer(new Endpoint() {
+            @Override
+            public void onOpen(Session session, EndpointConfig EndpointConfig) {
+                try {
+                    // Set message handler to receive messages
+                    session.addMessageHandler(new MessageHandler.Whole() {
+                        @Override
+                        public void onMessage(String message) {
+                            LOGGER.info("Client OnMessage called '" + message + "'");
+                            messageLatch.countDown();
+                            if (messageLatch.getCount() == 0) {
+                                try {
+                                    session.close();
+                                } catch (IOException e) {
+                                    fail("Unexpected exception " + e);
+                                }
+                            }
+                        }
+                    });
+
+                    // Send an initial message to start receiving
+                    session.getBasicRemote().sendText("SEND");
+                } catch (IOException e) {
+                    fail("Unexpected exception " + e);
+                }
+            }
+
+            @Override
+            public void onClose(Session session, CloseReason closeReason) {
+                LOGGER.info("Client OnClose called '" + closeReason + "'");
+            }
+
+            @Override
+            public void onError(Session session, Throwable thr) {
+                LOGGER.info("Client OnError called '" + thr + "'");
+
+            }
+        }, config, websocketUri);
+
+        // Wait until all messages are received
+        messageLatch.await(1000000, TimeUnit.SECONDS);
+    }
+}
diff --git a/examples/microprofile/websocket/src/test/resources/META-INF/microprofile-config.properties b/examples/microprofile/websocket/src/test/resources/META-INF/microprofile-config.properties
new file mode 100644
index 00000000000..a574e367656
--- /dev/null
+++ b/examples/microprofile/websocket/src/test/resources/META-INF/microprofile-config.properties
@@ -0,0 +1,17 @@
+#
+# Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+server.port=0
diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java
index e59acc9d273..1ad6bc4662a 100644
--- a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java
+++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java
@@ -104,7 +104,7 @@ public Set> getClasses() {
                                         .collect(Collectors.toList()));
 
         applications.clear();
-        resources.clear();
+        // resources.clear();
 
         return applicationMetas;
     }
diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java
index 54f83177902..948b6a58940 100644
--- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java
+++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java
@@ -42,7 +42,7 @@ public  Object create(Class c) {
     public boolean destroy(Object o) {
         try {
             CDI.current().destroy(o);
-        } catch (UnsupportedOperationException e) {
+        } catch (UnsupportedOperationException | IllegalStateException e) {
             return false;
         }
         return true;
diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
index 1c6bd882e15..a4c3c818ea2 100644
--- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
+++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java
@@ -19,6 +19,7 @@
 import java.util.HashSet;
 import java.util.Optional;
 import java.util.Set;
+import java.util.logging.Logger;
 
 import javax.websocket.Endpoint;
 import javax.websocket.server.ServerApplicationConfig;
@@ -78,6 +79,7 @@ public Set> annotatedEndpoints() {
      * Fluent API builder to create {@link WebSocketApplication} instances.
      */
     public static class Builder {
+        private static final Logger LOGGER = Logger.getLogger(WebSocketApplication.Builder.class.getName());
 
         private Class applicationClass;
         private Set> annotatedEndpoints = new HashSet<>();
@@ -90,9 +92,10 @@ public static class Builder {
          * @return The builder.
          */
         Builder updateApplicationClass(Class applicationClass) {
+            if (this.applicationClass != null) {
+                LOGGER.fine(() -> "Overriding websocket application using " + applicationClass);
+            }
             this.applicationClass = applicationClass;
-            annotatedEndpoints.clear();
-            programmaticEndpoints.clear();
             return this;
         }
 
diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java
index 602aa03c6d8..50466f4d6a0 100644
--- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java
+++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java
@@ -227,8 +227,10 @@ public void accept(ServerRequest req, ServerResponse res) {
 
             // Write reason for failure if not successful
             if (upgradeInfo.getStatus() != WebSocketEngine.UpgradeStatus.SUCCESS) {
-                publisherWriter.write(ByteBuffer.wrap(
-                        upgradeResponse.getReasonPhrase().getBytes(UTF_8)), null);
+                String reason = upgradeResponse.getReasonPhrase();
+                if (reason != null) {
+                    publisherWriter.write(ByteBuffer.wrap(reason.getBytes(UTF_8)), null);
+                }
             }
 
             // Flush upgrade response
@@ -239,8 +241,10 @@ public void accept(ServerRequest req, ServerResponse res) {
                     closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason));
 
             // Set up reader to pass data back to Tyrus
-            TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection);
-            req.content().subscribe(subscriber);
+            if (connection != null) {
+                TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection);
+                req.content().subscribe(subscriber);
+            }
         }
     }
 }

From 0f2693feb4f5583c2937b4ddba942d13acdcb924 Mon Sep 17 00:00:00 2001
From: Santiago Pericas-Geertsen 
Date: Mon, 27 Jan 2020 09:49:09 -0500
Subject: [PATCH 18/35] Fixed copyright problems.

---
 examples/microprofile/pom.xml                                   | 2 +-
 .../websocket/src/main/resources/META-INF/beans.xml             | 2 +-
 .../websocket/src/main/resources/logging.properties             | 2 +-
 .../src/test/resources/META-INF/microprofile-config.properties  | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/examples/microprofile/pom.xml b/examples/microprofile/pom.xml
index 3d8c40acd73..bc0aed14038 100644
--- a/examples/microprofile/pom.xml
+++ b/examples/microprofile/pom.xml
@@ -1,7 +1,7 @@
 
 
+
+
+    4.0.0
+    
+        io.helidon.applications
+        helidon-se
+        2.0-SNAPSHOT
+        ../../../applications/se/pom.xml
+    
+    io.helidon.examples.webserver
+    helidon-examples-webserver-websocket
+    Helidon WebServer Examples WebSocket
+
+    
+        Application demonstrates the use of websockets and REST.
+    
+
+    
+        io.helidon.webserver.examples.websocket.Main
+    
+
+    
+        
+            io.helidon.webserver
+            helidon-webserver
+        
+        
+            io.helidon.webserver
+            helidon-webserver-tyrus
+        
+        
+            io.helidon.jersey
+            helidon-jersey-client
+            test
+        
+        
+            org.junit.jupiter
+            junit-jupiter-api
+            test
+        
+        
+            org.hamcrest
+            hamcrest-all
+            test
+        
+        
+            io.helidon.webserver
+            helidon-webserver-test-support
+            test
+        
+    
+
+    
+        
+            
+                org.apache.maven.plugins
+                maven-dependency-plugin
+                
+                    
+                        copy-libs
+                    
+                
+            
+        
+    
+
diff --git a/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/Main.java b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/Main.java
new file mode 100644
index 00000000000..5b333bf5a49
--- /dev/null
+++ b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/Main.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.webserver.examples.websocket;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import javax.websocket.Encoder;
+import javax.websocket.server.ServerEndpointConfig;
+
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.ServerConfiguration;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.tyrus.TyrusSupport;
+
+import static io.helidon.webserver.examples.websocket.MessageBoardEndpoint.UppercaseEncoder;
+
+/**
+ * Application demonstrates combination of websocket and REST.
+ */
+public class Main {
+
+    private Main() {
+    }
+
+    /**
+     * Creates new {@link Routing}.
+     *
+     * @return the new instance
+     */
+    static Routing createRouting() {
+        List> encoders = Collections.singletonList(UppercaseEncoder.class);
+
+        return Routing.builder()
+                .register("/rest", new MessageQueueService())
+                .register("/websocket",
+                        TyrusSupport.builder().register(
+                                ServerEndpointConfig.Builder.create(MessageBoardEndpoint.class, "/board")
+                                        .encoders(encoders).build())
+                                .build())
+                .build();
+    }
+
+    static WebServer startWebServer() {
+        ServerConfiguration config = ServerConfiguration.builder()
+                .port(8080)
+                .build();
+        WebServer server = WebServer.create(config, createRouting());
+
+        // Start webserver
+        CompletableFuture started = new CompletableFuture<>();
+        server.start().thenAccept(ws -> {
+            System.out.println("WEB server is up! http://localhost:" + ws.port());
+            started.complete(null);
+        });
+
+        // Wait for webserver to start before returning
+        try {
+            started.toCompletableFuture().get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return server;
+    }
+
+    /**
+     * A java main class.
+     *
+     * @param args command line arguments.
+     */
+    public static void main(String[] args) {
+        WebServer server = startWebServer();
+
+        // Server threads are not demon. NO need to block. Just react.
+        server.whenShutdown()
+                .thenRun(() -> System.out.println("WEB server is DOWN. Good bye!"));
+
+    }
+}
diff --git a/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageBoardEndpoint.java b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageBoardEndpoint.java
new file mode 100644
index 00000000000..3a6ba984ab1
--- /dev/null
+++ b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageBoardEndpoint.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.webserver.examples.websocket;
+
+import java.util.logging.Logger;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Encoder;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+/**
+ * Class MessageBoardEndpoint.
+ */
+public class MessageBoardEndpoint extends Endpoint {
+    private static final Logger LOGGER = Logger.getLogger(MessageBoardEndpoint.class.getName());
+
+    private final MessageQueue messageQueue = MessageQueue.instance();
+
+    @Override
+    public void onOpen(Session session, EndpointConfig endpointConfig) {
+        session.addMessageHandler(new MessageHandler.Whole() {
+            @Override
+            public void onMessage(String message) {
+                try {
+                    // Send all messages in the queue
+                    if (message.equals("SEND")) {
+                        while (!messageQueue.isEmpty()) {
+                            session.getBasicRemote().sendObject(messageQueue.pop());
+                        }
+                    }
+                } catch (Exception e) {
+                    LOGGER.info(e.getMessage());
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onClose(Session session, CloseReason closeReason) {
+        super.onClose(session, closeReason);
+    }
+
+    @Override
+    public void onError(Session session, Throwable thr) {
+        super.onError(session, thr);
+    }
+
+    /**
+     * Uppercase encoder.
+     */
+    public static class UppercaseEncoder implements Encoder.Text {
+
+        @Override
+        public String encode(String s) {
+            LOGGER.info("UppercaseEncoder encode called");
+            return s.toUpperCase();
+        }
+
+        @Override
+        public void init(EndpointConfig config) {
+        }
+
+        @Override
+        public void destroy() {
+        }
+    }
+}
diff --git a/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageQueue.java b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageQueue.java
new file mode 100644
index 00000000000..8d3afbaeb71
--- /dev/null
+++ b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageQueue.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.webserver.examples.websocket;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Class MessageQueue.
+ */
+public class MessageQueue {
+
+    private static final MessageQueue INSTANCE = new MessageQueue();
+
+    private Queue queue = new ConcurrentLinkedQueue<>();
+
+    /**
+     * Return singleton instance of this class.
+     *
+     * @return Singleton.
+     */
+    public static MessageQueue instance() {
+        return INSTANCE;
+    }
+
+    private MessageQueue() {
+    }
+
+    /**
+     * Push string on stack.
+     *
+     * @param s String to push.
+     */
+    public void push(String s) {
+        queue.add(s);
+    }
+
+    /**
+     * Pop string from stack.
+     *
+     * @return The string or {@code null}.
+     */
+    public String pop() {
+        return queue.poll();
+    }
+
+    /**
+     * Check if stack is empty.
+     *
+     * @return Outcome of test.
+     */
+    public boolean isEmpty() {
+        return queue.isEmpty();
+    }
+
+    /**
+     * Peek at stack without changing it.
+     *
+     * @return String peeked or {@code null}.
+     */
+    public String peek() {
+        return queue.peek();
+    }
+}
diff --git a/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageQueueService.java b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageQueueService.java
new file mode 100644
index 00000000000..be6fa1a4263
--- /dev/null
+++ b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/MessageQueueService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.webserver.examples.websocket;
+
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.ServerRequest;
+import io.helidon.webserver.ServerResponse;
+import io.helidon.webserver.Service;
+
+/**
+ * Class MessageQueueResource.
+ */
+public class MessageQueueService implements Service {
+
+    private final MessageQueue messageQueue = MessageQueue.instance();
+
+    @Override
+    public void update(Routing.Rules routingRules) {
+        routingRules.post("/board", this::handlePost);
+    }
+
+    private void handlePost(ServerRequest request, ServerResponse response) {
+        request.content().as(String.class).thenAccept(messageQueue::push);
+        response.status(204).send();
+    }
+}
diff --git a/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/package-info.java b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/package-info.java
new file mode 100644
index 00000000000..742281b9f12
--- /dev/null
+++ b/examples/webserver/websocket/src/main/java/io/helidon/webserver/examples/websocket/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Application demonstrates combination of the websocket and REST.
+ *
+ * 

+ * Start with {@link io.helidon.webserver.examples.websocket.Main} class. + * + * @see io.helidon.webserver.examples.websocket.Main + */ +package io.helidon.webserver.examples.websocket; diff --git a/examples/webserver/websocket/src/main/resources/logging.properties b/examples/webserver/websocket/src/main/resources/logging.properties new file mode 100644 index 00000000000..988e7cbdd87 --- /dev/null +++ b/examples/webserver/websocket/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.common.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.server.level=INFO diff --git a/examples/webserver/websocket/src/test/java/io/helidon/webserver/examples/websocket/MessageBoardTest.java b/examples/webserver/websocket/src/test/java/io/helidon/webserver/examples/websocket/MessageBoardTest.java new file mode 100644 index 00000000000..832a1121348 --- /dev/null +++ b/examples/webserver/websocket/src/test/java/io/helidon/webserver/examples/websocket/MessageBoardTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.examples.websocket; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.CloseReason; +import javax.websocket.DeploymentException; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import io.helidon.webserver.WebServer; +import org.glassfish.tyrus.client.ClientManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.webserver.examples.websocket.Main.startWebServer; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Class MessageBoardTest. + */ +public class MessageBoardTest { + private static final Logger LOGGER = Logger.getLogger(MessageBoardTest.class.getName()); + + private static Client restClient = ClientBuilder.newClient(); + private static ClientManager websocketClient = ClientManager.createClient(); + private static WebServer server; + + private String[] messages = { "Whisky", "Tango", "Foxtrot" }; + + @BeforeAll + static void initClass() { + server = startWebServer(); + } + + @AfterAll + static void destroyClass() { + server.shutdown(); + } + + @Test + public void testBoard() throws IOException, DeploymentException, InterruptedException { + // Post messages using REST resource + URI restUri = URI.create("http://localhost:" + server.port() + "/rest/board"); + for (String message : messages) { + Response res = restClient.target(restUri).request().post(Entity.text(message)); + assertThat(res.getStatus(), is(204)); + LOGGER.info("Posting message '" + message + "'"); + } + + // Now connect to message board using WS and them back + URI websocketUri = URI.create("ws://localhost:" + server.port() + "/websocket/board"); + CountDownLatch messageLatch = new CountDownLatch(messages.length); + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + websocketClient.connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig EndpointConfig) { + try { + // Set message handler to receive messages + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String message) { + LOGGER.info("Client OnMessage called '" + message + "'"); + messageLatch.countDown(); + if (messageLatch.getCount() == 0) { + try { + session.close(); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + } + }); + + // Send an initial message to start receiving + session.getBasicRemote().sendText("SEND"); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + LOGGER.info("Client OnClose called '" + closeReason + "'"); + } + + @Override + public void onError(Session session, Throwable thr) { + LOGGER.info("Client OnError called '" + thr + "'"); + + } + }, config, websocketUri); + + // Wait until all messages are received + messageLatch.await(1000000, TimeUnit.SECONDS); + } +} From 4ea25ce005511f8488665dabf8f9ec28565372fe Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 29 Jan 2020 11:59:47 -0500 Subject: [PATCH 21/35] Initial doc for websocket support in Helidon SE and MP. Signed-off-by: Santiago Pericas-Geertsen --- docs/src/main/docs/websocket/01_overview.adoc | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 docs/src/main/docs/websocket/01_overview.adoc diff --git a/docs/src/main/docs/websocket/01_overview.adoc b/docs/src/main/docs/websocket/01_overview.adoc new file mode 100644 index 00000000000..7be492bae8f --- /dev/null +++ b/docs/src/main/docs/websocket/01_overview.adoc @@ -0,0 +1,257 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += WebSocket Introduction +:pagename: websocket-introduction +:description: Helidon WebSocket Introduction +:keywords: helidon, webserver, websocket + +Helidon integrates with Tyrus [1] to provide support for the Jakarta WebSocket API [2]. +The WebSocket API enables Java applications to participate in WebSocket interactions +as servers as well as clients. The server API supports two flavors: annotated and +programmatic endpoints. + +Annotated endpoints, as suggested by their name, use Java annotations to provide +the necessary meta-data to define WebSocket handlers; programmatic endpoints +implement API interfaces and are annotation free. Annotated endpoints tend to be +more flexible since they allow different method signatures depending on the +application needs, whereas programmatic endpoints must implement an interface +and are, therefore, bounded to its definition. This will become more clear as +we dive into some examples in the next few sections. + +Helidon has support for WebSockets both in SE and in MP. Helidon SE support +is based on the `TyrusSupport` class which is akin to `JerseySupport`. +Helidon MP support is focused on bean discovery using CDI and extensive use +of annotations, yet it is still possible to configure applications +programmatically with bean discovery disabled if so desired. + +As stated above, the Jakarta WebSocket API supports both annotated and +programmatic endpoints. Even though most Helidon MP applications rely +on the use of annotations, and conversely Helidon SE applications do +not, it is worth mentioning that annotated and programmatic endpoints +are supported in both SE and MP. + +== Running Example + +In the next few sections we shall show the implementation of a simple application +that uses a REST resource to push messages into a shared queue and a +WebSocket endpoint to download all messages, one at a time, over a connection. +This example will show how REST and WebSocket connections can +be seamlessly combined into a Helidon application. + +== Helidon SE + +The complete Helidon SE example is available here [3]. Let us start by +looking at `MessageQueueService`: + +[source,java] +---- +public class MessageQueueService implements Service { + + private final MessageQueue messageQueue = MessageQueue.instance(); + + @Override + public void update(Routing.Rules routingRules) { + routingRules.post("/board", this::handlePost); + } + + private void handlePost(ServerRequest request, ServerResponse response) { + request.content().as(String.class).thenAccept(messageQueue::push); + response.status(204).send(); + } +} +---- + +This class exposes a REST resource where messages can be posted. Upon +receiving a message, it simply pushes it onto a shared queue and +returns 204 (No Content). + +Messages pushed onto the queue can be obtained by opening a WebSocket +connection served by `MessageBoardEndpoint`: + +[source,java] +---- +public class MessageBoardEndpoint extends Endpoint { + + private final MessageQueue messageQueue = MessageQueue.instance(); + + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String message) { + try { + if (message.equals("SEND")) { + while (!messageQueue.isEmpty()) { + session.getBasicRemote().sendObject(messageQueue.pop()); + } + } + } catch (Exception e) { + // ... + } + } + }); + } +} +---- + +This is an example of a programmatic endpoint that extends `Endpoint`. The method +`onOpen` will be invoked for every new connection. In this example, the application +registers a message handler for strings, and when the special `SEND` message +is received, it empties the shared queue sending messages one at a time over +the WebSocket connection. + +In Helidon SE, REST and WebSocket classes need to be manually registered into +the web server. This is accomplished via a `Routing` builder: + +[source,java] +---- + List> encoders = + Collections.singletonList(UppercaseEncoder.class); + + Routing.builder() + .register("/rest", new MessageQueueService()) + .register("/websocket", + TyrusSupport.builder().register( + ServerEndpointConfig.Builder.create( + MessageBoardEndpoint.class, "/board").encoders( + encoders).build()).build()) + .build(); +[source,java] +---- + +This code snippet uses multiple builders for `Routing`, `TyrusSupport` and `ServerEndpointConfig`. +In particular, it registers `MessageBoardEndpoint.class` at `"/websocket/board"` and associates +with it a _message encoder_. For more information on message encoders and decoders the +reader is referred to [2]; in this example, `UppercaseEncoder.class` simply uppercases every +message sent from the server [3]. + + +== Helidon MP + +The equivalent Helidon MP application shown here takes full advantage of +CDI and class scanning and does not require startup code to initialize +the routes given that the necessary information is available from the +code annotations. + +The REST endpoint is implemented as a JAX-RS resource, and the shared +queue (in application scope) is directly injected: + +[source,java] +---- +@Path("rest") +public class MessageQueueResource { + + @Inject + private MessageQueue messageQueue; + + @POST + @Path("board") + @Consumes("text/plain") + public void push(String s) { + messageQueue.push(s); + } +} +---- + +In this case, we opt for the use of an annotated WebSocket endpoint decorated +by `@ServerEndpoint` that provides all the meta-data which in the SE example +was part of `ServerEndpointConfig`. + +[source,java] +---- +@ServerEndpoint( + value = "/board", + encoders = { UppercaseEncoder.class }) +public class MessageBoardEndpoint { + + @Inject + private MessageQueue messageQueue; + + @OnMessage + public void onMessage(Session session, String message) { + if (message.equals("SEND")) { + while (!messageQueue.isEmpty()) { + session.getBasicRemote().sendObject(messageQueue.pop()); + } + } + } +} +---- + +Since `MessageBoardEndpoint` is just a POJO, it uses additional +annotations for event handlers such as `@OnMessage`. One advantage of +this approach, much like in the JAX-RS API, is that method +signatures are not fixed. In the snipped above, the parameters +(which could be specified in any order!) include the WebSocket +session and the message received that triggered the call. + +So what else is needed to run this Helidon MP app? Nothing else +other than the supporting classes `MessageQueue` and `UppercaseEncoder`. +Helidon MP declares both `@Path` and `@ServerEndpoint` as +bean-defining annotation, so all that is needed is for CDI +discovery to be enabled. + +By default, all JAX-RS resources will be placed under the +application path `"/"` and all WebSocket endpoints under +`"/websocket"` for separation. These values can be overridden +by providing subclasses/implementations for `jakarta.ws.rs.Application` +and `jakarta.websocket.server.ServerApplicationConfig`, respectively. +JAX-RS uses `@ApplicationPath` on application subclasses to provide +this root path, but since there is no equivalent in the WebSocket +API, Helidon MP also supports `@ApplicationPath` as an annotation +on `jakarta.websocket.server.ServerApplicationConfig` implementations. + +For instance, if in our example we include the following class: + +[source,java] +---- +@ApplicationPath("/web") +public class MessageBoardApplication implements ServerApplicationConfig { + @Override + public Set getEndpointConfigs( + Set> endpoints) { + assert endpoints.isEmpty(); + return Collections.emptySet(); // No programmatic endpoints + } + + @Override + public Set> getAnnotatedEndpointClasses(Set> endpoints) { + return endpoints; // Returned scanned endpoints + } +} +---- + +the root path for WebSocket endpoints will be `"/web"` instead of the default +`"/websocket"`. + +Helidon MP provides developers the option to control the application's `main`, +including the server bootstrap steps. For this reason, the builder for +`io.helidon.microprofile.server.Server` also accepts a class of type +`Class` as a way to configure a +server manually. Using this builder would be required for those applications +for which CDI discovery is disabled. + +For more information on the MP version of this example, the reader is +referred to [4]. + + +- [1] https://projects.eclipse.org/projects/ee4j.tyrus +- [2] https://projects.eclipse.org/projects/ee4j.websocket +- [3] (Helidon SE Example) +- [4] (Helidon MP Example) \ No newline at end of file From d6e23e8273a9eee9dad35c1234350dcaeeedec8c Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 29 Jan 2020 13:26:19 -0500 Subject: [PATCH 22/35] Fixed copyright. Signed-off-by: Santiago Pericas-Geertsen --- docs/src/main/docs/websocket/01_overview.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/docs/websocket/01_overview.adoc b/docs/src/main/docs/websocket/01_overview.adoc index 7be492bae8f..58610170a96 100644 --- a/docs/src/main/docs/websocket/01_overview.adoc +++ b/docs/src/main/docs/websocket/01_overview.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 22865ca8d7a4607769e9a42a1d34481fdbf2872f Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 30 Jan 2020 11:22:15 -0500 Subject: [PATCH 23/35] Dropped support @ApplicationPath on websocket classes in favor of @RoutingPath/@RoutingName. --- docs/src/main/docs/websocket/01_overview.adoc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/src/main/docs/websocket/01_overview.adoc b/docs/src/main/docs/websocket/01_overview.adoc index 58610170a96..f2bf4879d25 100644 --- a/docs/src/main/docs/websocket/01_overview.adoc +++ b/docs/src/main/docs/websocket/01_overview.adoc @@ -204,7 +204,7 @@ session and the message received that triggered the call. So what else is needed to run this Helidon MP app? Nothing else other than the supporting classes `MessageQueue` and `UppercaseEncoder`. Helidon MP declares both `@Path` and `@ServerEndpoint` as -bean-defining annotation, so all that is needed is for CDI +bean defining annotation, so all that is needed is for CDI discovery to be enabled. By default, all JAX-RS resources will be placed under the @@ -214,14 +214,15 @@ by providing subclasses/implementations for `jakarta.ws.rs.Application` and `jakarta.websocket.server.ServerApplicationConfig`, respectively. JAX-RS uses `@ApplicationPath` on application subclasses to provide this root path, but since there is no equivalent in the WebSocket -API, Helidon MP also supports `@ApplicationPath` as an annotation +API, Helidon MP uses its own annotation `@RoutingPath` on `jakarta.websocket.server.ServerApplicationConfig` implementations. For instance, if in our example we include the following class: [source,java] ---- -@ApplicationPath("/web") +@ApplicationScoped +@RoutingPath("/web") public class MessageBoardApplication implements ServerApplicationConfig { @Override public Set getEndpointConfigs( @@ -238,7 +239,12 @@ public class MessageBoardApplication implements ServerApplicationConfig { ---- the root path for WebSocket endpoints will be `"/web"` instead of the default -`"/websocket"`. +`"/websocket"`. Note that `@RoutingPath` is _not_ a bean defining annotation, +thus the use of `@ApplicationScoped` --which, as before, requires CDI +bean discovery mode to be annotated. In addition to `@RoutingPath`, these +classes can be annotated with `@RoutingName` to associate an endpoint +with a Helidon named socket. Please refer to the Javadoc for that annotation +for additional information. Helidon MP provides developers the option to control the application's `main`, including the server bootstrap steps. For this reason, the builder for From 71e4f442143fa8c954648dbef4521987052d6cc9 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 30 Jan 2020 17:34:36 -0500 Subject: [PATCH 24/35] Initial support for executor services in Helidon MP. In MP, websocket endpoint methods are no longer called using Netty threads. --- .../io/helidon/common/HelidonFeatures.java | 9 ++ .../helidon/microprofile/server/Server.java | 14 +++ .../server/ServerCdiExtension.java | 108 ++++++++++++------ .../microprofile/server/WebSocketAppTest.java | 6 +- .../microprofile/server/WebSocketTest.java | 2 - .../src/test/resources/logging.properties | 24 ---- .../tyrus/TyrusReaderSubscriber.java | 34 ++++-- .../helidon/webserver/tyrus/TyrusSupport.java | 56 ++++++++- 8 files changed, 176 insertions(+), 77 deletions(-) delete mode 100644 microprofile/server/src/test/resources/logging.properties diff --git a/common/common/src/main/java/io/helidon/common/HelidonFeatures.java b/common/common/src/main/java/io/helidon/common/HelidonFeatures.java index eecb4f60249..ec9066f2d08 100644 --- a/common/common/src/main/java/io/helidon/common/HelidonFeatures.java +++ b/common/common/src/main/java/io/helidon/common/HelidonFeatures.java @@ -166,6 +166,15 @@ public static void flavor(HelidonFlavor flavor) { CURRENT_FLAVOR.compareAndSet(null, flavor); } + /** + * Get the currently set Helidon flavor. + * + * @return current flavor + */ + public static HelidonFlavor flavor() { + return CURRENT_FLAVOR.get(); + } + static final class Node { private final Map children = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); private final String name; diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java index 2670721be4a..6cc436dbf2e 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java @@ -71,6 +71,20 @@ static Server create(Class... applicationClasses) throws return builder.build(); } + /** + * Create a server instance using a Websocket application class. + * + * @param applicationClass websocket application class + * @return server instance to be started + * @throws MpException in case the server fails to be created + * @see #builder() + */ + static Server create(Class applicationClass) throws MpException { + Builder builder = builder(); + builder.websocketApplication(applicationClass); + return builder.build(); + } + /** * Create a server instance for discovered JAX-RS application (through CDI). * diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index 26bb8690c75..e4afd8160ae 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -45,7 +45,6 @@ import javax.enterprise.inject.spi.Extension; import javax.websocket.server.ServerApplicationConfig; import javax.websocket.server.ServerEndpointConfig; -import javax.ws.rs.ApplicationPath; import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; @@ -213,11 +212,16 @@ private void registerWebSockets(BeanManager beanManager, ServerConfiguration ser WebSocketApplication app = extension.toWebSocketApplication(); // If application present call its methods - String path = DEFAULT_WEBSOCKET_PATH; TyrusSupport.Builder builder = TyrusSupport.builder(); - Optional> applicationClass = app.applicationClass(); - if (applicationClass.isPresent()) { - Class c = applicationClass.get(); + Optional> appClass = app.applicationClass(); + + Optional contextRoot = appClass.flatMap(c -> findContextRoot(config, c)); + Optional namedRouting = appClass.flatMap(c -> findNamedRouting(config, c)); + boolean routingNameRequired = appClass.map(c -> isNamedRoutingRequired(config, c)).orElse(false); + + Routing.Builder routing; + if (appClass.isPresent()) { + Class c = appClass.get(); // Attempt to instantiate via CDI ServerApplicationConfig instance = null; @@ -235,33 +239,62 @@ private void registerWebSockets(BeanManager beanManager, ServerConfiguration ser throw new RuntimeException("Unable to instantiate websocket application " + c, e); } } + + // Call methods in application class Set endpointConfigs = instance.getEndpointConfigs(app.programmaticEndpoints()); Set> endpointClasses = instance.getAnnotatedEndpointClasses(app.annotatedEndpoints()); - // Helidon extension - allow @ApplicationPath class for WebSockets - ApplicationPath appPath = c.getAnnotation(ApplicationPath.class); - if (appPath != null) { - path = appPath.value(); - } - // Register classes and configs endpointClasses.forEach(builder::register); endpointConfigs.forEach(builder::register); + + // Create routing builder + routing = routingBuilder(namedRouting, routingNameRequired, serverConfig, c.getName()); } else { // Direct registration without calling application class app.annotatedEndpoints().forEach(builder::register); app.programmaticEndpoints().forEach(builder::register); + + // Create routing builder + routing = serverRoutingBuilder(); } // Finally register WebSockets in Helidon routing - LOGGER.info("Registering websocket application at " + path); - Routing.Builder routing = serverRoutingBuilder(); - routing.register(path, builder.build()); + String rootPath = contextRoot.orElse(DEFAULT_WEBSOCKET_PATH); + LOGGER.info("Registering websocket application at " + rootPath); + routing.register(rootPath, builder.build()); } catch (IllegalArgumentException e) { throw new RuntimeException("Unable to load WebSocket extension", e); } } + private Optional findContextRoot(io.helidon.config.Config config, + Class applicationClass) { + return config.get(applicationClass.getName() + "." + RoutingPath.CONFIG_KEY_PATH) + .asString() + .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingPath.class)) + .map(RoutingPath::value)) + .map(path -> path.startsWith("/") ? path : ("/" + path)); + } + + private Optional findNamedRouting(io.helidon.config.Config config, + Class applicationClass) { + return config.get(applicationClass.getName() + "." + RoutingName.CONFIG_KEY_NAME) + .asString() + .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingName.class)) + .map(RoutingName::value)) + .flatMap(name -> RoutingName.DEFAULT_NAME.equals(name) ? Optional.empty() : Optional.of(name)); + } + + private boolean isNamedRoutingRequired(io.helidon.config.Config config, + Class applicationClass) { + return config.get(applicationClass.getName() + "." + RoutingName.CONFIG_KEY_REQUIRED) + .asBoolean() + .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingName.class)) + .map(RoutingName::required)) + .orElse(false); + } + private void registerClasspathStaticContent(Config config) { Config context = config.get("context"); @@ -314,37 +347,43 @@ private void addApplication(ServerConfiguration serverConfig, JaxRsCdiExtension + ", routingNameRequired: " + routingNameRequired); } - Routing.Builder routing; + Routing.Builder routing = routingBuilder(namedRouting, routingNameRequired, serverConfig, + applicationMeta.appName()); + + JerseySupport jerseySupport = jaxRs.toJerseySupport(jaxRsExecutorService, applicationMeta); + if (contextRoot.isPresent()) { + String contextRootString = contextRoot.get(); + LOGGER.fine(() -> "JAX-RS application " + applicationMeta.appName() + " registered on '" + contextRootString + "'"); + routing.register(contextRootString, jerseySupport); + } else { + LOGGER.fine(() -> "JAX-RS application " + applicationMeta.appName() + " registered on '/'"); + routing.register(jerseySupport); + } + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private Routing.Builder routingBuilder(Optional namedRouting, boolean routingNameRequired, + ServerConfiguration serverConfig, String appName) { if (namedRouting.isPresent()) { String socket = namedRouting.get(); if (null == serverConfig.socket(socket)) { if (routingNameRequired) { - throw new IllegalStateException("JAX-RS application " - + applicationMeta.appName() - + " requires routing " - + socket - + " to exist, yet such a socket is not configured for web server"); + throw new IllegalStateException("Application " + + appName + + " requires routing " + + socket + + " to exist, yet such a socket is not configured for web server"); } else { LOGGER.info("Routing " + socket + " does not exist, using default routing for application " - + applicationMeta.appName()); + + appName); - routing = serverRoutingBuilder(); + return serverRoutingBuilder(); } } else { - routing = serverNamedRoutingBuilder(socket); + return serverNamedRoutingBuilder(socket); } } else { - routing = serverRoutingBuilder(); - } - - JerseySupport jerseySupport = jaxRs.toJerseySupport(jaxRsExecutorService, applicationMeta); - if (contextRoot.isPresent()) { - String contextRootString = contextRoot.get(); - LOGGER.fine(() -> "JAX-RS application " + applicationMeta.appName() + " registered on '" + contextRootString + "'"); - routing.register(contextRootString, jerseySupport); - } else { - LOGGER.fine(() -> "JAX-RS application " + applicationMeta.appName() + " registered on '/'"); - routing.register(jerseySupport); + return serverRoutingBuilder(); } } @@ -362,6 +401,7 @@ private void registerWebServerServices(BeanManager beanManager, } } + private static List> prioritySort(Set> beans) { List> prioritized = new ArrayList<>(beans); prioritized.sort((o1, o2) -> { diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java index 0e0b229bb35..b39e80ad160 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java +++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java @@ -16,7 +16,7 @@ package io.helidon.microprofile.server; -import javax.enterprise.context.Dependent; +import javax.enterprise.context.ApplicationScoped; import javax.websocket.CloseReason; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; @@ -29,7 +29,7 @@ import javax.websocket.server.ServerApplicationConfig; import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; -import javax.ws.rs.ApplicationPath; + import java.io.IOException; import java.net.URI; import java.util.Collections; @@ -74,7 +74,7 @@ public void testEchoProg() throws Exception { echoClient.echo("hi", "how are you?"); } - @ApplicationPath("/web") + @RoutingPath("/web") public static class EndpointApplication implements ServerApplicationConfig { @Override public Set getEndpointConfigs(Set> endpoints) { diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java index d9b1706a42b..9dfc1d03799 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java +++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java @@ -16,7 +16,6 @@ package io.helidon.microprofile.server; -import javax.enterprise.context.Dependent; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; @@ -57,7 +56,6 @@ public void testEcho() throws Exception { echoClient.echo("hi", "how are you?"); } - @Dependent @ServerEndpoint("/echo") public static class EchoEndpoint { private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); diff --git a/microprofile/server/src/test/resources/logging.properties b/microprofile/server/src/test/resources/logging.properties deleted file mode 100644 index c50d17e8bb2..00000000000 --- a/microprofile/server/src/test/resources/logging.properties +++ /dev/null @@ -1,24 +0,0 @@ -# -# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -handlers = java.util.logging.ConsoleHandler - -java.util.logging.ConsoleHandler.level = FINEST -java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter -java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n - -.level = WARNING - diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java index 8a01e75d593..30523bc5684 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java @@ -17,6 +17,7 @@ package io.helidon.webserver.tyrus; import java.nio.ByteBuffer; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Flow; import java.util.logging.Logger; @@ -35,40 +36,51 @@ public class TyrusReaderSubscriber implements Flow.Subscriber { private static final Logger LOGGER = Logger.getLogger(TyrusSupport.class.getName()); - private static final int MAX_RETRIES = 3; + private static final int MAX_RETRIES = 5; private static final CloseReason CONNECTION_CLOSED = new CloseReason(NORMAL_CLOSURE, "Connection closed"); private final Connection connection; + private final ExecutorService executorService; + private Flow.Subscription subscription; - TyrusReaderSubscriber(Connection connection) { + TyrusReaderSubscriber(Connection connection, ExecutorService executorService) { if (connection == null) { throw new IllegalArgumentException("Connection cannot be null"); } this.connection = connection; + this.executorService = executorService; } @Override public void onSubscribe(Flow.Subscription subscription) { - subscription.request(Long.MAX_VALUE); + this.subscription = subscription; + subscription.request(1L); } @Override public void onNext(DataChunk item) { - // Send data to Tyrus - ByteBuffer data = item.data(); - connection.getReadHandler().handle(data); + if (executorService == null) { + submitBuffer(item.data()); + } else { + executorService.submit(() -> submitBuffer(item.data())); + } + } - // Retry a few times if Tyrus did not consume all data + /** + * Submits data buffer to Tyrus. Retries a few times to make sure the entire buffer + * is consumed or logs an error. + * + * @param data Data buffer. + */ + private void submitBuffer(ByteBuffer data) { int retries = MAX_RETRIES; while (data.remaining() > 0 && retries-- > 0) { - LOGGER.warning("Tyrus did not consume all data buffer"); connection.getReadHandler().handle(data); } - - // Report error if data is still unconsumed if (retries == 0) { - throw new RuntimeException("Tyrus unable to consume data buffer"); + LOGGER.warning("Tyrus did not consume all data buffer after " + MAX_RETRIES + " retries"); } + subscription.request(1L); } @Override diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index 50466f4d6a0..fea57532462 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -22,18 +22,28 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import javax.websocket.DeploymentException; import javax.websocket.server.HandshakeRequest; import javax.websocket.server.ServerEndpointConfig; +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.common.configurable.ServerThreadPoolSupplier; +import io.helidon.common.context.Contexts; +import io.helidon.config.Config; import io.helidon.webserver.Handler; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import org.eclipse.microprofile.config.ConfigProvider; import org.glassfish.tyrus.core.RequestContext; import org.glassfish.tyrus.core.TyrusUpgradeResponse; import org.glassfish.tyrus.core.TyrusWebSocketEngine; @@ -54,15 +64,22 @@ public class TyrusSupport implements Service { */ private static final ByteBuffer FLUSH_BUFFER = ByteBuffer.allocateDirect(0); + private static final AtomicReference DEFAULT_THREAD_POOL = new AtomicReference<>(); + private final WebSocketEngine engine; private final TyrusHandler handler = new TyrusHandler(); private Set> endpointClasses; private Set endpointConfigs; + private ExecutorService executorService; TyrusSupport(WebSocketEngine engine, Set> endpointClasses, Set endpointConfigs) { this.engine = engine; this.endpointClasses = endpointClasses; this.endpointConfigs = endpointConfigs; + this.executorService = createExecutorService(); + if (this.executorService != null) { + this.executorService = Contexts.wrap(this.executorService); + } } /** @@ -184,6 +201,25 @@ public WebSocketEngine getWebSocketEngine() { } } + /** + * Creates executor service for Websocket in MP. No executor for SE. + * + * @return Executor service or {@code null}. + */ + private static ExecutorService createExecutorService() { + if (HelidonFeatures.flavor() == HelidonFlavor.MP && DEFAULT_THREAD_POOL.get() == null) { + Config executorConfig = ((Config) ConfigProvider.getConfig()) + .get("websocket.executor-service"); + + DEFAULT_THREAD_POOL.set(ServerThreadPoolSupplier.builder() + .name("websocket") + .config(executorConfig) + .build() + .get()); + } + return DEFAULT_THREAD_POOL.get(); + } + /** * A Helidon handler that integrates with Tyrus and can process WebSocket * upgrade requests. @@ -237,12 +273,26 @@ public void accept(ServerRequest req, ServerResponse res) { publisherWriter.write(FLUSH_BUFFER, null); // Setup the WebSocket connection and internally the ReaderHandler - Connection connection = upgradeInfo.createConnection(publisherWriter, - closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason)); + Connection connection; + if (executorService != null) { + try { + // Set up connection and call @onOpen + Future future = executorService.submit(() -> + upgradeInfo.createConnection(publisherWriter, + closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason))); + connection = future.get(); // Need to sync here + } catch (InterruptedException | ExecutionException e) { + LOGGER.warning("Unable to create websocket connection"); + throw new RuntimeException(e); + } + } else { + connection = upgradeInfo.createConnection(publisherWriter, + closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason)); + } // Set up reader to pass data back to Tyrus if (connection != null) { - TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection); + TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection, executorService); req.content().subscribe(subscriber); } } From 70e696eeca2dc8e76720ee9a17acfa6ac617b308 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 31 Jan 2020 08:20:28 -0500 Subject: [PATCH 25/35] Updated tests to verify threads in which endpoint methods are called. --- .../microprofile/server/WebSocketTest.java | 22 +++++++++++++++++-- .../helidon/webserver/tyrus/EchoEndpoint.java | 20 ++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java index 9dfc1d03799..016f6bb97e5 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java +++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java @@ -56,6 +56,21 @@ public void testEcho() throws Exception { echoClient.echo("hi", "how are you?"); } + /** + * Verify that endpoint methods are running in a Helidon thread pool. + * + * @param session Websocket session. + * @param logger A logger. + * @throws IOException Exception during close. + */ + private static void verifyRunningThread(Session session, Logger logger) throws IOException { + Thread thread = Thread.currentThread(); + if (!thread.getName().contains("helidon")) { + logger.warning("Websocket handler running in incorrect thread " + thread); + session.close(); + } + } + @ServerEndpoint("/echo") public static class EchoEndpoint { private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); @@ -63,22 +78,25 @@ public static class EchoEndpoint { @OnOpen public void onOpen(Session session) throws IOException { LOGGER.info("OnOpen called"); + verifyRunningThread(session, LOGGER); } @OnMessage public void echo(Session session, String message) throws Exception { LOGGER.info("Endpoint OnMessage called '" + message + "'"); + verifyRunningThread(session, LOGGER); session.getBasicRemote().sendObject(message); } @OnError - public void onError(Throwable t) { + public void onError(Throwable t) throws IOException { LOGGER.info("OnError called"); } @OnClose - public void onClose(Session session) { + public void onClose(Session session) throws IOException { LOGGER.info("OnClose called"); + verifyRunningThread(session, LOGGER); } } } diff --git a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java index 948b8bbc4c5..8e06b799ae3 100644 --- a/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java +++ b/webserver/tyrus/src/test/java/io/helidon/webserver/tyrus/EchoEndpoint.java @@ -47,6 +47,21 @@ public class EchoEndpoint { static AtomicBoolean modifyHandshakeCalled = new AtomicBoolean(false); + /** + * Verify that endpoint methods are running in a Helidon thread pool. + * + * @param session Websocket session. + * @param logger A logger. + * @throws IOException Exception during close. + */ + private static void verifyRunningThread(Session session, Logger logger) throws IOException { + Thread thread = Thread.currentThread(); + if (!thread.getName().contains("EventLoop")) { + logger.warning("Websocket handler running in incorrect thread " + thread); + session.close(); + } + } + public static class ServerConfigurator extends ServerEndpointConfig.Configurator { @Override @@ -60,6 +75,7 @@ public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, @OnOpen public void onOpen(Session session) throws IOException { LOGGER.info("OnOpen called"); + verifyRunningThread(session, LOGGER); if (!modifyHandshakeCalled.get()) { session.close(); // unexpected } @@ -68,6 +84,7 @@ public void onOpen(Session session) throws IOException { @OnMessage public void echo(Session session, String message) throws Exception { LOGGER.info("Endpoint OnMessage called '" + message + "'"); + verifyRunningThread(session, LOGGER); if (!isDecoded(message)) { throw new InternalError("Message has not been decoded"); } @@ -81,8 +98,9 @@ public void onError(Throwable t) { } @OnClose - public void onClose(Session session) { + public void onClose(Session session) throws IOException { LOGGER.info("OnClose called"); + verifyRunningThread(session, LOGGER); modifyHandshakeCalled.set(false); } } From 0d0e215cfb326c55d421d679515926ec947caa61 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 31 Jan 2020 08:39:35 -0500 Subject: [PATCH 26/35] Updated docs explaining websocket threading model for SE and MP. --- docs/src/main/docs/websocket/01_overview.adoc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/src/main/docs/websocket/01_overview.adoc b/docs/src/main/docs/websocket/01_overview.adoc index f2bf4879d25..01a23747e7f 100644 --- a/docs/src/main/docs/websocket/01_overview.adoc +++ b/docs/src/main/docs/websocket/01_overview.adoc @@ -141,6 +141,11 @@ with it a _message encoder_. For more information on message encoders and decode reader is referred to [2]; in this example, `UppercaseEncoder.class` simply uppercases every message sent from the server [3]. +Endpoint methods in Helidon SE are executed in Netty's worker thread pool. Threads in this +pool are intended to be _non-blocking_, thus it is recommended for any blocking or +long-running operation triggered by an endpoint method to be executed using a separate +thread pool. See the documentation for `io.helidon.common.configurable.ThreadPoolSupplier`. + == Helidon MP @@ -253,11 +258,16 @@ including the server bootstrap steps. For this reason, the builder for server manually. Using this builder would be required for those applications for which CDI discovery is disabled. +Unlike Helidon SE, all endpoint methods in Helidon MP are executed in +a separate thread pool, independently of Netty. Therefore, there is no +need to create additional threads for blocking or long-running operations +as these will not affect Netty's ability to process networking data. + For more information on the MP version of this example, the reader is referred to [4]. - [1] https://projects.eclipse.org/projects/ee4j.tyrus - [2] https://projects.eclipse.org/projects/ee4j.websocket -- [3] (Helidon SE Example) -- [4] (Helidon MP Example) \ No newline at end of file +- [3] https://github.com/oracle/helidon/tree/websockets20/examples/webserver/websocket +- [4] https://github.com/oracle/helidon/tree/websockets20/examples/microprofile/websocket \ No newline at end of file From 3e2d7867d805a388237d46ddcb599bbd1151a65f Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 4 Feb 2020 09:22:39 -0500 Subject: [PATCH 27/35] Minor improvements and fixes to logging calls. --- .../helidon/microprofile/tyrus/WebSocketCdiExtension.java | 8 ++++---- .../java/io/helidon/webserver/tyrus/TyrusSupport.java | 5 ++--- .../main/java/io/helidon/webserver/ForwardingHandler.java | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java index 2d136e7b3f9..472f47fcab0 100644 --- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java @@ -47,7 +47,7 @@ public class WebSocketCdiExtension implements Extension { * @param applicationClass Application class. */ private void applicationClass(@Observes ProcessAnnotatedType applicationClass) { - LOGGER.info(() -> "Application class found " + applicationClass.getAnnotatedType().getJavaClass()); + LOGGER.finest(() -> "Application class found " + applicationClass.getAnnotatedType().getJavaClass()); appBuilder.applicationClass(applicationClass.getAnnotatedType().getJavaClass()); } @@ -57,7 +57,7 @@ private void applicationClass(@Observes ProcessAnnotatedType applicationClass) { - LOGGER.info(() -> "Using manually set application class " + applicationClass); + LOGGER.finest(() -> "Using manually set application class " + applicationClass); appBuilder.updateApplicationClass(applicationClass); } @@ -67,7 +67,7 @@ public void applicationClass(Class applicatio * @param endpoint The endpoint. */ private void endpointClasses(@Observes @WithAnnotations(ServerEndpoint.class) ProcessAnnotatedType endpoint) { - LOGGER.info(() -> "Annotated endpoint found " + endpoint.getAnnotatedType().getJavaClass()); + LOGGER.finest(() -> "Annotated endpoint found " + endpoint.getAnnotatedType().getJavaClass()); appBuilder.annotatedEndpoint(endpoint.getAnnotatedType().getJavaClass()); } @@ -77,7 +77,7 @@ private void endpointClasses(@Observes @WithAnnotations(ServerEndpoint.class) Pr * @param endpoint The endpoint. */ private void endpointConfig(@Observes ProcessAnnotatedType endpoint) { - LOGGER.info(() -> "Programmatic endpoint found " + endpoint.getAnnotatedType().getJavaClass()); + LOGGER.finest(() -> "Programmatic endpoint found " + endpoint.getAnnotatedType().getJavaClass()); appBuilder.programmaticEndpoint(endpoint.getAnnotatedType().getJavaClass()); } diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index fea57532462..1ee1249cf27 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -236,7 +236,7 @@ private class TyrusHandler implements Handler { public void accept(ServerRequest req, ServerResponse res) { // Skip this handler if not an upgrade request Optional secWebSocketKey = req.headers().value(HandshakeRequest.SEC_WEBSOCKET_KEY); - if (!secWebSocketKey.isPresent()) { + if (secWebSocketKey.isEmpty()) { req.next(); return; } @@ -247,8 +247,7 @@ public void accept(ServerRequest req, ServerResponse res) { RequestContext requestContext = RequestContext.Builder.create() .requestURI(URI.create(req.path().toString())) // excludes context path .build(); - req.headers().toMap().entrySet().forEach(e -> - requestContext.getHeaders().put(e.getKey(), e.getValue())); + req.headers().toMap().forEach((key, value) -> requestContext.getHeaders().put(key, value)); // Use Tyrus to process a WebSocket upgrade request final TyrusUpgradeResponse upgradeResponse = new TyrusUpgradeResponse(); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java index 18fd9ffb46c..bd5e0712275 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ForwardingHandler.java @@ -215,7 +215,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) { throw new IllegalStateException("Received ByteBuf without upgrading to WebSockets"); } // Simply forward raw bytebuf to Tyrus for processing - LOGGER.fine("Received ByteBuf of WebSockets connection" + msg); + LOGGER.finest(() -> "Received ByteBuf of WebSockets connection" + msg); requestContext.publisher().submit((ByteBuf) msg); } } From 0f9a2f5df5d17400278e03affc5c1dc9a5bf24f6 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 4 Feb 2020 10:09:27 -0500 Subject: [PATCH 28/35] Ensure Netty's thread is not blocked when setting up a connection in MP. --- .../helidon/webserver/tyrus/TyrusSupport.java | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index 1ee1249cf27..6b999628c5d 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -255,8 +256,7 @@ public void accept(ServerRequest req, ServerResponse res) { // Respond to upgrade request using response from Tyrus res.status(upgradeResponse.getStatus()); - upgradeResponse.getHeaders().entrySet().forEach(e -> - res.headers().add(e.getKey(), e.getValue())); + upgradeResponse.getHeaders().forEach((key, value) -> res.headers().add(key, value)); TyrusWriterPublisher publisherWriter = new TyrusWriterPublisher(); res.send(publisherWriter); @@ -271,28 +271,24 @@ public void accept(ServerRequest req, ServerResponse res) { // Flush upgrade response publisherWriter.write(FLUSH_BUFFER, null); - // Setup the WebSocket connection and internally the ReaderHandler - Connection connection; + // Setup the WebSocket connection and subscriber, calls @onOpen if (executorService != null) { - try { - // Set up connection and call @onOpen - Future future = executorService.submit(() -> - upgradeInfo.createConnection(publisherWriter, - closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason))); - connection = future.get(); // Need to sync here - } catch (InterruptedException | ExecutionException e) { - LOGGER.warning("Unable to create websocket connection"); - throw new RuntimeException(e); - } + CompletableFuture future = + CompletableFuture.supplyAsync( + () -> upgradeInfo.createConnection(publisherWriter, + closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason)), + executorService); + future.thenAccept(c -> { + TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(c, executorService); + req.content().subscribe(subscriber); + }); } else { - connection = upgradeInfo.createConnection(publisherWriter, + Connection connection = upgradeInfo.createConnection(publisherWriter, closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason)); - } - - // Set up reader to pass data back to Tyrus - if (connection != null) { - TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection, executorService); - req.content().subscribe(subscriber); + if (connection != null) { + TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection, executorService); + req.content().subscribe(subscriber); + } } } } From 09534885d4a6b362a86dcb78b1fb298a57bdcf14 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 4 Feb 2020 10:13:54 -0500 Subject: [PATCH 29/35] Fixed checkstyle. Signed-off-by: Santiago Pericas-Geertsen --- .../src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index 6b999628c5d..2adba44fe3c 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -23,9 +23,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; From 946db23e8bbcfa6b3acd4c812ec30e6942915d83 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 5 Feb 2020 15:50:15 -0500 Subject: [PATCH 30/35] Re-shuffled code to remove dependency with websockets API and Tyrus in microprofile's server module. Use CDI events between modules for loose coupling. --- examples/microprofile/websocket/pom.xml | 4 + microprofile/server/pom.xml | 4 - .../helidon/microprofile/server/Server.java | 12 +- .../server/ServerApplicationConfigEvent.java | 49 ++++++ .../server/ServerCdiExtension.java | 112 ++------------ .../microprofile/server/ServerImpl.java | 10 +- .../server/src/main/java/module-info.java | 4 - microprofile/tyrus/pom.xml | 8 + .../tyrus/WebSocketCdiExtension.java | 143 +++++++++++++++++- .../tyrus/src/main/java/module-info.java | 4 + .../microprofile/tyrus}/EchoClient.java | 2 +- .../microprofile/tyrus/EchoEndpoint.java | 35 ----- .../microprofile/tyrus/EchoEndpointProg.java | 43 ------ .../microprofile/tyrus}/WebSocketAppTest.java | 5 +- ...lication.java => WebSocketBadAppTest.java} | 34 +++-- .../tyrus/WebSocketCdiExtensionTest.java | 62 -------- .../microprofile/tyrus}/WebSocketTest.java | 3 +- 17 files changed, 254 insertions(+), 280 deletions(-) create mode 100644 microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java rename microprofile/{server/src/test/java/io/helidon/microprofile/server => tyrus/src/test/java/io/helidon/microprofile/tyrus}/EchoClient.java (99%) delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java rename microprofile/{server/src/test/java/io/helidon/microprofile/server => tyrus/src/test/java/io/helidon/microprofile/tyrus}/WebSocketAppTest.java (97%) rename microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/{EndpointApplication.java => WebSocketBadAppTest.java} (52%) delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java rename microprofile/{server/src/test/java/io/helidon/microprofile/server => tyrus/src/test/java/io/helidon/microprofile/tyrus}/WebSocketTest.java (97%) diff --git a/examples/microprofile/websocket/pom.xml b/examples/microprofile/websocket/pom.xml index a343394b202..0754e4828e0 100644 --- a/examples/microprofile/websocket/pom.xml +++ b/examples/microprofile/websocket/pom.xml @@ -40,6 +40,10 @@ io.helidon.microprofile.bundles helidon-microprofile + + io.helidon.microprofile.tyrus + helidon-microprofile-tyrus + io.helidon.microprofile.bundles internal-test-libs diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index 51696df8c60..7034cb6abbe 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -52,10 +52,6 @@ io.helidon.common helidon-common-service-loader - - io.helidon.microprofile.tyrus - helidon-microprofile-tyrus - javax.interceptor javax.interceptor-api diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java index 6cc436dbf2e..6ceb1abc041 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java @@ -26,7 +26,6 @@ import java.util.logging.Logger; import javax.enterprise.inject.spi.CDI; -import javax.websocket.server.ServerApplicationConfig; import javax.ws.rs.core.Application; import io.helidon.common.configurable.ServerThreadPoolSupplier; @@ -72,14 +71,15 @@ static Server create(Class... applicationClasses) throws } /** - * Create a server instance using a Websocket application class. + * Create a server instance using a Websocket application class. Application class + * is of type {@code lass}. * * @param applicationClass websocket application class * @return server instance to be started * @throws MpException in case the server fails to be created * @see #builder() */ - static Server create(Class applicationClass) throws MpException { + static Server create(Class applicationClass) throws MpException { Builder builder = builder(); builder.websocketApplication(applicationClass); return builder.build(); @@ -159,7 +159,7 @@ final class Builder { private Supplier defaultExecutorService; private JaxRsCdiExtension jaxRs; private boolean retainDiscovered = false; - private Class wsApplication; + private Class wsApplication; private Builder() { if (!IN_PROGRESS_OR_RUNNING.compareAndSet(false, true)) { @@ -389,7 +389,7 @@ public Builder addApplication(Application application) { * @param wsApplication websocket application * @return modified builder */ - public Builder websocketApplication(Class wsApplication) { + public Builder websocketApplication(Class wsApplication) { if (this.wsApplication != null) { throw new IllegalStateException("Cannot register more than one websocket application"); } @@ -523,7 +523,7 @@ int port() { return port; } - Optional> websocketApplication() { + Optional> websocketApplication() { return Optional.ofNullable(wsApplication); } } diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java new file mode 100644 index 00000000000..d2f9cc0d094 --- /dev/null +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.server; + +/** + * Class ServerApplicationConfigEvent. This wrapper class is used by the server + * to inform the {@code WebSocketCdiExtension} that an application class has been + * set in its builder and that any scanned classes should be ignored. + */ +public class ServerApplicationConfigEvent { + + /** + * Application class of type {@code Class}, + * we use {@code Class} to avoid a static dependency with the WebSocket API. + */ + private final Class applicationClass; + + /** + * Construct an event given a websocket application class. + * + * @param applicationClass Application class. + */ + public ServerApplicationConfigEvent(Class applicationClass) { + this.applicationClass = applicationClass; + } + + /** + * Get access to application class. + * + * @return Application class. + */ + public Class applicationClass() { + return applicationClass; + } +} diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index e4afd8160ae..e2e6dbb580a 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -37,14 +37,10 @@ import javax.enterprise.context.Initialized; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.event.Observes; -import javax.enterprise.inject.UnsatisfiedResolutionException; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; -import javax.enterprise.inject.spi.CDI; import javax.enterprise.inject.spi.DeploymentException; import javax.enterprise.inject.spi.Extension; -import javax.websocket.server.ServerApplicationConfig; -import javax.websocket.server.ServerEndpointConfig; import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; @@ -53,8 +49,6 @@ import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.microprofile.cdi.RuntimeStart; -import io.helidon.microprofile.tyrus.WebSocketApplication; -import io.helidon.microprofile.tyrus.WebSocketCdiExtension; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; import io.helidon.webserver.Service; @@ -62,7 +56,6 @@ import io.helidon.webserver.StaticContentSupport; import io.helidon.webserver.WebServer; import io.helidon.webserver.jersey.JerseySupport; -import io.helidon.webserver.tyrus.TyrusSupport; import org.eclipse.microprofile.config.ConfigProvider; @@ -79,8 +72,6 @@ public class ServerCdiExtension implements Extension { private static final Logger LOGGER = Logger.getLogger(ServerCdiExtension.class.getName()); - private static final String DEFAULT_WEBSOCKET_PATH = "/websocket"; - // build time private ServerConfiguration.Builder serverConfigBuilder = ServerConfiguration.builder() .port(7001); @@ -126,9 +117,6 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( // register static content if configured registerStaticContent(); - // register websocket endpoints - registerWebSockets(beanManager, serverConfig); - // start the webserver WebServer.Builder wsBuilder = WebServer.builder(routingBuilder.build()); wsBuilder.config(serverConfig); @@ -206,95 +194,6 @@ private void registerPathStaticContent(Config config) { } } - private void registerWebSockets(BeanManager beanManager, ServerConfiguration serverConfig) { - try { - WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class); - WebSocketApplication app = extension.toWebSocketApplication(); - - // If application present call its methods - TyrusSupport.Builder builder = TyrusSupport.builder(); - Optional> appClass = app.applicationClass(); - - Optional contextRoot = appClass.flatMap(c -> findContextRoot(config, c)); - Optional namedRouting = appClass.flatMap(c -> findNamedRouting(config, c)); - boolean routingNameRequired = appClass.map(c -> isNamedRoutingRequired(config, c)).orElse(false); - - Routing.Builder routing; - if (appClass.isPresent()) { - Class c = appClass.get(); - - // Attempt to instantiate via CDI - ServerApplicationConfig instance = null; - try { - instance = CDI.current().select(c).get(); - } catch (UnsatisfiedResolutionException e) { - // falls through - } - - // Otherwise, we create instance directly - if (instance == null) { - try { - instance = c.getDeclaredConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException("Unable to instantiate websocket application " + c, e); - } - } - - // Call methods in application class - Set endpointConfigs = instance.getEndpointConfigs(app.programmaticEndpoints()); - Set> endpointClasses = instance.getAnnotatedEndpointClasses(app.annotatedEndpoints()); - - // Register classes and configs - endpointClasses.forEach(builder::register); - endpointConfigs.forEach(builder::register); - - // Create routing builder - routing = routingBuilder(namedRouting, routingNameRequired, serverConfig, c.getName()); - } else { - // Direct registration without calling application class - app.annotatedEndpoints().forEach(builder::register); - app.programmaticEndpoints().forEach(builder::register); - - // Create routing builder - routing = serverRoutingBuilder(); - } - - // Finally register WebSockets in Helidon routing - String rootPath = contextRoot.orElse(DEFAULT_WEBSOCKET_PATH); - LOGGER.info("Registering websocket application at " + rootPath); - routing.register(rootPath, builder.build()); - } catch (IllegalArgumentException e) { - throw new RuntimeException("Unable to load WebSocket extension", e); - } - } - - private Optional findContextRoot(io.helidon.config.Config config, - Class applicationClass) { - return config.get(applicationClass.getName() + "." + RoutingPath.CONFIG_KEY_PATH) - .asString() - .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingPath.class)) - .map(RoutingPath::value)) - .map(path -> path.startsWith("/") ? path : ("/" + path)); - } - - private Optional findNamedRouting(io.helidon.config.Config config, - Class applicationClass) { - return config.get(applicationClass.getName() + "." + RoutingName.CONFIG_KEY_NAME) - .asString() - .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingName.class)) - .map(RoutingName::value)) - .flatMap(name -> RoutingName.DEFAULT_NAME.equals(name) ? Optional.empty() : Optional.of(name)); - } - - private boolean isNamedRoutingRequired(io.helidon.config.Config config, - Class applicationClass) { - return config.get(applicationClass.getName() + "." + RoutingName.CONFIG_KEY_REQUIRED) - .asBoolean() - .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingName.class)) - .map(RoutingName::required)) - .orElse(false); - } - private void registerClasspathStaticContent(Config config) { Config context = config.get("context"); @@ -361,8 +260,17 @@ private void addApplication(ServerConfiguration serverConfig, JaxRsCdiExtension } } + /** + * Provides access to routing builder. + * + * @param namedRouting Named routing. + * @param routingNameRequired Routing name required. + * @param serverConfig Server configuration. + * @param appName Application's name. + * @return The routing builder. + */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private Routing.Builder routingBuilder(Optional namedRouting, boolean routingNameRequired, + public Routing.Builder routingBuilder(Optional namedRouting, boolean routingNameRequired, ServerConfiguration serverConfig, String appName) { if (namedRouting.isPresent()) { String socket = namedRouting.get(); diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java index e19ee22522a..f731f7e291b 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java @@ -20,12 +20,12 @@ import java.net.UnknownHostException; import java.util.logging.Logger; +import javax.enterprise.event.Event; import javax.enterprise.inject.se.SeContainer; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; import io.helidon.microprofile.cdi.HelidonContainer; -import io.helidon.microprofile.tyrus.WebSocketCdiExtension; import static io.helidon.microprofile.server.Server.Builder.IN_PROGRESS_OR_RUNNING; @@ -69,9 +69,11 @@ public class ServerImpl implements Server { serverExtension.listenHost(this.host); - // Update extension with manually configured application -- overrides scanning - WebSocketCdiExtension wsExtension = beanManager.getExtension(WebSocketCdiExtension.class); - builder.websocketApplication().ifPresent(wsExtension::applicationClass); + // Inform WebSocket extension of an app specified in builder + builder.websocketApplication().ifPresent(app -> { + Event event = container.getBeanManager().getEvent(); + event.fire(new ServerApplicationConfigEvent(app)); + }); STARTUP_LOGGER.finest("Builders ready"); diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index cd480313101..d2d4c8494df 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -36,10 +36,6 @@ // there is now a hardcoded dependency on Weld, to configure additional bean defining annotation requires java.management; - requires io.helidon.microprofile.tyrus; - requires io.helidon.webserver.tyrus; - requires jakarta.websocket.api; - exports io.helidon.microprofile.server; provides javax.enterprise.inject.spi.Extension with diff --git a/microprofile/tyrus/pom.xml b/microprofile/tyrus/pom.xml index 4f4aaeed970..5a716979a57 100644 --- a/microprofile/tyrus/pom.xml +++ b/microprofile/tyrus/pom.xml @@ -36,6 +36,10 @@ org.glassfish.tyrus tyrus-core + + io.helidon.microprofile.cdi + helidon-microprofile-cdi + io.helidon.webserver helidon-webserver-tyrus @@ -62,5 +66,9 @@ internal-test-libs test + + io.helidon.microprofile.server + helidon-microprofile-server + diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java index 472f47fcab0..0f48d46974c 100644 --- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java @@ -16,18 +16,38 @@ package io.helidon.microprofile.tyrus; +import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; import javax.enterprise.event.Observes; +import javax.enterprise.inject.UnsatisfiedResolutionException; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; import javax.enterprise.inject.spi.Extension; import javax.enterprise.inject.spi.ProcessAnnotatedType; import javax.enterprise.inject.spi.WithAnnotations; import javax.websocket.Endpoint; import javax.websocket.server.ServerApplicationConfig; import javax.websocket.server.ServerEndpoint; +import javax.websocket.server.ServerEndpointConfig; import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; +import io.helidon.config.Config; +import io.helidon.microprofile.cdi.RuntimeStart; +import io.helidon.microprofile.server.RoutingName; +import io.helidon.microprofile.server.RoutingPath; +import io.helidon.microprofile.server.ServerApplicationConfigEvent; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.tyrus.TyrusSupport; + +import static javax.interceptor.Interceptor.Priority.PLATFORM_AFTER; /** * Configure Tyrus related things. @@ -35,12 +55,28 @@ public class WebSocketCdiExtension implements Extension { private static final Logger LOGGER = Logger.getLogger(WebSocketCdiExtension.class.getName()); + private static final String DEFAULT_WEBSOCKET_PATH = "/websocket"; + static { HelidonFeatures.register(HelidonFlavor.MP, "WebSocket"); } + private Config config; + + private ServerCdiExtension serverCdiExtension; + private WebSocketApplication.Builder appBuilder = WebSocketApplication.builder(); + private void prepareRuntime(@Observes @RuntimeStart Config config) { + this.config = config; + } + + private void startServer(@Observes @Priority(PLATFORM_AFTER + 99) @Initialized(ApplicationScoped.class) Object event, + BeanManager beanManager) { + serverCdiExtension = beanManager.getExtension(ServerCdiExtension.class); + registerWebSockets(beanManager, serverCdiExtension.serverConfigBuilder().build()); + } + /** * Collect application class extending {@code ServerApplicationConfig}. * @@ -86,7 +122,112 @@ private void endpointConfig(@Observes ProcessAnnotatedType e * * @return Application. */ - public WebSocketApplication toWebSocketApplication() { + WebSocketApplication toWebSocketApplication() { return appBuilder.build(); } + + /** + * Event fired off by server to inform this extension that an application class + * has been set in the server's builder and that any application class scanned + * by this extension should be ignored. + * + * @param event Event containing new application class. + */ + @SuppressWarnings("unchecked") + private void overrideApplication(@Observes ServerApplicationConfigEvent event) { + Class applicationClass = event.applicationClass(); + if (!ServerApplicationConfig.class.isAssignableFrom(applicationClass)) { + throw new IllegalArgumentException("Websocket application class " + applicationClass + + " must implement " + ServerApplicationConfig.class); + } + appBuilder.updateApplicationClass((Class) event.applicationClass()); + } + + private void registerWebSockets(BeanManager beanManager, ServerConfiguration serverConfig) { + try { + WebSocketApplication app = toWebSocketApplication(); + + // If application present call its methods + TyrusSupport.Builder builder = TyrusSupport.builder(); + Optional> appClass = app.applicationClass(); + + Optional contextRoot = appClass.flatMap(c -> findContextRoot(config, c)); + Optional namedRouting = appClass.flatMap(c -> findNamedRouting(config, c)); + boolean routingNameRequired = appClass.map(c -> isNamedRoutingRequired(config, c)).orElse(false); + + Routing.Builder routing; + if (appClass.isPresent()) { + Class c = appClass.get(); + + // Attempt to instantiate via CDI + ServerApplicationConfig instance = null; + try { + instance = CDI.current().select(c).get(); + } catch (UnsatisfiedResolutionException e) { + // falls through + } + + // Otherwise, we create instance directly + if (instance == null) { + try { + instance = c.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Unable to instantiate websocket application " + c, e); + } + } + + // Call methods in application class + Set endpointConfigs = instance.getEndpointConfigs(app.programmaticEndpoints()); + Set> endpointClasses = instance.getAnnotatedEndpointClasses(app.annotatedEndpoints()); + + // Register classes and configs + endpointClasses.forEach(builder::register); + endpointConfigs.forEach(builder::register); + + // Create routing builder + routing = serverCdiExtension.routingBuilder(namedRouting, routingNameRequired, serverConfig, c.getName()); + } else { + // Direct registration without calling application class + app.annotatedEndpoints().forEach(builder::register); + app.programmaticEndpoints().forEach(builder::register); + + // Create routing builder + routing = serverCdiExtension.serverRoutingBuilder(); + } + + // Finally register WebSockets in Helidon routing + String rootPath = contextRoot.orElse(DEFAULT_WEBSOCKET_PATH); + LOGGER.info("Registering websocket application at " + rootPath); + routing.register(rootPath, builder.build()); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Unable to load WebSocket extension", e); + } + } + + private Optional findContextRoot(io.helidon.config.Config config, + Class applicationClass) { + return config.get(applicationClass.getName() + "." + RoutingPath.CONFIG_KEY_PATH) + .asString() + .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingPath.class)) + .map(RoutingPath::value)) + .map(path -> path.startsWith("/") ? path : ("/" + path)); + } + + private Optional findNamedRouting(io.helidon.config.Config config, + Class applicationClass) { + return config.get(applicationClass.getName() + "." + RoutingName.CONFIG_KEY_NAME) + .asString() + .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingName.class)) + .map(RoutingName::value)) + .flatMap(name -> RoutingName.DEFAULT_NAME.equals(name) ? Optional.empty() : Optional.of(name)); + } + + private boolean isNamedRoutingRequired(io.helidon.config.Config config, + Class applicationClass) { + return config.get(applicationClass.getName() + "." + RoutingName.CONFIG_KEY_REQUIRED) + .asBoolean() + .or(() -> Optional.ofNullable(applicationClass.getAnnotation(RoutingName.class)) + .map(RoutingName::required)) + .orElse(false); + } } diff --git a/microprofile/tyrus/src/main/java/module-info.java b/microprofile/tyrus/src/main/java/module-info.java index 5bbc225b754..97890fe86ee 100644 --- a/microprofile/tyrus/src/main/java/module-info.java +++ b/microprofile/tyrus/src/main/java/module-info.java @@ -26,7 +26,11 @@ requires transitive jakarta.websocket.api; requires io.helidon.common; + requires io.helidon.config; + requires io.helidon.microprofile.cdi; requires tyrus.core; + requires io.helidon.microprofile.server; + requires io.helidon.webserver.tyrus; exports io.helidon.microprofile.tyrus; provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.tyrus.WebSocketCdiExtension; diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java similarity index 99% rename from microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java rename to microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java index 29224aacd44..95a99aa9f56 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/EchoClient.java +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.microprofile.server; +package io.helidon.microprofile.tyrus; import javax.websocket.ClientEndpointConfig; import javax.websocket.CloseReason; diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java deleted file mode 100644 index cc13e9c830e..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpoint.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.enterprise.context.Dependent; -import javax.websocket.OnMessage; -import javax.websocket.Session; -import javax.websocket.server.ServerEndpoint; - -/** - * Class EchoEndpoint. - */ -@Dependent -@ServerEndpoint("/echo") -public class EchoEndpoint { - - @OnMessage - public void echo(Session session, String message) throws Exception { - session.getBasicRemote().sendObject(message); - } -} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java deleted file mode 100644 index d1849d287e3..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoEndpointProg.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.enterprise.context.Dependent; -import javax.websocket.Endpoint; -import javax.websocket.EndpointConfig; -import javax.websocket.MessageHandler; -import javax.websocket.Session; - -/** - * Class EchoEndpointProg. Using WebSocket programmatic API. - */ -@Dependent -public class EchoEndpointProg extends Endpoint { - - @Override - public void onOpen(Session session, EndpointConfig endpointConfig) { - session.addMessageHandler(new MessageHandler.Whole() { - @Override - public void onMessage(String message) { - try { - session.getBasicRemote().sendObject(message); // calls encoder - } catch (Exception e) { - } - } - }); - } -} diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java similarity index 97% rename from microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java rename to microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java index b39e80ad160..82bc7c47c07 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketAppTest.java +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package io.helidon.microprofile.server; +package io.helidon.microprofile.tyrus; -import javax.enterprise.context.ApplicationScoped; import javax.websocket.CloseReason; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; @@ -36,6 +35,8 @@ import java.util.Set; import java.util.logging.Logger; +import io.helidon.microprofile.server.RoutingPath; +import io.helidon.microprofile.server.Server; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java similarity index 52% rename from microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java rename to microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java index 5f1f7597d42..63c1ca78870 100644 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EndpointApplication.java +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java @@ -16,25 +16,29 @@ package io.helidon.microprofile.tyrus; -import javax.enterprise.context.Dependent; -import javax.websocket.Endpoint; -import javax.websocket.server.ServerApplicationConfig; -import javax.websocket.server.ServerEndpointConfig; -import java.util.Collections; -import java.util.Set; +import io.helidon.microprofile.server.Server; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; /** - * Class EndpointApplication. + * Class WebSocketBadAppTest. */ -@Dependent -public class EndpointApplication implements ServerApplicationConfig { - @Override - public Set getEndpointConfigs(Set> endpointClasses) { - return Collections.emptySet(); +public class WebSocketBadAppTest { + + @BeforeAll + static void initClass() { + Server.Builder builder = Server.builder(); + builder.websocketApplication(BadEndpointApplication.class); + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + public void test() { + // no-op } - @Override - public Set> getAnnotatedEndpointClasses(Set> scanned) { - return Collections.emptySet(); + public static class BadEndpointApplication /* implements ServerApplicationConfig */ { } } diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java deleted file mode 100644 index dce9f932ff3..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketCdiExtensionTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.enterprise.inject.se.SeContainer; -import javax.enterprise.inject.spi.BeanManager; - -import io.helidon.microprofile.cdi.HelidonContainer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Class WebSocketExtensionTest. - */ -public class WebSocketCdiExtensionTest { - - private static SeContainer cdiContainer; - - @BeforeAll - public static void startCdiContainer() { - cdiContainer = HelidonContainer.instance().start(); - } - - @AfterAll - public static void shutDownCdiContainer() { - if (cdiContainer != null) { - cdiContainer.close(); - } - } - - @Test - public void testExtension() { - WebSocketApplication application = webSocketApplication(); - assertThat(application.applicationClass().isPresent(), is(true)); - assertThat(application.annotatedEndpoints().size(), is(1)); - assertThat(application.programmaticEndpoints().size(), is(1)); - } - - private WebSocketApplication webSocketApplication() { - BeanManager beanManager = cdiContainer.getBeanManager(); - WebSocketCdiExtension extension = beanManager.getExtension(WebSocketCdiExtension.class); - return extension.toWebSocketApplication(); - } -} diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java similarity index 97% rename from microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java rename to microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java index 016f6bb97e5..760d2fd0158 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/WebSocketTest.java +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.microprofile.server; +package io.helidon.microprofile.tyrus; import javax.websocket.OnClose; import javax.websocket.OnError; @@ -27,6 +27,7 @@ import java.net.URI; import java.util.logging.Logger; +import io.helidon.microprofile.server.Server; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; From 363e5b91d0bf94d209c8d129626adfa06d12b047 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 6 Feb 2020 11:49:17 -0500 Subject: [PATCH 31/35] Dropped support for websockets in Server.Builder. Created subclass for TyrusSupport in MP. Fixed tests. --- .../helidon/microprofile/server/Server.java | 35 ------ .../microprofile/server/ServerImpl.java | 7 -- .../microprofile/tyrus/TyrusSupportMp.java | 47 ++++++++ .../tyrus/WebSocketCdiExtension.java | 20 +--- .../tyrus/src/main/java/module-info.java | 1 + .../microprofile/tyrus/WebSocketAppTest.java | 26 ++++- .../tyrus/WebSocketBadAppTest.java | 44 -------- .../microprofile/tyrus/WebSocketTest.java | 103 ------------------ .../helidon/webserver/tyrus/TyrusSupport.java | 56 ++++------ 9 files changed, 94 insertions(+), 245 deletions(-) create mode 100644 microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java delete mode 100644 microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java index 6ceb1abc041..11b4d97003f 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java @@ -19,7 +19,6 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -70,21 +69,6 @@ static Server create(Class... applicationClasses) throws return builder.build(); } - /** - * Create a server instance using a Websocket application class. Application class - * is of type {@code lass}. - * - * @param applicationClass websocket application class - * @return server instance to be started - * @throws MpException in case the server fails to be created - * @see #builder() - */ - static Server create(Class applicationClass) throws MpException { - Builder builder = builder(); - builder.websocketApplication(applicationClass); - return builder.build(); - } - /** * Create a server instance for discovered JAX-RS application (through CDI). * @@ -159,7 +143,6 @@ final class Builder { private Supplier defaultExecutorService; private JaxRsCdiExtension jaxRs; private boolean retainDiscovered = false; - private Class wsApplication; private Builder() { if (!IN_PROGRESS_OR_RUNNING.compareAndSet(false, true)) { @@ -383,20 +366,6 @@ public Builder addApplication(Application application) { return this; } - /** - * Registers a WebSocket application in the server. At most one application can be registered. - * - * @param wsApplication websocket application - * @return modified builder - */ - public Builder websocketApplication(Class wsApplication) { - if (this.wsApplication != null) { - throw new IllegalStateException("Cannot register more than one websocket application"); - } - this.wsApplication = wsApplication; - return this; - } - /** * If any application or resource class is added through this builder, applications discovered by CDI are ignored. * You can change this behavior by setting the retain discovered applications to {@code true}. @@ -522,9 +491,5 @@ String host() { int port() { return port; } - - Optional> websocketApplication() { - return Optional.ofNullable(wsApplication); - } } } diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java index f731f7e291b..040b416c7b2 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java @@ -20,7 +20,6 @@ import java.net.UnknownHostException; import java.util.logging.Logger; -import javax.enterprise.event.Event; import javax.enterprise.inject.se.SeContainer; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; @@ -69,12 +68,6 @@ public class ServerImpl implements Server { serverExtension.listenHost(this.host); - // Inform WebSocket extension of an app specified in builder - builder.websocketApplication().ifPresent(app -> { - Event event = container.getBeanManager().getEvent(); - event.fire(new ServerApplicationConfigEvent(app)); - }); - STARTUP_LOGGER.finest("Builders ready"); STARTUP_LOGGER.finest("Static classpath"); diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java new file mode 100644 index 00000000000..6c2440f4f7f --- /dev/null +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import java.util.concurrent.ExecutorService; + +import io.helidon.common.configurable.ThreadPoolSupplier; +import io.helidon.webserver.tyrus.TyrusSupport; + +/** + * Class TyrusSupportMp. + */ +class TyrusSupportMp extends TyrusSupport { + + private final ThreadPoolSupplier threadPoolSupplier; + + TyrusSupportMp(TyrusSupport other) { + super(other); + threadPoolSupplier = ThreadPoolSupplier.builder() + .threadNamePrefix("helidon-websocket-") + .build(); + } + + /** + * Returns executor service for Websocket in MP. + * + * @return Executor service or {@code null}. + */ + @Override + protected ExecutorService executorService() { + return threadPoolSupplier.get(); + } +} diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java index 0f48d46974c..89b26b363a0 100644 --- a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java +++ b/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java @@ -41,7 +41,6 @@ import io.helidon.microprofile.cdi.RuntimeStart; import io.helidon.microprofile.server.RoutingName; import io.helidon.microprofile.server.RoutingPath; -import io.helidon.microprofile.server.ServerApplicationConfigEvent; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; @@ -126,23 +125,6 @@ WebSocketApplication toWebSocketApplication() { return appBuilder.build(); } - /** - * Event fired off by server to inform this extension that an application class - * has been set in the server's builder and that any application class scanned - * by this extension should be ignored. - * - * @param event Event containing new application class. - */ - @SuppressWarnings("unchecked") - private void overrideApplication(@Observes ServerApplicationConfigEvent event) { - Class applicationClass = event.applicationClass(); - if (!ServerApplicationConfig.class.isAssignableFrom(applicationClass)) { - throw new IllegalArgumentException("Websocket application class " + applicationClass - + " must implement " + ServerApplicationConfig.class); - } - appBuilder.updateApplicationClass((Class) event.applicationClass()); - } - private void registerWebSockets(BeanManager beanManager, ServerConfiguration serverConfig) { try { WebSocketApplication app = toWebSocketApplication(); @@ -198,7 +180,7 @@ private void registerWebSockets(BeanManager beanManager, ServerConfiguration ser // Finally register WebSockets in Helidon routing String rootPath = contextRoot.orElse(DEFAULT_WEBSOCKET_PATH); LOGGER.info("Registering websocket application at " + rootPath); - routing.register(rootPath, builder.build()); + routing.register(rootPath, new TyrusSupportMp(builder.build())); } catch (IllegalArgumentException e) { throw new RuntimeException("Unable to load WebSocket extension", e); } diff --git a/microprofile/tyrus/src/main/java/module-info.java b/microprofile/tyrus/src/main/java/module-info.java index 97890fe86ee..80704119d30 100644 --- a/microprofile/tyrus/src/main/java/module-info.java +++ b/microprofile/tyrus/src/main/java/module-info.java @@ -31,6 +31,7 @@ requires tyrus.core; requires io.helidon.microprofile.server; requires io.helidon.webserver.tyrus; + requires tyrus.spi; exports io.helidon.microprofile.tyrus; provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.tyrus.WebSocketCdiExtension; diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java index 82bc7c47c07..b3317a288db 100644 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java +++ b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java @@ -16,6 +16,7 @@ package io.helidon.microprofile.tyrus; +import javax.enterprise.context.Dependent; import javax.websocket.CloseReason; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; @@ -51,7 +52,6 @@ public class WebSocketAppTest { @BeforeAll static void initClass() { Server.Builder builder = Server.builder(); - builder.websocketApplication(EndpointApplication.class); server = builder.build(); server.start(); } @@ -75,6 +75,7 @@ public void testEchoProg() throws Exception { echoClient.echo("hi", "how are you?"); } + @Dependent @RoutingPath("/web") public static class EndpointApplication implements ServerApplicationConfig { @Override @@ -97,22 +98,26 @@ public static class EchoEndpointAnnot { @OnOpen public void onOpen(Session session) throws IOException { LOGGER.info("OnOpen called"); + verifyRunningThread(session, LOGGER); } @OnMessage public void echo(Session session, String message) throws Exception { LOGGER.info("OnMessage called '" + message + "'"); session.getBasicRemote().sendObject(message); + verifyRunningThread(session, LOGGER); } @OnError - public void onError(Throwable t) { + public void onError(Throwable t, Session session) throws IOException { LOGGER.info("OnError called"); + verifyRunningThread(session, LOGGER); } @OnClose - public void onClose(Session session) { + public void onClose(Session session) throws IOException { LOGGER.info("OnClose called"); + verifyRunningThread(session, LOGGER); } } @@ -147,4 +152,19 @@ public void onClose(Session session, CloseReason closeReason) { super.onClose(session, closeReason); } } + + /** + * Verify that endpoint methods are running in a Helidon thread pool. + * + * @param session Websocket session. + * @param logger A logger. + * @throws IOException Exception during close. + */ + private static void verifyRunningThread(Session session, Logger logger) throws IOException { + Thread thread = Thread.currentThread(); + if (!thread.getName().contains("helidon")) { + logger.warning("Websocket handler running in incorrect thread " + thread); + session.close(); + } + } } diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java deleted file mode 100644 index 63c1ca78870..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketBadAppTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import io.helidon.microprofile.server.Server; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * Class WebSocketBadAppTest. - */ -public class WebSocketBadAppTest { - - @BeforeAll - static void initClass() { - Server.Builder builder = Server.builder(); - builder.websocketApplication(BadEndpointApplication.class); - assertThrows(IllegalArgumentException.class, builder::build); - } - - @Test - public void test() { - // no-op - } - - public static class BadEndpointApplication /* implements ServerApplicationConfig */ { - } -} diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java b/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java deleted file mode 100644 index 760d2fd0158..00000000000 --- a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.tyrus; - -import javax.websocket.OnClose; -import javax.websocket.OnError; -import javax.websocket.OnMessage; -import javax.websocket.OnOpen; -import javax.websocket.Session; -import javax.websocket.server.ServerEndpoint; - -import java.io.IOException; -import java.net.URI; -import java.util.logging.Logger; - -import io.helidon.microprofile.server.Server; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -/** - * Class EchoEndpointTest. - */ -public class WebSocketTest { - - private static Server server; - - @BeforeAll - static void initClass() { - server = Server.create(); - server.start(); - } - - @AfterAll - static void destroyClass() { - server.stop(); - } - - @Test - public void testEcho() throws Exception { - URI echoUri = URI.create("ws://localhost:" + server.port() + "/websocket/echo"); - EchoClient echoClient = new EchoClient(echoUri); - echoClient.echo("hi", "how are you?"); - } - - /** - * Verify that endpoint methods are running in a Helidon thread pool. - * - * @param session Websocket session. - * @param logger A logger. - * @throws IOException Exception during close. - */ - private static void verifyRunningThread(Session session, Logger logger) throws IOException { - Thread thread = Thread.currentThread(); - if (!thread.getName().contains("helidon")) { - logger.warning("Websocket handler running in incorrect thread " + thread); - session.close(); - } - } - - @ServerEndpoint("/echo") - public static class EchoEndpoint { - private static final Logger LOGGER = Logger.getLogger(EchoEndpoint.class.getName()); - - @OnOpen - public void onOpen(Session session) throws IOException { - LOGGER.info("OnOpen called"); - verifyRunningThread(session, LOGGER); - } - - @OnMessage - public void echo(Session session, String message) throws Exception { - LOGGER.info("Endpoint OnMessage called '" + message + "'"); - verifyRunningThread(session, LOGGER); - session.getBasicRemote().sendObject(message); - } - - @OnError - public void onError(Throwable t) throws IOException { - LOGGER.info("OnError called"); - } - - @OnClose - public void onClose(Session session) throws IOException { - LOGGER.info("OnClose called"); - verifyRunningThread(session, LOGGER); - } - } -} diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index 2adba44fe3c..9c07c9b82a7 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -24,25 +24,18 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import javax.websocket.DeploymentException; import javax.websocket.server.HandshakeRequest; import javax.websocket.server.ServerEndpointConfig; -import io.helidon.common.HelidonFeatures; -import io.helidon.common.HelidonFlavor; -import io.helidon.common.configurable.ServerThreadPoolSupplier; -import io.helidon.common.context.Contexts; -import io.helidon.config.Config; import io.helidon.webserver.Handler; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; -import org.eclipse.microprofile.config.ConfigProvider; import org.glassfish.tyrus.core.RequestContext; import org.glassfish.tyrus.core.TyrusUpgradeResponse; import org.glassfish.tyrus.core.TyrusWebSocketEngine; @@ -63,22 +56,26 @@ public class TyrusSupport implements Service { */ private static final ByteBuffer FLUSH_BUFFER = ByteBuffer.allocateDirect(0); - private static final AtomicReference DEFAULT_THREAD_POOL = new AtomicReference<>(); - private final WebSocketEngine engine; private final TyrusHandler handler = new TyrusHandler(); private Set> endpointClasses; private Set endpointConfigs; - private ExecutorService executorService; + + /** + * Create from another instance. + * + * @param other The other instance. + */ + protected TyrusSupport(TyrusSupport other) { + this.engine = other.engine; + this.endpointClasses = other.endpointClasses; + this.endpointConfigs = other.endpointConfigs; + } TyrusSupport(WebSocketEngine engine, Set> endpointClasses, Set endpointConfigs) { this.engine = engine; this.endpointClasses = endpointClasses; this.endpointConfigs = endpointConfigs; - this.executorService = createExecutorService(); - if (this.executorService != null) { - this.executorService = Contexts.wrap(this.executorService); - } } /** @@ -111,6 +108,15 @@ public Set endpointConfigs() { return Collections.unmodifiableSet(endpointConfigs); } + /** + * Returns executor service, can be overridden. + * + * @return Executor service or {@code null}. + */ + protected ExecutorService executorService() { + return null; + } + /** * Creates a builder for this class. * @@ -123,7 +129,7 @@ public static Builder builder() { /** * Builder for convenient way to create {@link TyrusSupport}. */ - public static final class Builder implements io.helidon.common.Builder { + public static class Builder implements io.helidon.common.Builder { private Set> endpointClasses = new HashSet<>(); private Set endpointConfigs = new HashSet<>(); @@ -200,25 +206,6 @@ public WebSocketEngine getWebSocketEngine() { } } - /** - * Creates executor service for Websocket in MP. No executor for SE. - * - * @return Executor service or {@code null}. - */ - private static ExecutorService createExecutorService() { - if (HelidonFeatures.flavor() == HelidonFlavor.MP && DEFAULT_THREAD_POOL.get() == null) { - Config executorConfig = ((Config) ConfigProvider.getConfig()) - .get("websocket.executor-service"); - - DEFAULT_THREAD_POOL.set(ServerThreadPoolSupplier.builder() - .name("websocket") - .config(executorConfig) - .build() - .get()); - } - return DEFAULT_THREAD_POOL.get(); - } - /** * A Helidon handler that integrates with Tyrus and can process WebSocket * upgrade requests. @@ -270,6 +257,7 @@ public void accept(ServerRequest req, ServerResponse res) { publisherWriter.write(FLUSH_BUFFER, null); // Setup the WebSocket connection and subscriber, calls @onOpen + ExecutorService executorService = executorService(); if (executorService != null) { CompletableFuture future = CompletableFuture.supplyAsync( From 8f6051e8ed26f8c487fbb6c0d4dd0c2467b16d4e Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 6 Feb 2020 13:32:44 -0500 Subject: [PATCH 32/35] Fixed spotbugs error. --- .../io/helidon/webserver/tyrus/TyrusReaderSubscriber.java | 4 ++++ .../main/java/io/helidon/webserver/tyrus/TyrusSupport.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java index 30523bc5684..0b6ce8dc652 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusReaderSubscriber.java @@ -43,6 +43,10 @@ public class TyrusReaderSubscriber implements Flow.Subscriber { private final ExecutorService executorService; private Flow.Subscription subscription; + TyrusReaderSubscriber(Connection connection) { + this(connection, null); + } + TyrusReaderSubscriber(Connection connection, ExecutorService executorService) { if (connection == null) { throw new IllegalArgumentException("Connection cannot be null"); diff --git a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java index 9c07c9b82a7..b85bdd318fb 100644 --- a/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java +++ b/webserver/tyrus/src/main/java/io/helidon/webserver/tyrus/TyrusSupport.java @@ -272,7 +272,7 @@ public void accept(ServerRequest req, ServerResponse res) { Connection connection = upgradeInfo.createConnection(publisherWriter, closeReason -> LOGGER.fine(() -> "Connection closed: " + closeReason)); if (connection != null) { - TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection, executorService); + TyrusReaderSubscriber subscriber = new TyrusReaderSubscriber(connection); req.content().subscribe(subscriber); } } From 3d874d67744cdaa957d2cd02dd4171b610ec0c75 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 7 Feb 2020 08:20:40 -0500 Subject: [PATCH 33/35] Removed unused method and updated docs. --- .../java/io/helidon/common/HelidonFeatures.java | 9 --------- docs/src/main/docs/websocket/01_overview.adoc | 15 +++------------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/common/common/src/main/java/io/helidon/common/HelidonFeatures.java b/common/common/src/main/java/io/helidon/common/HelidonFeatures.java index ec9066f2d08..eecb4f60249 100644 --- a/common/common/src/main/java/io/helidon/common/HelidonFeatures.java +++ b/common/common/src/main/java/io/helidon/common/HelidonFeatures.java @@ -166,15 +166,6 @@ public static void flavor(HelidonFlavor flavor) { CURRENT_FLAVOR.compareAndSet(null, flavor); } - /** - * Get the currently set Helidon flavor. - * - * @return current flavor - */ - public static HelidonFlavor flavor() { - return CURRENT_FLAVOR.get(); - } - static final class Node { private final Map children = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); private final String name; diff --git a/docs/src/main/docs/websocket/01_overview.adoc b/docs/src/main/docs/websocket/01_overview.adoc index 01a23747e7f..09d2e27ed1a 100644 --- a/docs/src/main/docs/websocket/01_overview.adoc +++ b/docs/src/main/docs/websocket/01_overview.adoc @@ -36,9 +36,8 @@ we dive into some examples in the next few sections. Helidon has support for WebSockets both in SE and in MP. Helidon SE support is based on the `TyrusSupport` class which is akin to `JerseySupport`. -Helidon MP support is focused on bean discovery using CDI and extensive use -of annotations, yet it is still possible to configure applications -programmatically with bean discovery disabled if so desired. +Helidon MP support is centered around annotations and bean discovery using +CDI. As stated above, the Jakarta WebSocket API supports both annotated and programmatic endpoints. Even though most Helidon MP applications rely @@ -246,18 +245,11 @@ public class MessageBoardApplication implements ServerApplicationConfig { the root path for WebSocket endpoints will be `"/web"` instead of the default `"/websocket"`. Note that `@RoutingPath` is _not_ a bean defining annotation, thus the use of `@ApplicationScoped` --which, as before, requires CDI -bean discovery mode to be annotated. In addition to `@RoutingPath`, these +bean discovery mode to be `annotated`. In addition to `@RoutingPath`, these classes can be annotated with `@RoutingName` to associate an endpoint with a Helidon named socket. Please refer to the Javadoc for that annotation for additional information. -Helidon MP provides developers the option to control the application's `main`, -including the server bootstrap steps. For this reason, the builder for -`io.helidon.microprofile.server.Server` also accepts a class of type -`Class` as a way to configure a -server manually. Using this builder would be required for those applications -for which CDI discovery is disabled. - Unlike Helidon SE, all endpoint methods in Helidon MP are executed in a separate thread pool, independently of Netty. Therefore, there is no need to create additional threads for blocking or long-running operations @@ -266,7 +258,6 @@ as these will not affect Netty's ability to process networking data. For more information on the MP version of this example, the reader is referred to [4]. - - [1] https://projects.eclipse.org/projects/ee4j.tyrus - [2] https://projects.eclipse.org/projects/ee4j.websocket - [3] https://github.com/oracle/helidon/tree/websockets20/examples/webserver/websocket From 9515f52ec7e283728774e0c7be692fb9195734f0 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 7 Feb 2020 08:28:25 -0500 Subject: [PATCH 34/35] Removed unused class. --- .../server/ServerApplicationConfigEvent.java | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java deleted file mode 100644 index d2f9cc0d094..00000000000 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerApplicationConfigEvent.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.microprofile.server; - -/** - * Class ServerApplicationConfigEvent. This wrapper class is used by the server - * to inform the {@code WebSocketCdiExtension} that an application class has been - * set in its builder and that any scanned classes should be ignored. - */ -public class ServerApplicationConfigEvent { - - /** - * Application class of type {@code Class}, - * we use {@code Class} to avoid a static dependency with the WebSocket API. - */ - private final Class applicationClass; - - /** - * Construct an event given a websocket application class. - * - * @param applicationClass Application class. - */ - public ServerApplicationConfigEvent(Class applicationClass) { - this.applicationClass = applicationClass; - } - - /** - * Get access to application class. - * - * @return Application class. - */ - public Class applicationClass() { - return applicationClass; - } -} From 1456226e7215eef56cf1926b2f0208ef74625472 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 13 Feb 2020 09:33:09 -0500 Subject: [PATCH 35/35] Renamed module from tyrus to websocket. Restored transitive dependencies in MP server. Signed-off-by: Santiago Pericas-Geertsen --- bom/pom.xml | 4 ++-- examples/microprofile/websocket/pom.xml | 4 ++-- microprofile/pom.xml | 2 +- microprofile/server/src/main/java/module-info.java | 4 ++-- microprofile/{tyrus => websocket}/pom.xml | 6 +++--- .../microprofile/tyrus/HelidonComponentProvider.java | 0 .../java/io/helidon/microprofile/tyrus/TyrusSupportMp.java | 0 .../io/helidon/microprofile/tyrus/WebSocketApplication.java | 0 .../helidon/microprofile/tyrus/WebSocketCdiExtension.java | 0 .../java/io/helidon/microprofile/tyrus/package-info.java | 0 .../{tyrus => websocket}/src/main/java/module-info.java | 0 .../META-INF/services/javax.enterprise.inject.spi.Extension | 0 .../services/org.glassfish.tyrus.core.ComponentProvider | 0 .../test/java/io/helidon/microprofile/tyrus/EchoClient.java | 0 .../io/helidon/microprofile/tyrus/WebSocketAppTest.java | 0 .../src/test/resources/META-INF/beans.xml | 0 .../src/test/resources/logging.properties | 0 17 files changed, 10 insertions(+), 10 deletions(-) rename microprofile/{tyrus => websocket}/pom.xml (94%) rename microprofile/{tyrus => websocket}/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java (100%) rename microprofile/{tyrus => websocket}/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java (100%) rename microprofile/{tyrus => websocket}/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java (100%) rename microprofile/{tyrus => websocket}/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java (100%) rename microprofile/{tyrus => websocket}/src/main/java/io/helidon/microprofile/tyrus/package-info.java (100%) rename microprofile/{tyrus => websocket}/src/main/java/module-info.java (100%) rename microprofile/{tyrus => websocket}/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension (100%) rename microprofile/{tyrus => websocket}/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider (100%) rename microprofile/{tyrus => websocket}/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java (100%) rename microprofile/{tyrus => websocket}/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java (100%) rename microprofile/{tyrus => websocket}/src/test/resources/META-INF/beans.xml (100%) rename microprofile/{tyrus => websocket}/src/test/resources/logging.properties (100%) diff --git a/bom/pom.xml b/bom/pom.xml index b512ca3d3b5..7464964eec0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -427,8 +427,8 @@ ${helidon.version} - io.helidon.microprofile.tyrus - helidon-microprofile-tyrus + io.helidon.microprofile.websocket + helidon-microprofile-websocket ${helidon.version} diff --git a/examples/microprofile/websocket/pom.xml b/examples/microprofile/websocket/pom.xml index 0754e4828e0..8bf52286839 100644 --- a/examples/microprofile/websocket/pom.xml +++ b/examples/microprofile/websocket/pom.xml @@ -41,8 +41,8 @@ helidon-microprofile - io.helidon.microprofile.tyrus - helidon-microprofile-tyrus + io.helidon.microprofile.websocket + helidon-microprofile-websocket io.helidon.microprofile.bundles diff --git a/microprofile/pom.xml b/microprofile/pom.xml index abba9f7a396..0ae42b555e3 100644 --- a/microprofile/pom.xml +++ b/microprofile/pom.xml @@ -49,6 +49,6 @@ grpc cdi weld - tyrus + websocket diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index d2d4c8494df..b0241663e3b 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -26,8 +26,8 @@ requires transitive io.helidon.microprofile.cdi; - requires cdi.api; - requires java.ws.rs; + requires transitive cdi.api; + requires transitive java.ws.rs; requires javax.interceptor.api; requires java.logging; diff --git a/microprofile/tyrus/pom.xml b/microprofile/websocket/pom.xml similarity index 94% rename from microprofile/tyrus/pom.xml rename to microprofile/websocket/pom.xml index 5a716979a57..56da72e33f9 100644 --- a/microprofile/tyrus/pom.xml +++ b/microprofile/websocket/pom.xml @@ -25,9 +25,9 @@ 2.0-SNAPSHOT - io.helidon.microprofile.tyrus - helidon-microprofile-tyrus - Helidon MicroProfile Tyrus + io.helidon.microprofile.websocket + helidon-microprofile-websocket + Helidon MicroProfile WebSocket Helidon MP integration with Tyrus diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java similarity index 100% rename from microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java rename to microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java similarity index 100% rename from microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java rename to microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusSupportMp.java diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java similarity index 100% rename from microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java rename to microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/WebSocketApplication.java diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java similarity index 100% rename from microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java rename to microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/WebSocketCdiExtension.java diff --git a/microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/package-info.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/package-info.java similarity index 100% rename from microprofile/tyrus/src/main/java/io/helidon/microprofile/tyrus/package-info.java rename to microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/package-info.java diff --git a/microprofile/tyrus/src/main/java/module-info.java b/microprofile/websocket/src/main/java/module-info.java similarity index 100% rename from microprofile/tyrus/src/main/java/module-info.java rename to microprofile/websocket/src/main/java/module-info.java diff --git a/microprofile/tyrus/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/microprofile/websocket/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension similarity index 100% rename from microprofile/tyrus/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension rename to microprofile/websocket/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension diff --git a/microprofile/tyrus/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider b/microprofile/websocket/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider similarity index 100% rename from microprofile/tyrus/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider rename to microprofile/websocket/src/main/resources/META-INF/services/org.glassfish.tyrus.core.ComponentProvider diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java b/microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java similarity index 100% rename from microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java rename to microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/EchoClient.java diff --git a/microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java b/microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java similarity index 100% rename from microprofile/tyrus/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java rename to microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/WebSocketAppTest.java diff --git a/microprofile/tyrus/src/test/resources/META-INF/beans.xml b/microprofile/websocket/src/test/resources/META-INF/beans.xml similarity index 100% rename from microprofile/tyrus/src/test/resources/META-INF/beans.xml rename to microprofile/websocket/src/test/resources/META-INF/beans.xml diff --git a/microprofile/tyrus/src/test/resources/logging.properties b/microprofile/websocket/src/test/resources/logging.properties similarity index 100% rename from microprofile/tyrus/src/test/resources/logging.properties rename to microprofile/websocket/src/test/resources/logging.properties