Skip to content

Commit

Permalink
Added support for WS endpoints in application scope. New test. See is…
Browse files Browse the repository at this point in the history
…sue helidon-io#7234.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
  • Loading branch information
spericas committed Jul 25, 2023
1 parent 0696554 commit 54af480
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2021 Oracle and/or its affiliates.
* Copyright (c) 2020, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,31 +16,59 @@

package io.helidon.microprofile.tyrus;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
import org.glassfish.tyrus.core.ComponentProvider;

/**
* Class HelidonComponentProvider. A service provider for Tyrus to create and destroy
* beans using CDI.
* A service provider for Tyrus to create and destroy beans using CDI. By default,
* and according to the Jakarta WebSocket specification, beans are created and
* destroyed for each client connection (in "connection scope"). However, this provider
* also supports endpoints in {@link ApplicationScoped}. These endpoint instances
* are not destroyed here but at a later time by the CDI container. No other scopes
* are currently supported.
*/
public class HelidonComponentProvider extends ComponentProvider {

/**
* Checks if a bean is known to CDI.
*
* @param c {@link Class} to be checked
* @return outcome of test
*/
@Override
public boolean isApplicable(Class<?> c) {
BeanManager beanManager = CDI.current().getBeanManager();
return beanManager.getBeans(c).size() > 0;
}

/**
* Create a new instance using CDI. Note that if the bean is {@link ApplicationScoped}
* the same instance will be returned every time this method is called.
*
* @param c {@link Class} to be created
* @return new instance
* @param <T> type of new instance
*/
@Override
public <T> Object create(Class<T> c) {
return CDI.current().select(c).get();
}

/**
* Beans are normally scoped to a client connection. However, if a bean is explicitly
* set to be in {@link ApplicationScoped}, it will not be destroyed here.
*
* @param o instance to be destroyed
* @return outcome of operation
*/
@Override
public boolean destroy(Object o) {
try {
CDI.current().destroy(o);
if (!o.getClass().isAnnotationPresent(ApplicationScoped.class)) {
CDI.current().destroy(o);
}
} catch (UnsupportedOperationException | IllegalStateException e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* 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.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import io.helidon.microprofile.tests.junit5.AddBean;
import io.helidon.microprofile.tests.junit5.HelidonTest;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.ws.rs.client.WebTarget;
import org.glassfish.tyrus.client.ClientManager;
import org.glassfish.tyrus.container.jdk.client.JdkClientContainer;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@HelidonTest
@AddBean(ApplicationScopeTest.WebsocketEndpoint.class)
class ApplicationScopeTest {

static Semaphore semaphore = new Semaphore(0);

@Inject
protected WebsocketEndpoint endpoint;

@Inject
protected WebTarget webTarget;

@Test
void test() throws Exception {
// two message sent over different sessions
Session session_1 = connectToWebsocket("websocket/id-1");
session_1.getBasicRemote().sendText("A message 1");
Session session_2 = connectToWebsocket("websocket/id-2");
session_2.getBasicRemote().sendText("A message 2");

// wait until first two messages received
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS), is(true));
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS), is(true));

// verify application scoped bean persists session closing
assertThat(endpoint.messageMap().size(), is(2));
session_2.close();
assertThat(endpoint.messageMap().size(), is(2));

// and that we can push more messages to map if needed
Session session_3 = connectToWebsocket("websocket/id-3");
session_3.getBasicRemote().sendText("A message 3");

// wait until last message received
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS), is(true));

// verify application scoped bean
assertThat(endpoint.messageMap().size(), is(3));

// close other sessions
session_1.close();
session_3.close();
}

public Session connectToWebsocket(String path) {
Endpoint endpoint = new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
}
};

try {
ClientManager clientManager = ClientManager.createClient(JdkClientContainer.class.getName());
return clientManager.connectToServer(endpoint,
new URI("ws://localhost:" + webTarget.getUri().getPort() + "/" + path));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

@ApplicationScoped
@ServerEndpoint("/websocket/{id}")
public static class WebsocketEndpoint {

private final Map<String, List<String>> messageMap = new ConcurrentHashMap<>();

@OnOpen
public void onOpen(@PathParam("id") String id, Session session) {
messageMap.put(id, new ArrayList<>());
}

@OnMessage
public void onMessage(@PathParam("id") String id, Session session, String message) {
messageMap.get(id).add(message);
semaphore.release();
}

public Map<String, List<String>> messageMap() {
return messageMap;
}

// Verify single instance is created/destroyed by CDI

private static final AtomicBoolean live = new AtomicBoolean();

@PostConstruct
void construct() {
assertThat(live.compareAndSet(false, true), is(true));
}

@PreDestroy
void destroy() {
assertThat(live.compareAndSet(true, false), is(true));
}
}
}

0 comments on commit 54af480

Please sign in to comment.