Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x: Added support for WS endpoints in application scope #7340

Merged
merged 1 commit into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2022 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));
}
}
}