Skip to content

Commit 7dff709

Browse files
authored
Added support for WS endpoints in application scope. New test. See issue helidon-io#7234. (helidon-io#7245) (helidon-io#7340)
Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
1 parent 23b7867 commit 7dff709

2 files changed

Lines changed: 176 additions & 4 deletions

File tree

microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/HelidonComponentProvider.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2022 Oracle and/or its affiliates.
2+
* Copyright (c) 2020, 2023 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,31 +16,59 @@
1616

1717
package io.helidon.microprofile.tyrus;
1818

19+
import jakarta.enterprise.context.ApplicationScoped;
1920
import jakarta.enterprise.inject.spi.BeanManager;
2021
import jakarta.enterprise.inject.spi.CDI;
2122
import org.glassfish.tyrus.core.ComponentProvider;
2223

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

34+
/**
35+
* Checks if a bean is known to CDI.
36+
*
37+
* @param c {@link Class} to be checked
38+
* @return outcome of test
39+
*/
2940
@Override
3041
public boolean isApplicable(Class<?> c) {
3142
BeanManager beanManager = CDI.current().getBeanManager();
3243
return beanManager.getBeans(c).size() > 0;
3344
}
3445

46+
/**
47+
* Create a new instance using CDI. Note that if the bean is {@link ApplicationScoped}
48+
* the same instance will be returned every time this method is called.
49+
*
50+
* @param c {@link Class} to be created
51+
* @return new instance
52+
* @param <T> type of new instance
53+
*/
3554
@Override
3655
public <T> Object create(Class<T> c) {
3756
return CDI.current().select(c).get();
3857
}
3958

59+
/**
60+
* Beans are normally scoped to a client connection. However, if a bean is explicitly
61+
* set to be in {@link ApplicationScoped}, it will not be destroyed here.
62+
*
63+
* @param o instance to be destroyed
64+
* @return outcome of operation
65+
*/
4066
@Override
4167
public boolean destroy(Object o) {
4268
try {
43-
CDI.current().destroy(o);
69+
if (!o.getClass().isAnnotationPresent(ApplicationScoped.class)) {
70+
CDI.current().destroy(o);
71+
}
4472
} catch (UnsupportedOperationException | IllegalStateException e) {
4573
return false;
4674
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.microprofile.tyrus;
18+
19+
import java.net.URI;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.concurrent.Semaphore;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.concurrent.atomic.AtomicBoolean;
27+
28+
import io.helidon.microprofile.tests.junit5.AddBean;
29+
import io.helidon.microprofile.tests.junit5.HelidonTest;
30+
import jakarta.annotation.PostConstruct;
31+
import jakarta.annotation.PreDestroy;
32+
import jakarta.enterprise.context.ApplicationScoped;
33+
import jakarta.inject.Inject;
34+
import jakarta.websocket.Endpoint;
35+
import jakarta.websocket.EndpointConfig;
36+
import jakarta.websocket.OnMessage;
37+
import jakarta.websocket.OnOpen;
38+
import jakarta.websocket.Session;
39+
import jakarta.websocket.server.PathParam;
40+
import jakarta.websocket.server.ServerEndpoint;
41+
import jakarta.ws.rs.client.WebTarget;
42+
import org.glassfish.tyrus.client.ClientManager;
43+
import org.glassfish.tyrus.container.jdk.client.JdkClientContainer;
44+
import org.junit.jupiter.api.Test;
45+
46+
import static org.hamcrest.CoreMatchers.is;
47+
import static org.hamcrest.MatcherAssert.assertThat;
48+
49+
@HelidonTest
50+
@AddBean(ApplicationScopeTest.WebsocketEndpoint.class)
51+
class ApplicationScopeTest {
52+
53+
static Semaphore semaphore = new Semaphore(0);
54+
55+
@Inject
56+
protected WebsocketEndpoint endpoint;
57+
58+
@Inject
59+
protected WebTarget webTarget;
60+
61+
@Test
62+
void test() throws Exception {
63+
// two message sent over different sessions
64+
Session session_1 = connectToWebsocket("websocket/id-1");
65+
session_1.getBasicRemote().sendText("A message 1");
66+
Session session_2 = connectToWebsocket("websocket/id-2");
67+
session_2.getBasicRemote().sendText("A message 2");
68+
69+
// wait until first two messages received
70+
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS), is(true));
71+
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS), is(true));
72+
73+
// verify application scoped bean persists session closing
74+
assertThat(endpoint.messageMap().size(), is(2));
75+
session_2.close();
76+
assertThat(endpoint.messageMap().size(), is(2));
77+
78+
// and that we can push more messages to map if needed
79+
Session session_3 = connectToWebsocket("websocket/id-3");
80+
session_3.getBasicRemote().sendText("A message 3");
81+
82+
// wait until last message received
83+
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS), is(true));
84+
85+
// verify application scoped bean
86+
assertThat(endpoint.messageMap().size(), is(3));
87+
88+
// close other sessions
89+
session_1.close();
90+
session_3.close();
91+
}
92+
93+
public Session connectToWebsocket(String path) {
94+
Endpoint endpoint = new Endpoint() {
95+
@Override
96+
public void onOpen(Session session, EndpointConfig config) {
97+
}
98+
};
99+
100+
try {
101+
ClientManager clientManager = ClientManager.createClient(JdkClientContainer.class.getName());
102+
return clientManager.connectToServer(endpoint,
103+
new URI("ws://localhost:" + webTarget.getUri().getPort() + "/" + path));
104+
} catch (Exception ex) {
105+
throw new RuntimeException(ex);
106+
}
107+
}
108+
109+
@ApplicationScoped
110+
@ServerEndpoint("/websocket/{id}")
111+
public static class WebsocketEndpoint {
112+
113+
private final Map<String, List<String>> messageMap = new ConcurrentHashMap<>();
114+
115+
@OnOpen
116+
public void onOpen(@PathParam("id") String id, Session session) {
117+
messageMap.put(id, new ArrayList<>());
118+
}
119+
120+
@OnMessage
121+
public void onMessage(@PathParam("id") String id, Session session, String message) {
122+
messageMap.get(id).add(message);
123+
semaphore.release();
124+
}
125+
126+
public Map<String, List<String>> messageMap() {
127+
return messageMap;
128+
}
129+
130+
// Verify single instance is created/destroyed by CDI
131+
132+
private static final AtomicBoolean live = new AtomicBoolean();
133+
134+
@PostConstruct
135+
void construct() {
136+
assertThat(live.compareAndSet(false, true), is(true));
137+
}
138+
139+
@PreDestroy
140+
void destroy() {
141+
assertThat(live.compareAndSet(true, false), is(true));
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)