diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java index 308d4f2c13..c29ebf886f 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java @@ -67,6 +67,11 @@ public class OlogHttpClient implements LogClient { private static final String OLOG_CLIENT_INFO_HEADER = "X-Olog-Client-Info"; private static final String CLIENT_INFO = "CS Studio " + Messages.AppVersion + " on " + System.getProperty("os.name"); + /** + * Each endpoint needs this as by default the service's context path is /, but all + * API endpoints are prefixed with Olog. + */ + public static final String OLOG_PREFIX = "/Olog"; private static final Logger LOGGER = Logger.getLogger(OlogHttpClient.class.getName()); private final List changeHandlers = new ArrayList<>(); @@ -182,7 +187,7 @@ private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException } HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/logs/multipart?" + QueryParamsHelper.mapToQueryParams(queryParams))) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/multipart?" + QueryParamsHelper.mapToQueryParams(queryParams))) .header("Content-Type", httpRequestMultipartBody.getContentType()) .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) .header("Authorization", basicAuthenticationHeader) @@ -225,7 +230,7 @@ public LogEntry getLog(Long logId) { @Override public LogEntry findLogById(Long logId) { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/logs/" + logId)) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/" + logId)) .header("Content-Type", CONTENT_TYPE_JSON) .GET() .build(); @@ -252,7 +257,7 @@ public List findLogs(Map map) { */ private SearchResult findLogs(MultivaluedMap searchParams) throws RuntimeException { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/search?" + QueryParamsHelper.mapToQueryParams(searchParams))) .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) .header("Content-Type", CONTENT_TYPE_JSON) @@ -311,7 +316,7 @@ public LogEntry update(LogEntry logEntry) { try { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/logs/" + logEntry.getId() + "?markup=commonmark")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/" + logEntry.getId() + "?markup=commonmark")) .header("Content-Type", CONTENT_TYPE_JSON) .header("Authorization", basicAuthenticationHeader) .POST(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntry))) @@ -345,7 +350,7 @@ public SearchResult search(Map map) { public void groupLogEntries(List logEntryIds) throws LogbookException { try { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/logs/group")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/group")) .header("Content-Type", CONTENT_TYPE_JSON) .header("Authorization", basicAuthenticationHeader) .POST(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntryIds))) @@ -371,7 +376,7 @@ public void groupLogEntries(List logEntryIds) throws LogbookException { * @return An {@link AuthenticationStatus} to indicate the outcome of the login attempt. */ public AuthenticationStatus authenticate(String userName, String password) { - String stringBuilder = Preferences.olog_url + + String stringBuilder = Preferences.olog_url + OLOG_PREFIX + "/login"; try { HttpRequest request = HttpRequest.newBuilder() @@ -400,7 +405,7 @@ public AuthenticationStatus authenticate(String userName, String password) { @Override public String serviceInfo() { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url)) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX)) .header("Content-Type", CONTENT_TYPE_JSON) .GET() .build(); @@ -428,7 +433,7 @@ public Collection listLogs() { @Override public Collection listLogbooks() { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/logbooks")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logbooks")) .header("Content-Type", CONTENT_TYPE_JSON) .GET() .build(); @@ -445,7 +450,7 @@ public Collection listLogbooks() { @Override public Collection listTags() { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/tags")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/tags")) .header("Content-Type", CONTENT_TYPE_JSON) .GET() .build(); @@ -462,7 +467,7 @@ public Collection listTags() { @Override public Collection listProperties() { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/properties")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/properties")) .header("Content-Type", CONTENT_TYPE_JSON) .GET() .build(); @@ -481,7 +486,7 @@ public Collection listProperties() { public InputStream getAttachment(Long logId, String attachmentName) { try { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/logs/attachments/" + logId + "/" + + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/attachments/" + logId + "/" + URLEncoder.encode(attachmentName, StandardCharsets.UTF_8).replace("+", "%20"))) // + char does not work in path element! .GET() .build(); @@ -506,7 +511,7 @@ public InputStream getAttachment(Long logId, String attachmentName) { public SearchResult getArchivedEntries(long id) { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/archived/" + id)) .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) .header("Content-Type", CONTENT_TYPE_JSON) @@ -527,7 +532,7 @@ public SearchResult getArchivedEntries(long id) { public Collection getTemplates() { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/templates")) .header("Content-Type", CONTENT_TYPE_JSON) .GET() @@ -554,7 +559,7 @@ public LogTemplate saveTemplate(LogTemplate template) throws LogbookException { try { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/templates")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/templates")) .header("Content-Type", CONTENT_TYPE_JSON) .header("Authorization", basicAuthenticationHeader) .PUT(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(template))) @@ -575,7 +580,7 @@ public LogTemplate saveTemplate(LogTemplate template) throws LogbookException { @Override public Collection listLevels() { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + "/levels")) + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/levels")) .GET() .build(); diff --git a/app/logbook/olog/ui/pom.xml b/app/logbook/olog/ui/pom.xml index 9b26eb88b8..48915873b2 100644 --- a/app/logbook/olog/ui/pom.xml +++ b/app/logbook/olog/ui/pom.xml @@ -16,9 +16,14 @@ org.phoebus - core-websocket + core-websocket-common 5.0.3-SNAPSHOT + + org.phoebus + core-websocket-client + 5.0.3-SNAPSHOT + org.phoebus core-ui @@ -60,6 +65,12 @@ 9.0-r1 + + javax.json + javax.json-api + 1.1.2 + + com.atlassian.commonmark diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java index 89d64c8815..924ef05bd6 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java @@ -99,6 +99,7 @@ public LogEntryCalenderViewController(LogClient logClient, OlogQueryManager olog @FXML public void initialize() { + super.initialize(); advancedSearchViewController.setSearchCallback(this::search); configureComboBox(); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 4a058ad0cb..3e5bb795be 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -43,7 +43,7 @@ import javafx.util.Duration; import javafx.util.StringConverter; import org.phoebus.applications.logbook.authentication.OlogAuthenticationScope; -import org.phoebus.core.websocket.WebSocketMessageHandler; +import org.phoebus.core.websocket.client.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; @@ -69,7 +69,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.TimeZone; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -176,6 +175,7 @@ protected void setGoBackAndGoForwardActions(LogEntryTable.GoBackAndGoForwardActi @FXML public void initialize() { + super.initialize(); logEntryDisplayController.setLogEntryTableViewController(this); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 1f5a4ac0ee..e3dda11d1c 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -1,7 +1,9 @@ package org.phoebus.logbook.olog.ui; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -9,18 +11,19 @@ import javafx.fxml.FXML; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; -import org.phoebus.core.websocket.WebSocketMessageHandler; -import org.phoebus.core.websocket.springframework.WebSocketClientService; +import org.phoebus.core.websocket.client.WebSocketMessageHandler; +import org.phoebus.core.websocket.common.Constants; +import org.phoebus.core.websocket.common.WebSocketMessage; +import org.phoebus.core.websocket.client.WebSocketClientService; import org.phoebus.framework.jobs.Job; import org.phoebus.framework.jobs.JobManager; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.SearchResult; -import org.phoebus.logbook.olog.ui.websocket.MessageType; -import org.phoebus.logbook.olog.ui.websocket.WebSocketMessage; +import org.phoebus.logbook.olog.ui.websocket.LogbookMessageType; +import org.phoebus.logbook.olog.ui.websocket.LogbookWebSocketMessageDeserializer; import org.phoebus.olog.es.api.Preferences; -import java.net.URI; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -57,9 +60,8 @@ public abstract class LogbookSearchController implements WebSocketMessageHandler new SimpleObjectProperty<>(ConnectivityMode.NOT_CONNECTED); protected WebSocketClientService webSocketClientService; - private final String webSocketConnectUrl; - private final String subscriptionEndpoint; protected final CountDownLatch connectivityCheckerCountDownLatch = new CountDownLatch(1); + private String webSocketConnectUrl; @SuppressWarnings("unused") @FXML @@ -69,36 +71,32 @@ public abstract class LogbookSearchController implements WebSocketMessageHandler @FXML private GridPane viewSearchPane; - public LogbookSearchController() { - String baseUrl = Preferences.olog_url; - URI uri = URI.create(baseUrl); - String scheme = uri.getScheme(); - String host = uri.getHost(); - int port = uri.getPort(); - String path = uri.getPath(); - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; - this.webSocketConnectUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path + "/web-socket"; - this.subscriptionEndpoint = path + "/web-socket/messages"; + @FXML + public void initialize() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(WebSocketMessage.class, new LogbookWebSocketMessageDeserializer(WebSocketMessage.class)); + objectMapper.registerModule(module); + webSocketConnectUrl = Preferences.olog_url.trim().toLowerCase().startsWith("https://") ? + Preferences.olog_url.trim().replace("https", "wss") : + Preferences.olog_url.trim().replace("http", "ws"); + webSocketConnectUrl += "/Olog" + Constants.WEB_SOCKET; } /** * Determines how the client may connect to the remote service. The service info endpoint is called to establish * availability of the service. If available, then a single web socket connection is attempted to determine * if the service supports web sockets. + * * @param consumer {@link Consumer} called when the connectivity mode has been determined. */ - protected void determineConnectivity(Consumer consumer){ - + protected void determineConnectivity(Consumer consumer) { // Try to determine the connection mode: is the remote service available at all? // If so, does it accept web socket connections? JobManager.schedule("Connection mode probe", monitor -> { ConnectivityMode connectivityMode = ConnectivityMode.NOT_CONNECTED; String serviceInfo = client.serviceInfo(); if (serviceInfo != null && !serviceInfo.isEmpty()) { // service online, check web socket availability - if (WebSocketClientService.checkAvailability(this.webSocketConnectUrl)) { + if (WebSocketClientService.checkAvailability(webSocketConnectUrl)) { connectivityMode = ConnectivityMode.WEB_SOCKETS_SUPPORTED; } else { connectivityMode = ConnectivityMode.HTTP_ONLY; @@ -184,8 +182,7 @@ public void shutdown() { Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Shutting down web socket"); webSocketClientService.removeWebSocketMessageHandler(this); webSocketClientService.shutdown(); - } - else if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)){ + } else if (connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)) { cancelPeriodSearch(); } } @@ -204,7 +201,7 @@ protected void connectWebSocket() { webSocketConnected.set(false); viewSearchPane.visibleProperty().set(false); errorPane.visibleProperty().set(true); - }, webSocketConnectUrl, subscriptionEndpoint, null); + }, webSocketConnectUrl); webSocketClientService.addWebSocketMessageHandler(this); webSocketClientService.connect(); } @@ -212,8 +209,9 @@ protected void connectWebSocket() { @Override public void handleWebSocketMessage(String message) { try { - WebSocketMessage webSocketMessage = objectMapper.readValue(message, WebSocketMessage.class); - if (webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)) { + WebSocketMessage webSocketMessage = objectMapper.readValue(message, new TypeReference<>() { + }); + if (webSocketMessage.messageType().equals(LogbookMessageType.NEW_LOG_ENTRY)) { // Add a random sleep 0 - 5 seconds to avoid an avalanche of search requests on the service. long randomSleepTime = Math.round(5000 * Math.random()); try { diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/LogbookMessageType.java similarity index 83% rename from app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java rename to app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/LogbookMessageType.java index b7406c1ff1..28e15291ef 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/LogbookMessageType.java @@ -18,7 +18,12 @@ package org.phoebus.logbook.olog.ui.websocket; -public enum MessageType { +import org.phoebus.core.websocket.common.MessageType; + +/** + * Web socket message types particular to the logbook + */ +public enum LogbookMessageType implements MessageType { NEW_LOG_ENTRY, LOG_ENTRY_UPDATED, SHOW_BANNER diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/LogbookWebSocketMessageDeserializer.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/LogbookWebSocketMessageDeserializer.java new file mode 100644 index 0000000000..ac9f8c3dab --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/LogbookWebSocketMessageDeserializer.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.logbook.olog.ui.websocket; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.phoebus.core.websocket.common.WebSocketMessage; + +/** + * Custom JSON deserializer of {@link WebSocketMessage}s particular to the logbook app. + */ +public class LogbookWebSocketMessageDeserializer extends StdDeserializer> { + + public LogbookWebSocketMessageDeserializer(Class clazz) { + super(clazz); + } + + /** + * Deserializes a save-and-restore {@link WebSocketMessage}. + * + * @param jsonParser Parsed used for reading JSON content + * @param context Context that can be used to access information about + * this deserialization activity. + * @return A {@link WebSocketMessage} object, or null if deserialization fails, e.g. due to + * unknown/invalid {@link org.phoebus.core.websocket.common.MessageType} or null payload. + */ + @Override + public WebSocketMessage deserialize(JsonParser jsonParser, DeserializationContext context) { + try { + JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); + LogbookMessageType logbookMessageType = LogbookMessageType.valueOf(rootNode.get("messageType").asText()); + JsonNode payload = rootNode.get("payload"); + switch (logbookMessageType) { + case NEW_LOG_ENTRY -> { + return new WebSocketMessage<>(logbookMessageType, null); + } + case LOG_ENTRY_UPDATED -> { + return new WebSocketMessage<>(logbookMessageType, payload.textValue()); + } + case SHOW_BANNER -> throw new RuntimeException("SHOW_BANNER not yet implemented"); + + } + } catch (Exception e) { + return null; + } + return null; + } + +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java deleted file mode 100644 index 299304bcf8..0000000000 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2020 European Spallation Source ERIC. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -package org.phoebus.logbook.olog.ui.websocket; - -public record WebSocketMessage(MessageType messageType, String payload) { -} diff --git a/app/logbook/olog/ui/src/test/java/org/phoebus/logbook/olog/ui/websocket/LogbookWebSocketMessageDeserializerTest.java b/app/logbook/olog/ui/src/test/java/org/phoebus/logbook/olog/ui/websocket/LogbookWebSocketMessageDeserializerTest.java new file mode 100644 index 0000000000..94c57b5e42 --- /dev/null +++ b/app/logbook/olog/ui/src/test/java/org/phoebus/logbook/olog/ui/websocket/LogbookWebSocketMessageDeserializerTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.logbook.olog.ui.websocket; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.jupiter.api.Test; +import org.phoebus.core.websocket.common.WebSocketMessage; + +import static org.junit.jupiter.api.Assertions.*; + +public class LogbookWebSocketMessageDeserializerTest { + + private ObjectMapper objectMapper = new ObjectMapper(); + + public LogbookWebSocketMessageDeserializerTest() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(WebSocketMessage.class, new LogbookWebSocketMessageDeserializer(WebSocketMessage.class)); + objectMapper.registerModule(module); + } + + @Test + public void testDeserialize_1() throws Exception{ + WebSocketMessage webSocketMessage = objectMapper.readValue(getClass().getResourceAsStream("/websocketexample1.json"), new TypeReference<>() { + }); + assertEquals(LogbookMessageType.NEW_LOG_ENTRY, webSocketMessage.messageType()); + } + + @Test + public void testDeserialize_2() throws Exception{ + WebSocketMessage webSocketMessage = objectMapper.readValue(getClass().getResourceAsStream("/websocketexample2.json"), new TypeReference<>() { + }); + assertEquals(LogbookMessageType.LOG_ENTRY_UPDATED, webSocketMessage.messageType()); + assertEquals("logEntryId", webSocketMessage.payload()); + } + + @Test + public void testDeserialize_3() throws Exception{ + WebSocketMessage webSocketMessage = objectMapper.readValue(getClass().getResourceAsStream("/websocketexample3.json"), new TypeReference<>() { + }); + assertNull(webSocketMessage); + } +} diff --git a/app/logbook/olog/ui/src/test/resources/websocketexample1.json b/app/logbook/olog/ui/src/test/resources/websocketexample1.json new file mode 100644 index 0000000000..5eeb172715 --- /dev/null +++ b/app/logbook/olog/ui/src/test/resources/websocketexample1.json @@ -0,0 +1,3 @@ +{ + "messageType": "NEW_LOG_ENTRY" +} \ No newline at end of file diff --git a/app/logbook/olog/ui/src/test/resources/websocketexample2.json b/app/logbook/olog/ui/src/test/resources/websocketexample2.json new file mode 100644 index 0000000000..1cb8559541 --- /dev/null +++ b/app/logbook/olog/ui/src/test/resources/websocketexample2.json @@ -0,0 +1,4 @@ +{ + "messageType": "LOG_ENTRY_UPDATED", + "payload" : "logEntryId" +} \ No newline at end of file diff --git a/app/logbook/olog/ui/src/test/resources/websocketexample3.json b/app/logbook/olog/ui/src/test/resources/websocketexample3.json new file mode 100644 index 0000000000..0a0274351a --- /dev/null +++ b/app/logbook/olog/ui/src/test/resources/websocketexample3.json @@ -0,0 +1,3 @@ +{ + "messageType": "INVALID" +} \ No newline at end of file diff --git a/app/save-and-restore/app/pom.xml b/app/save-and-restore/app/pom.xml index 29b2b634a8..10a3a4feaf 100644 --- a/app/save-and-restore/app/pom.xml +++ b/app/save-and-restore/app/pom.xml @@ -32,7 +32,12 @@ org.phoebus - core-websocket + core-websocket-common + 5.0.3-SNAPSHOT + + + org.phoebus + core-websocket-client 5.0.3-SNAPSHOT diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java index e1e55911f0..f5837494fa 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java @@ -21,7 +21,6 @@ import javafx.beans.property.SimpleStringProperty; import org.phoebus.applications.saveandrestore.authentication.SaveAndRestoreAuthenticationScope; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.security.store.SecureStore; import org.phoebus.security.tokens.ScopedAuthenticationToken; @@ -33,11 +32,11 @@ public abstract class SaveAndRestoreBaseController { protected final SimpleStringProperty userIdentity = new SimpleStringProperty(); - protected final WebSocketClientService webSocketClientService; + //protected final WebSocketClientService webSocketClientService; protected final SaveAndRestoreService saveAndRestoreService; public SaveAndRestoreBaseController() { - this.webSocketClientService = WebSocketClientService.getInstance(); + //this.webSocketClientService = WebSocketClientService.getInstance(); this.saveAndRestoreService = SaveAndRestoreService.getInstance(); try { SecureStore secureStore = new SecureStore(); @@ -50,7 +49,7 @@ public SaveAndRestoreBaseController() { } } catch (Exception e) { Logger.getLogger(SaveAndRestoreBaseController.class.getName()).log(Level.WARNING, "Unable to retrieve authentication token for " + - new SaveAndRestoreAuthenticationScope().getScope()+ " scope", e); + new SaveAndRestoreAuthenticationScope().getScope() + " scope", e); } } @@ -69,13 +68,6 @@ public SimpleStringProperty getUserIdentity() { return userIdentity; } - /** - * Default no-op implementation of a handler for {@link SaveAndRestoreWebSocketMessage}s. - * @param webSocketMessage See {@link SaveAndRestoreWebSocketMessage} - */ - protected void handleWebSocketMessage(SaveAndRestoreWebSocketMessage webSocketMessage){ - } - /** * Performs suitable cleanup, e.g. close web socket and PVs (where applicable). */ @@ -83,6 +75,7 @@ protected void handleWebSocketMessage(SaveAndRestoreWebSocketMessage webSocke /** * Checks if the tab may be closed, e.g. if data managed in the UI has been saved. + * * @return false if tab contains unsaved data, otherwise true */ public abstract boolean doCloseCheck(); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java index b658453f9f..d18bc71c94 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java @@ -80,7 +80,8 @@ import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil.Keys; import org.phoebus.applications.saveandrestore.model.search.SearchResult; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.configuration.ConfigurationTab; import org.phoebus.applications.saveandrestore.ui.contextmenu.CopyUniqueIdToClipboardMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.CreateSnapshotMenuItem; @@ -101,6 +102,7 @@ import org.phoebus.applications.saveandrestore.ui.snapshot.SnapshotTab; import org.phoebus.applications.saveandrestore.ui.snapshot.tag.TagUtil; import org.phoebus.applications.saveandrestore.ui.snapshot.tag.TagWidget; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.preferences.PhoebusPreferenceService; import org.phoebus.framework.selection.SelectionService; @@ -132,7 +134,6 @@ import java.util.ServiceLoader; import java.util.Stack; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -141,7 +142,7 @@ * Main controller for the save and restore UI. */ public class SaveAndRestoreController extends SaveAndRestoreBaseController - implements Initializable, WebSocketMessageHandler { + implements Initializable, SaveAndRestoreWebSocketMessageHandler { @FXML protected TreeView treeView; @@ -252,7 +253,6 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @Override public void initialize(URL url, ResourceBundle resourceBundle) { - // Tree items are first compared on type, then on name (case-insensitive). treeNodeComparator = Comparator.comparing(TreeItem::getValue); @@ -368,10 +368,10 @@ public Filter fromString(String s) { treeView.visibleProperty().bind(serviceConnected); errorPane.visibleProperty().bind(serviceConnected.not()); - webSocketClientService.addWebSocketMessageHandler(this); - webSocketClientService.setConnectCallback(this::handleWebSocketConnected); - webSocketClientService.setDisconnectCallback(this::handleWebSocketDisconnected); - webSocketClientService.connect(); + saveAndRestoreService.addSaveAndRestoreWebSocketMessageHandler(this); + saveAndRestoreService.setConnectCallback(this::handleWebSocketConnected); + saveAndRestoreService.setDisconnectCallback(this::handleWebSocketDisconnected); + saveAndRestoreService.connectWebSocket(); } @@ -816,8 +816,10 @@ private void nodeAdded(String nodeId) { // Find the parent to which the new node is to be added TreeItem parentTreeItem = recursiveSearch(parentNode.getUniqueId(), treeView.getRoot()); TreeItem newTreeItem = createTreeItem(newNode); - parentTreeItem.getChildren().add(newTreeItem); - parentTreeItem.getChildren().sort(treeNodeComparator); + Platform.runLater(() -> { + parentTreeItem.getChildren().add(newTreeItem); + parentTreeItem.getChildren().sort(treeNodeComparator); + }); } catch (Exception e) { throw new RuntimeException(e); } @@ -832,7 +834,7 @@ private void nodeAdded(String nodeId) { private void nodeRemoved(String nodeId) { TreeItem treeItemToRemove = recursiveSearch(nodeId, treeView.getRoot()); if (treeItemToRemove != null) { - treeItemToRemove.getParent().getChildren().remove(treeItemToRemove); + Platform.runLater(() -> treeItemToRemove.getParent().getChildren().remove(treeItemToRemove)); } } @@ -861,7 +863,7 @@ public void locateNode(Stack nodeStack) { while (!nodeStack.isEmpty()) { Node currentNode = nodeStack.pop(); TreeItem currentTreeItem = recursiveSearch(currentNode.getUniqueId(), parentTreeItem); - if(!currentTreeItem.isExpanded()){ + if (!currentTreeItem.isExpanded()) { expandTreeNode(currentTreeItem); } parentTreeItem = currentTreeItem; @@ -942,9 +944,10 @@ public void saveLocalState() { @Override public void handleTabClosed() { - tabPane.getTabs().forEach(t -> ((SaveAndRestoreTab)t).handleTabClosed()); + tabPane.getTabs().forEach(t -> ((SaveAndRestoreTab) t).handleTabClosed()); saveLocalState(); - webSocketClientService.closeWebSocket(); + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); + saveAndRestoreService.closeWebSocket(); filterActivators.forEach(FilterActivator::stop); } @@ -1125,11 +1128,11 @@ public boolean matchesFilter(Node node) { * the {@link Filter}, then the {@link TreeView} is updated based on the search result. * * @param filter {@link Filter} selected by user or through business logic. If null, then the - * no filter {@link Filter} is applied. + * no filter {@link Filter} is applied. */ private void applyFilter(Filter filter) { treeView.getSelectionModel().clearSelection(); - if(filter == null){ + if (filter == null) { return; } Map searchParams = @@ -1176,7 +1179,7 @@ private void filterAddedOrUpdated(Filter filter) { filtersComboBox.valueProperty().set(filter); // If this is the current filter, update the tree view if (filter.equals(filtersComboBox.getSelectionModel().getSelectedItem())) { - currentFilterProperty.set(filter); + currentFilterProperty.set(filter); } } } @@ -1438,13 +1441,14 @@ private void addOptionalLoggingMenuItem() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - switch (saveAndRestoreWebSocketMessage.messageType()) { - case NODE_ADDED -> nodeAdded((String) saveAndRestoreWebSocketMessage.payload()); - case NODE_REMOVED -> nodeRemoved((String) saveAndRestoreWebSocketMessage.payload()); - case NODE_UPDATED -> nodeChanged((Node) saveAndRestoreWebSocketMessage.payload()); - case FILTER_ADDED_OR_UPDATED -> filterAddedOrUpdated((Filter) saveAndRestoreWebSocketMessage.payload()); - case FILTER_REMOVED -> filterRemoved((String) saveAndRestoreWebSocketMessage.payload()); + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage) { + SaveAndRestoreMessageType messageType = (SaveAndRestoreMessageType) webSocketMessage.messageType(); + switch (messageType) { + case NODE_ADDED -> nodeAdded((String) webSocketMessage.payload()); + case NODE_REMOVED -> nodeRemoved((String) webSocketMessage.payload()); + case NODE_UPDATED -> nodeChanged((Node) webSocketMessage.payload()); + case FILTER_ADDED_OR_UPDATED -> filterAddedOrUpdated((Filter) webSocketMessage.payload()); + case FILTER_REMOVED -> filterRemoved((String) webSocketMessage.payload()); } } @@ -1478,6 +1482,7 @@ public void activateFilter(String filterName) { /** * If auto {@link Filter} activation is enabled and the active filter matches filterName, then * this method will switch to no filter but maintain auto activation. + * * @param filterName Name/id of the de-activated filter. */ public void deactivateFilter(String filterName) { @@ -1500,16 +1505,16 @@ private void handleWebSocketDisconnected() { } @Override - public boolean doCloseCheck(){ - for(Tab tab : tabPane.getTabs()){ - if(!((SaveAndRestoreTab)tab).doCloseCheck()){ + public boolean doCloseCheck() { + for (Tab tab : tabPane.getTabs()) { + if (!((SaveAndRestoreTab) tab).doCloseCheck()) { return false; } } return true; } - private void findReferences(){ + private void findReferences() { SearchAndFilterTab searchAndFilterTab = openSearchWindow(); searchAndFilterTab.getController().findReferencesForSnapshot(treeView.getSelectionModel().getSelectedItems().get(0).getValue().getUniqueId()); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java index 1203fe1d7f..707e93b90b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java @@ -18,7 +18,11 @@ package org.phoebus.applications.saveandrestore.ui; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.client.Preferences; import org.phoebus.applications.saveandrestore.client.SaveAndRestoreClient; import org.phoebus.applications.saveandrestore.client.SaveAndRestoreClientImpl; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; @@ -35,7 +39,13 @@ import org.phoebus.applications.saveandrestore.model.TagData; import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchResult; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageDeserializer; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.core.websocket.common.Constants; +import org.phoebus.core.websocket.common.WebSocketMessage; +import org.phoebus.core.websocket.client.WebSocketMessageHandler; +import org.phoebus.core.websocket.client.WebSocketClientService; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.saveandrestore.util.VNoData; @@ -45,6 +55,7 @@ import javax.ws.rs.core.MultivaluedMap; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -55,7 +66,12 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -public class SaveAndRestoreService { +/** + * Single to class providing an API to interact with the remote Save-and-Restore service. It also + * manages web socket communication such that API clients may be notified when service dispatches + * messages, e.g. when data has changed. + */ +public class SaveAndRestoreService implements WebSocketMessageHandler { private final ExecutorService executor; @@ -64,10 +80,23 @@ public class SaveAndRestoreService { private static SaveAndRestoreService instance; private final SaveAndRestoreClient saveAndRestoreClient; + private final WebSocketClientService webSocketClientService; + private final List saveAndRestoreWebSocketMessageHandlers = + Collections.synchronizedList(new ArrayList<>()); + private final ObjectMapper objectMapper = new ObjectMapper(); private SaveAndRestoreService() { saveAndRestoreClient = new SaveAndRestoreClientImpl(); + String webSocketConnectUrl = Preferences.jmasarServiceUrl.trim().toLowerCase().startsWith("https://") ? + Preferences.jmasarServiceUrl.trim().replace("https", "wss") : + Preferences.jmasarServiceUrl.trim().replace("http", "ws"); + webSocketConnectUrl += Constants.WEB_SOCKET; + webSocketClientService = new WebSocketClientService(webSocketConnectUrl); + webSocketClientService.addWebSocketMessageHandler(this); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addDeserializer(WebSocketMessage.class, new SaveAndRestoreWebSocketMessageDeserializer(WebSocketMessage.class)); + objectMapper.registerModule(simpleModule); } public static SaveAndRestoreService getInstance() { @@ -421,4 +450,70 @@ private VType readFromArchiver(String pvName, Instant time) { } return pvValue == null ? VDisconnectedData.INSTANCE : pvValue; } + + /** + * Connects to the web socket + */ + public void connectWebSocket() { + webSocketClientService.connect(); + } + + /** + * Registers a handler for {@link WebSocketMessage}s. + * + * @param saveAndRestoreWebSocketMessageHandler A {@link SaveAndRestoreWebSocketMessageHandler} instance. + */ + public void addSaveAndRestoreWebSocketMessageHandler(SaveAndRestoreWebSocketMessageHandler saveAndRestoreWebSocketMessageHandler) { + saveAndRestoreWebSocketMessageHandlers.add(saveAndRestoreWebSocketMessageHandler); + } + + /** + * Unregisters a handler for {@link WebSocketMessage}s + * + * @param saveAndRestoreWebSocketMessageHandler A {@link SaveAndRestoreWebSocketMessageHandler} instance. + */ + public void removeSaveAndRestoreWebSocketMessageHandler(SaveAndRestoreWebSocketMessageHandler saveAndRestoreWebSocketMessageHandler) { + saveAndRestoreWebSocketMessageHandlers.remove(saveAndRestoreWebSocketMessageHandler); + } + + /** + * @param callback A {@link Runnable} called when the web socket has been connected successfully. + */ + public void setConnectCallback(Runnable callback) { + webSocketClientService.setConnectCallback(callback); + } + + /** + * @param callback A {@link Runnable} called when the web socket has been disconnected for whatever reason, + * e.g. remote service becomes unavailable. + */ + public void setDisconnectCallback(Runnable callback) { + webSocketClientService.setDisconnectCallback(callback); + } + + /** + * Closes the web socket + */ + public void closeWebSocket() { + webSocketClientService.removeWebSocketMessageHandler(this); + webSocketClientService.shutdown(); + } + + /** + * Handler for raw web socket messages. + * + * @param message A raw message as dispatched by the web socket peer. + */ + @Override + public void handleWebSocketMessage(String message) { + try { + WebSocketMessage webSocketMessage = objectMapper.readValue(message, new TypeReference<>() { + }); + saveAndRestoreWebSocketMessageHandlers.forEach(h -> + h.handleSaveAndRestoreWebSocketMessage(webSocketMessage)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to deserialize web socket message " + message, e); + } + } + } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java index 7edab3004d..14a46fc7fb 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java @@ -24,8 +24,10 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; import javafx.scene.image.ImageView; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.snapshot.SnapshotTab; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.javafx.ImageCache; @@ -35,7 +37,7 @@ /** * Base class for save-n-restore {@link Tab}s containing common functionality. */ -public abstract class SaveAndRestoreTab extends Tab implements WebSocketMessageHandler { +public abstract class SaveAndRestoreTab extends Tab implements SaveAndRestoreWebSocketMessageHandler{ protected SaveAndRestoreBaseController controller; @@ -64,11 +66,14 @@ public SaveAndRestoreTab() { setOnCloseRequest(event -> { if (doCloseCheck()) { + SaveAndRestoreService.getInstance().removeSaveAndRestoreWebSocketMessageHandler(this); handleTabClosed(); } else { event.consume(); } }); + + SaveAndRestoreService.getInstance().addSaveAndRestoreWebSocketMessageHandler(this); } /** @@ -80,11 +85,6 @@ public void secureStoreChanged(List validTokens) { controller.secureStoreChanged(validTokens); } - @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - - } - /** * Performs suitable cleanup, e.g. close web socket and PVs (where applicable). */ @@ -94,9 +94,21 @@ public void handleTabClosed() { /** * Checks if the tab may be closed, e.g. if data managed in the UI has been saved. + * * @return false if tab contains unsaved data, otherwise true */ public boolean doCloseCheck() { return controller.doCloseCheck(); } + + @Override + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage){ + if (webSocketMessage.messageType().equals(SaveAndRestoreMessageType.NODE_REMOVED)) { + String nodeId = (String) webSocketMessage.payload(); + if (getId() != null && nodeId.equals(getId())) { + Platform.runLater(() -> getTabPane().getTabs().remove(this)); + } + } + } + } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketClientService.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketClientService.java deleted file mode 100644 index 0deddbd10d..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketClientService.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.applications.saveandrestore.ui; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.phoebus.applications.saveandrestore.client.Preferences; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; -import org.phoebus.applications.saveandrestore.model.websocket.WebMessageDeserializer; -import org.phoebus.core.websocket.WebSocketClient; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class WebSocketClientService { - - private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); - - private static WebSocketClientService instance; - private final WebSocketClient webSocketClient; - - private final ObjectMapper objectMapper; - - private WebSocketClientService() { - String baseUrl = Preferences.jmasarServiceUrl; - String schema = baseUrl.startsWith("https") ? "wss" : "ws"; - String webSocketUrl = schema + baseUrl.substring(baseUrl.indexOf("://")) + "/web-socket"; - URI webSocketUri = URI.create(webSocketUrl); - webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketMessage); - objectMapper = new ObjectMapper(); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - SimpleModule module = new SimpleModule(); - module.addDeserializer(SaveAndRestoreWebSocketMessage.class, - new WebMessageDeserializer(SaveAndRestoreWebSocketMessage.class)); - objectMapper.registerModule(module); - } - - public static WebSocketClientService getInstance() { - if (instance == null) { - instance = new WebSocketClientService(); - } - return instance; - } - - public void connect() { - webSocketClient.connect(); - } - - public void closeWebSocket() { - webSocketMessageHandlers.clear(); - webSocketClient.close("Application shutdown"); - } - - public void addWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler) { - webSocketMessageHandlers.add(webSocketMessageHandler); - } - - public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler) { - webSocketMessageHandlers.remove(webSocketMessageHandler); - } - - private void handleWebSocketMessage(CharSequence charSequence) { - try { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = - objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); - webSocketMessageHandlers.forEach(w -> w.handleWebSocketMessage(saveAndRestoreWebSocketMessage)); - - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - public void setConnectCallback(Runnable connectCallback) { - webSocketClient.setConnectCallback(connectCallback); - } - - public void setDisconnectCallback(Runnable disconnectCallback) { - webSocketClient.setDisconnectCallback(disconnectCallback); - } -} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketMessageHandler.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketMessageHandler.java deleted file mode 100644 index 3e202fb660..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketMessageHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.applications.saveandrestore.ui; - -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; - -public interface WebSocketMessageHandler { - - void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage); -} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java index 9e252b8738..5967350d36 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java @@ -59,11 +59,11 @@ import org.phoebus.applications.saveandrestore.model.ConfigurationData; import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.core.types.ProcessVariable; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; @@ -83,7 +83,8 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -public class ConfigurationController extends SaveAndRestoreBaseController implements WebSocketMessageHandler { +public class ConfigurationController extends SaveAndRestoreBaseController + implements SaveAndRestoreWebSocketMessageHandler { @FXML @SuppressWarnings("unused") @@ -365,7 +366,7 @@ public void commitEdit(Double value) { addPVsPane.disableProperty().bind(userIdentity.isNull()); - webSocketClientService.addWebSocketMessageHandler(this); + saveAndRestoreService.addSaveAndRestoreWebSocketMessageHandler(this); } @FXML @@ -534,14 +535,14 @@ public boolean doCloseCheck() { } @Override - public void handleTabClosed(){ - webSocketClientService.removeWebSocketMessageHandler(this); + public void handleTabClosed() { + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { - Node node = (Node) saveAndRestoreWebSocketMessage.payload(); + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage) { + if (webSocketMessage.messageType().equals(SaveAndRestoreMessageType.NODE_UPDATED)) { + Node node = (Node) webSocketMessage.payload(); if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { loadConfiguration(node); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java index dcbb9cb9ca..5db38a5343 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java @@ -18,23 +18,19 @@ */ package org.phoebus.applications.saveandrestore.ui.configuration; -import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.image.ImageView; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; -public class ConfigurationTab extends SaveAndRestoreTab implements WebSocketMessageHandler { +public class ConfigurationTab extends SaveAndRestoreTab { public ConfigurationTab() { try { @@ -80,14 +76,4 @@ public void editConfiguration(Node configurationNode) { public void configureForNewConfiguration(Node parentNode) { ((ConfigurationController) controller).newConfiguration(parentNode); } - - @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_REMOVED)) { - String nodeId = (String) saveAndRestoreWebSocketMessage.payload(); - if (getId() != null && nodeId.equals(getId())) { - Platform.runLater(() -> getTabPane().getTabs().remove(this)); - } - } - } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java index 2f4fd38bac..4e00ab82e2 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java @@ -50,12 +50,13 @@ import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil.Keys; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.HelpViewer; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.framework.jobs.JobManager; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.autocomplete.PVAutocompleteMenu; @@ -79,7 +80,7 @@ import java.util.stream.Collectors; public class SearchAndFilterViewController extends SaveAndRestoreBaseController - implements Initializable, WebSocketMessageHandler { + implements Initializable, SaveAndRestoreWebSocketMessageHandler { private final SaveAndRestoreController saveAndRestoreController; @@ -380,7 +381,7 @@ public void initialize(URL url, ResourceBundle resourceBundle) { loadFilters(); - webSocketClientService.addWebSocketMessageHandler(this); + saveAndRestoreService.addSaveAndRestoreWebSocketMessageHandler(this); progressIndicator.visibleProperty().bind(disableUi); disableUi.addListener((observable, oldValue, newValue) -> mainUi.setDisable(newValue)); @@ -636,7 +637,7 @@ private void updatedQueryEditor() { } public void handleSaveAndFilterTabClosed() { - webSocketClientService.removeWebSocketMessageHandler(this); + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); searchResultTableViewController.handleTabClosed(); } @@ -647,15 +648,15 @@ public void secureStoreChanged(List validTokens) { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - switch (saveAndRestoreWebSocketMessage.messageType()) { + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage) { + switch ((SaveAndRestoreMessageType) webSocketMessage.messageType()) { case FILTER_REMOVED, FILTER_ADDED_OR_UPDATED -> loadFilters(); } } @Override public void handleTabClosed() { - webSocketClientService.removeWebSocketMessageHandler(this); + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); } @Override diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java index 661c564843..2c3d203a49 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java @@ -41,18 +41,19 @@ import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil; import org.phoebus.applications.saveandrestore.model.search.SearchResult; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.RestoreMode; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.contextmenu.LoginMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.RestoreFromClientMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.RestoreFromServiceMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.TagGoldenMenuItem; import org.phoebus.applications.saveandrestore.ui.snapshot.tag.TagUtil; import org.phoebus.applications.saveandrestore.ui.snapshot.tag.TagWidget; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.workbench.ApplicationService; import org.phoebus.ui.dialog.DialogHelper; @@ -80,7 +81,7 @@ * Controller for the search result table. */ public class SearchResultTableViewController extends SaveAndRestoreBaseController - implements Initializable, WebSocketMessageHandler { + implements Initializable, SaveAndRestoreWebSocketMessageHandler { @SuppressWarnings("unused") @FXML @@ -288,7 +289,7 @@ protected void updateItem(Node node, boolean empty) { } }); - webSocketClientService.addWebSocketMessageHandler(this); + saveAndRestoreService.addSaveAndRestoreWebSocketMessageHandler(this); } private ImageView getImageView(Node node) { @@ -413,19 +414,19 @@ public void loadFilter(String filterId) { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - switch (saveAndRestoreWebSocketMessage.messageType()) { + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage) { + switch ((SaveAndRestoreMessageType) webSocketMessage.messageType()) { case NODE_UPDATED, NODE_REMOVED, NODE_ADDED -> search(); } } @Override public void handleTabClosed() { - webSocketClientService.removeWebSocketMessageHandler(this); + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); } @Override - public boolean doCloseCheck(){ + public boolean doCloseCheck() { return true; } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java index 733f6fef2c..e73d8321d6 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java @@ -59,12 +59,12 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Tag; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -82,7 +82,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -public class CompositeSnapshotController extends SaveAndRestoreBaseController implements WebSocketMessageHandler { +public class CompositeSnapshotController extends SaveAndRestoreBaseController + implements SaveAndRestoreWebSocketMessageHandler { @SuppressWarnings("unused") @FXML @@ -347,7 +348,7 @@ public void updateItem(Node item, boolean empty) { } }); - webSocketClientService.addWebSocketMessageHandler(this); + saveAndRestoreService.addSaveAndRestoreWebSocketMessageHandler(this); } @FXML @@ -443,8 +444,8 @@ public boolean doCloseCheck() { } @Override - public void handleTabClosed(){ - webSocketClientService.removeWebSocketMessageHandler(this); + public void handleTabClosed() { + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); } /** @@ -554,9 +555,9 @@ private void removeListeners() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { - Node node = (Node) saveAndRestoreWebSocketMessage.payload(); + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage) { + if (webSocketMessage.messageType().equals(SaveAndRestoreMessageType.NODE_UPDATED)) { + Node node = (Node) webSocketMessage.payload(); if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { loadCompositeSnapshot(node); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotTab.java index 4068204013..cf9f16c520 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotTab.java @@ -19,17 +19,13 @@ package org.phoebus.applications.saveandrestore.ui.snapshot; -import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.image.ImageView; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -48,8 +44,7 @@ * {@link SnapshotTab} is used to show actual snapshot data. *

*/ -public class CompositeSnapshotTab extends SaveAndRestoreTab implements WebSocketMessageHandler { - +public class CompositeSnapshotTab extends SaveAndRestoreTab { public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { @@ -95,8 +90,6 @@ public void configureForNewCompositeSnapshot(Node parentNode, List snapsho * Configures UI to edit an existing composite snapshot {@link Node} * * @param compositeSnapshotNode non-null configuration {@link Node} - * @param snapshotNodes A potentially empty (but non-null) list of snapshot nodes that should - * be added to the list of references snapshots. */ public void editCompositeSnapshot(Node compositeSnapshotNode) { ((CompositeSnapshotController) controller).loadCompositeSnapshot(compositeSnapshotNode); @@ -111,15 +104,4 @@ public void editCompositeSnapshot(Node compositeSnapshotNode) { public void addToCompositeSnapshot(List snapshotNodes) { ((CompositeSnapshotController) controller).addToCompositeSnapshot(snapshotNodes); } - - @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_REMOVED)) { - String nodeId = (String) saveAndRestoreWebSocketMessage.payload(); - if (getId() != null && nodeId.equals(getId())) { - Platform.runLater(() -> getTabPane().getTabs().remove(this)); - } - } - } - } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index 0e5781836f..e8b53d4c7b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -76,17 +76,17 @@ import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.model.Tag; import org.phoebus.applications.saveandrestore.model.event.SaveAndRestoreEventReceiver; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.RestoreMode; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SnapshotMode; import org.phoebus.applications.saveandrestore.ui.VTypePair; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.core.types.TimeStampedProcessVariable; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; import org.phoebus.saveandrestore.util.SnapshotUtil; @@ -124,7 +124,8 @@ * Once the snapshot has been saved, this controller calls the {@link SnapshotTab} API to load * the view associated with restore actions. */ -public class SnapshotController extends SaveAndRestoreBaseController implements WebSocketMessageHandler { +public class SnapshotController extends SaveAndRestoreBaseController + implements SaveAndRestoreWebSocketMessageHandler { @SuppressWarnings("unused") @@ -271,9 +272,11 @@ public class SnapshotController extends SaveAndRestoreBaseController implements @FXML protected TableColumn baseSnapshotColumn; + @SuppressWarnings("unused") @FXML private TableColumn storedSeverityColumn; + @SuppressWarnings("unused") @FXML private TableColumn liveSeverityColumn; @@ -357,8 +360,8 @@ public class SnapshotController extends SaveAndRestoreBaseController implements * determine which elements in the {@link List} to actually represent. * *

- * Note that the list is cleared and recreated whenever snapshot data has changed, i.e. - * when retrieved from service or when taking a snapshot. + * Note that the list is cleared and recreated whenever snapshot data has changed, i.e. + * when retrieved from service or when taking a snapshot. *

*/ protected final List tableEntryItems = new ArrayList<>(); @@ -670,7 +673,7 @@ protected void updateItem(TableEntry item, boolean empty) { snapshotUtil = new SnapshotUtil(); - webSocketClientService.addWebSocketMessageHandler(this); + saveAndRestoreService.addSaveAndRestoreWebSocketMessageHandler(this); } private void updateUi() { @@ -870,8 +873,8 @@ public boolean doCloseCheck() { } @Override - public void handleTabClosed(){ - webSocketClientService.removeWebSocketMessageHandler(this); + public void handleTabClosed() { + saveAndRestoreService.removeSaveAndRestoreWebSocketMessageHandler(this); dispose(); } @@ -1001,9 +1004,10 @@ private Snapshot getSnapshotFromService(Node snapshotNode) throws Exception { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { - Node node = (Node) saveAndRestoreWebSocketMessage.payload(); + public void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage) { + + if (webSocketMessage.messageType().equals(SaveAndRestoreMessageType.NODE_UPDATED)) { + Node node = (Node) webSocketMessage.payload(); if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { loadSnapshot(node); } @@ -1110,11 +1114,11 @@ private void takeSnapshotReadPVs(Consumer> consumer) { Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode( Node.builder() - .nodeType(NodeType.SNAPSHOT) - // set name and description to preserve the name / comment fields - .name(snapshotNameProperty.getValue()) - .description(snapshotCommentProperty.getValue()) - .build() + .nodeType(NodeType.SNAPSHOT) + // set name and description to preserve the name / comment fields + .name(snapshotNameProperty.getValue()) + .description(snapshotCommentProperty.getValue()) + .build() ); SnapshotData snapshotData = new SnapshotData(); snapshotData.setSnapshotItems(snapshotItems); @@ -1145,7 +1149,7 @@ private void showTakeSnapshotResult(List snapshotItems) { if (snapshotItem.getValue().equals(VDisconnectedData.INSTANCE)) { disconnectedPvEncountered.set(true); Platform.runLater(() -> - actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png")))); + actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png")))); break; } } @@ -1154,7 +1158,7 @@ private void showTakeSnapshotResult(List snapshotItems) { snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { disconnectedReadbackPvEncountered.set(true); Platform.runLater(() -> - actionResultReadbackColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png")))); + actionResultReadbackColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png")))); break; } @@ -1492,7 +1496,7 @@ private void showSnapshotInTable() { }); JobManager.schedule("Connect to PVs", monitor -> - tableEntryItems.forEach(TableEntry::connect)); + tableEntryItems.forEach(TableEntry::connect)); updateTable(); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java index eb8d77565e..63b5b5b885 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java @@ -17,7 +17,6 @@ */ package org.phoebus.applications.saveandrestore.ui.snapshot; -import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; @@ -27,12 +26,9 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Snapshot; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; -import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; @@ -51,7 +47,7 @@ * Note that this class is used also to show the snapshot view for {@link Node}s of type {@link NodeType#COMPOSITE_SNAPSHOT}. *

*/ -public class SnapshotTab extends SaveAndRestoreTab implements WebSocketMessageHandler { +public class SnapshotTab extends SaveAndRestoreTab { public SaveAndRestoreService saveAndRestoreService; @@ -139,14 +135,4 @@ public Node getSnapshotNode() { public Node getConfigNode() { return ((SnapshotController) controller).getConfigurationNode(); } - - @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_REMOVED)) { - String nodeId = (String) saveAndRestoreWebSocketMessage.payload(); - if (getId() != null && nodeId.equals(getId())) { - Platform.runLater(() -> getTabPane().getTabs().remove(this)); - } - } - } } diff --git a/app/save-and-restore/model/pom.xml b/app/save-and-restore/model/pom.xml index 37f2ad21c0..9034653ef4 100644 --- a/app/save-and-restore/model/pom.xml +++ b/app/save-and-restore/model/pom.xml @@ -17,6 +17,18 @@ 5.0.3-SNAPSHOT
+ + org.phoebus + core-websocket-common + 5.0.3-SNAPSHOT + + + + org.phoebus + core-websocket-client + 5.0.3-SNAPSHOT + + javax.json javax.json-api diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/MessageType.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreMessageType.java similarity index 73% rename from app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/MessageType.java rename to app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreMessageType.java index 9cc99875bd..abd666ec1c 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/MessageType.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreMessageType.java @@ -4,10 +4,12 @@ package org.phoebus.applications.saveandrestore.model.websocket; +import org.phoebus.core.websocket.common.MessageType; + /** * Enum to indicate what type of web socket message the service is sending to clients. */ -public enum MessageType { +public enum SaveAndRestoreMessageType implements MessageType { NODE_ADDED, NODE_UPDATED, NODE_REMOVED, diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessage.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessage.java deleted file mode 100644 index 30e6a7d6d7..0000000000 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessage.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (C) 2024 European Spallation Source ERIC. - */ - -package org.phoebus.applications.saveandrestore.model.websocket; - -/** - * Record encapsulating a {@link MessageType} and a payload. - * @param messageType The {@link MessageType} of a web socket message - * @param payload The payload, e.g. {@link String} or {@link org.phoebus.applications.saveandrestore.model.Node} - */ -public record SaveAndRestoreWebSocketMessage(MessageType messageType, T payload) { -} diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializer.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializer.java new file mode 100644 index 0000000000..77ea7d4fbe --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializer.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.model.websocket; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.core.websocket.common.WebSocketMessage; + +/** + * Custom JSON deserializer of {@link WebSocketMessage}s particular to save-and-restore. + */ +public class SaveAndRestoreWebSocketMessageDeserializer extends StdDeserializer> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public SaveAndRestoreWebSocketMessageDeserializer(Class clazz) { + super(clazz); + } + + /** + * Deserializes a save-and-restore {@link WebSocketMessage}. + * + * @param jsonParser Parsed used for reading JSON content + * @param context Context that can be used to access information about + * this deserialization activity. + * @return A {@link WebSocketMessage} object, or null if deserialization fails, e.g. due to + * unknown/invalid {@link org.phoebus.core.websocket.common.MessageType} or null payload. + */ + @Override + public WebSocketMessage deserialize(JsonParser jsonParser, DeserializationContext context) { + try { + JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); + SaveAndRestoreMessageType saveAndRestoreMessageType = SaveAndRestoreMessageType.valueOf(rootNode.get("messageType").asText()); + JsonNode payload = rootNode.get("payload"); + switch (saveAndRestoreMessageType) { + case NODE_ADDED, NODE_REMOVED, FILTER_REMOVED-> { + return new WebSocketMessage<>(saveAndRestoreMessageType, payload.textValue()); + } + case NODE_UPDATED -> { + Node node = objectMapper.readValue(payload.toString(), Node.class); + return new WebSocketMessage<>(saveAndRestoreMessageType, node); + } + case FILTER_ADDED_OR_UPDATED -> { + Filter filter = objectMapper.readValue(payload.toString(), Filter.class); + return new WebSocketMessage<>(saveAndRestoreMessageType, filter); + } + } + } catch (Exception e) { + return null; + } + return null; + } + +} + diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageHandler.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageHandler.java new file mode 100644 index 0000000000..3a83cc476d --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageHandler.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.model.websocket; + +import org.phoebus.core.websocket.common.WebSocketMessage; + +/** + * Handler for web socket messages that have already been deserialized + * to a {@link WebSocketMessage}. + */ +public interface SaveAndRestoreWebSocketMessageHandler { + void handleSaveAndRestoreWebSocketMessage(WebSocketMessage webSocketMessage); +} diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java deleted file mode 100644 index 42c4807a74..0000000000 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.applications.saveandrestore.model.websocket; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.search.Filter; - -/** - * Custom JSON deserializer of {@link SaveAndRestoreWebSocketMessage}s. - */ -public class WebMessageDeserializer extends StdDeserializer { - - private final ObjectMapper objectMapper = new ObjectMapper(); - - public WebMessageDeserializer(Class clazz) { - super(clazz); - } - - /** - * Deserializes a {@link SaveAndRestoreWebSocketMessage}- - * - * @param jsonParser Parsed used for reading JSON content - * @param context Context that can be used to access information about - * this deserialization activity. - * @return A {@link SaveAndRestoreWebSocketMessage} object, or null if deserialization fails. - */ - @Override - public SaveAndRestoreWebSocketMessage deserialize(JsonParser jsonParser, - DeserializationContext context) { - try { - JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); - String messageType = rootNode.get("messageType").asText(); - switch (MessageType.valueOf(messageType)) { - case NODE_ADDED, NODE_REMOVED, FILTER_REMOVED-> { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = - objectMapper.readValue(rootNode.toString(), SaveAndRestoreWebSocketMessage.class); - return saveAndRestoreWebSocketMessage; - } - case NODE_UPDATED -> { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { - }); - return saveAndRestoreWebSocketMessage; - } - case FILTER_ADDED_OR_UPDATED -> { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { - }); - return saveAndRestoreWebSocketMessage; - } - } - } catch (Exception e) { - return null; - } - return null; - } - -} - diff --git a/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java index 0e60a5eb07..8fd044000a 100644 --- a/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java +++ b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java @@ -10,9 +10,11 @@ import org.junit.jupiter.api.Test; import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.core.websocket.common.WebSocketMessage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class SaveAndRestoreWebSocketMessageDeserializerTest { @@ -20,32 +22,30 @@ public class SaveAndRestoreWebSocketMessageDeserializerTest { public SaveAndRestoreWebSocketMessageDeserializerTest(){ SimpleModule module = new SimpleModule(); - module.addDeserializer(SaveAndRestoreWebSocketMessage.class, - new WebMessageDeserializer(SaveAndRestoreWebSocketMessage.class)); + module.addDeserializer(WebSocketMessage.class, new SaveAndRestoreWebSocketMessageDeserializer(WebSocketMessage.class)); mapper.registerModule(module); } @Test public void test1() { try { - SaveAndRestoreWebSocketMessage webSocketMessage = + WebSocketMessage webSocketMessage = mapper.readValue(getClass().getResourceAsStream("/websocketexample2.json"), new TypeReference<>() { }); - assertEquals(MessageType.NODE_UPDATED, webSocketMessage.messageType()); + assertEquals(SaveAndRestoreMessageType.NODE_UPDATED, webSocketMessage.messageType()); assertEquals("a", webSocketMessage.payload().getUniqueId()); } catch (Exception e) { throw new RuntimeException(e); } - } @Test public void test2() { try { - SaveAndRestoreWebSocketMessage webSocketMessage = + WebSocketMessage webSocketMessage = mapper.readValue(getClass().getResourceAsStream("/websocketexample1.json"), new TypeReference<>() { }); - assertEquals(MessageType.NODE_ADDED, webSocketMessage.messageType()); + assertEquals(SaveAndRestoreMessageType.NODE_ADDED, webSocketMessage.messageType()); assertEquals("parentNodeId", webSocketMessage.payload()); } catch (Exception e) { throw new RuntimeException(e); @@ -55,10 +55,10 @@ public void test2() { @Test public void test3() { try { - SaveAndRestoreWebSocketMessage webSocketMessage = + WebSocketMessage webSocketMessage = mapper.readValue(getClass().getResourceAsStream("/websocketexample3.json"), new TypeReference<>() { }); - assertEquals(MessageType.FILTER_ADDED_OR_UPDATED, webSocketMessage.messageType()); + assertEquals(SaveAndRestoreMessageType.FILTER_ADDED_OR_UPDATED, webSocketMessage.messageType()); assertEquals("myFilter", webSocketMessage.payload().getName()); } catch (Exception e) { throw new RuntimeException(e); @@ -68,7 +68,7 @@ public void test3() { @Test public void test4() { try { - SaveAndRestoreWebSocketMessage webSocketMessage = + WebSocketMessage webSocketMessage = mapper.readValue(getClass().getResourceAsStream("/websocketexample4.json"), new TypeReference<>() { }); assertNull(webSocketMessage); @@ -81,10 +81,10 @@ public void test4() { @Test public void test5() { try { - SaveAndRestoreWebSocketMessage webSocketMessage = + WebSocketMessage webSocketMessage = mapper.readValue(getClass().getResourceAsStream("/websocketexample5.json"), new TypeReference<>() { }); - assertNull(webSocketMessage.payload()); + assertNull(webSocketMessage); } catch (Exception e) { throw new RuntimeException(e); diff --git a/core/websocket/client/pom.xml b/core/websocket/client/pom.xml new file mode 100644 index 0000000000..9e5b8093f5 --- /dev/null +++ b/core/websocket/client/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + core-websocket-client + + + org.phoebus + core-websocket + 5.0.3-SNAPSHOT + + + + + org.phoebus + core-framework + 5.0.3-SNAPSHOT + + + org.phoebus + core-websocket-common + 5.0.3-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} + + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + \ No newline at end of file diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/client/src/main/java/org.phoebus.core.websocket.client/WebSocketClientService.java similarity index 84% rename from core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java rename to core/websocket/client/src/main/java/org.phoebus.core.websocket.client/WebSocketClientService.java index 5b83cb5bb6..8aecf35427 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/client/src/main/java/org.phoebus.core.websocket.client/WebSocketClientService.java @@ -2,9 +2,10 @@ * Copyright (C) 2025 European Spallation Source ERIC. */ -package org.phoebus.core.websocket.springframework; +package org.phoebus.core.websocket.client; -import org.phoebus.core.websocket.WebSocketMessageHandler; +import org.phoebus.core.websocket.common.Constants; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.simp.stomp.ConnectionLostException; @@ -19,6 +20,7 @@ import org.springframework.web.socket.messaging.WebSocketStompClient; import java.lang.reflect.Type; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,8 +44,8 @@ *

* Since web socket URL paths are currently hard coded, a remote peer (e.g. Spring Framework STOMP web socket service) must: *

    - *
  • Publish a connect URL like ws(s)://host:port/path/web-socket, where path is optional.
  • - *
  • Publish a topic named /path/web-socket/messages, where path is optional.
  • + *
  • Publish a connect URL like ws(s)://host:port/path/web-socket, where path is optional.
  • + *
  • Publish a topic named /path/web-socket/messages, where path is optional.
  • *
*

*

@@ -64,36 +66,38 @@ public class WebSocketClientService { */ private final String connectUrl; /** - * Subscription endpoint, e.g. /Olog/web-socket/messages + * Subscription endpoint, e.g. /Olog/web-socket/messages or /save-restore/web-socket/messages */ private final String subscriptionEndpoint; - /** - * Echo endpoint /Olog/web-socket/echo - */ - private final String echoEndpoint; private static final Logger logger = Logger.getLogger(WebSocketClientService.class.getName()); /** * Constructor if connect/disconnect callbacks are not needed. + * + * @param connectUrl URL to the service web socket, e.g. ws://localhost:8080/Olog/web.socket. + * The subscription and echo endpoints are computed from the connectUrl. */ @SuppressWarnings("unused") - public WebSocketClientService(String connectUrl, String subscriptionEndpoint, String echoEndpoint) { + public WebSocketClientService(@NonNull String connectUrl) { this.connectUrl = connectUrl; - this.subscriptionEndpoint = subscriptionEndpoint; - this.echoEndpoint = echoEndpoint; + URI uri = URI.create(connectUrl); + String path = uri.getPath(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + this.subscriptionEndpoint = path + Constants.MESSAGES; } /** - * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. - * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. - * remote peer has been shut down. - * @param connectUrl URL to the service web socket, e.g. ws://localhost:8080/Olog/web.socket - * @param subscriptionEndpoint E.g. /Olog/web-socket/messages - * @param echoEndpoint E.g. /Olog/web-socket/echo. May be null if client has no need for echo messages. + * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. + * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. + * remote peer has been shut down. + * @param connectUrl URL to the service web socket, e.g. ws://localhost:8080/Olog/web.socket. + * The subscription and echo endpoints are computed from the connectUrl. */ - public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallback, String connectUrl, String subscriptionEndpoint, String echoEndpoint) { - this(connectUrl, subscriptionEndpoint, echoEndpoint); + public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallback, @NonNull String connectUrl) { + this(connectUrl); this.connectCallback = connectCallback; this.disconnectCallback = disconnectCallback; } @@ -116,18 +120,6 @@ public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessa webSocketMessageHandlers.remove(webSocketMessageHandler); } - /** - * For debugging purposes: peer should just echo back the message on the subscribed topic. - * - * @param message Message for the service to echo - */ - @SuppressWarnings("unused") - public void sendEcho(String message) { - if (stompSession != null && stompSession.isConnected() && echoEndpoint != null) { - stompSession.send(echoEndpoint, message); - } - } - /** * Disconnects the socket if connected and terminates connection thread. */ @@ -155,9 +147,9 @@ public void connect() { logger.log(Level.INFO, "Attempting web socket connection to " + connectUrl); new Thread(() -> { while (true) { - try{ + try { synchronized (WebSocketClientService.this) { - if(attemptReconnect.get()) { + if (attemptReconnect.get()) { stompSession = stompClient.connect(connectUrl, sessionHandler).get(); stompSession.subscribe(this.subscriptionEndpoint, new StompFrameHandler() { @Override @@ -181,7 +173,7 @@ public void handleFrame(StompHeaders headers, Object payload) { try { Thread.sleep(10000); } catch (InterruptedException e) { - logger.log(Level.WARNING, "Got exception when trying to connect", e); + logger.log(Level.WARNING, "Got exception when putting thread to sleep", e); } } }).start(); diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java b/core/websocket/client/src/main/java/org.phoebus.core.websocket.client/WebSocketMessageHandler.java similarity index 78% rename from core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java rename to core/websocket/client/src/main/java/org.phoebus.core.websocket.client/WebSocketMessageHandler.java index 7299c2a2fd..9876f5b4ee 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java +++ b/core/websocket/client/src/main/java/org.phoebus.core.websocket.client/WebSocketMessageHandler.java @@ -2,9 +2,8 @@ * Copyright (C) 2025 European Spallation Source ERIC. */ -package org.phoebus.core.websocket; +package org.phoebus.core.websocket.client; public interface WebSocketMessageHandler { - void handleWebSocketMessage(String message); } diff --git a/core/websocket/common/pom.xml b/core/websocket/common/pom.xml new file mode 100644 index 0000000000..eb3a988a63 --- /dev/null +++ b/core/websocket/common/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + core-websocket-common + + org.phoebus + core-websocket + 5.0.3-SNAPSHOT + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + \ No newline at end of file diff --git a/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/Constants.java b/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/Constants.java new file mode 100644 index 0000000000..32ddd94b3e --- /dev/null +++ b/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/Constants.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.core.websocket.common; + +/** + * Utility class to define string constants shared between client and service. + */ +public class Constants { + + public static final String WEB_SOCKET = "/web-socket"; + public static final String MESSAGES = "/messages"; +} diff --git a/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/MessageType.java b/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/MessageType.java new file mode 100644 index 0000000000..2c61070d65 --- /dev/null +++ b/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/MessageType.java @@ -0,0 +1,8 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.core.websocket.common; + +public interface MessageType { +} diff --git a/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/WebSocketMessage.java b/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/WebSocketMessage.java new file mode 100644 index 0000000000..1c261f3fd0 --- /dev/null +++ b/core/websocket/common/src/main/java/org/phoebus/core/websocket/common/WebSocketMessage.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.core.websocket.common; + +/** + * Record encapsulating a {@link MessageType} and a payload of arbitrary type. + *

+ * The deserialization process of a web socket message into a concrete {@link WebSocketMessage} must be + * delegated to a custom deserializer. + *

+ * @param messageType The {@link MessageType} of a web socket message. Apps can implement as needed. + * @param payload The payload like a {@link String}, or something more specific for the actual use case, e.g. a + * logbook entry or save-and-restore object. + */ +public record WebSocketMessage(MessageType messageType, T payload) { +} diff --git a/core/websocket/pom.xml b/core/websocket/pom.xml index ab45ff67b8..89003b9fed 100644 --- a/core/websocket/pom.xml +++ b/core/websocket/pom.xml @@ -6,29 +6,14 @@ 4.0.0 core-websocket ${project.groupId}:${project.artifactId} + pom org.phoebus core 5.0.3-SNAPSHOT - - - - org.phoebus - core-framework - 5.0.3-SNAPSHOT - - - org.springframework - spring-websocket - ${spring.framework.version} - - - org.springframework - spring-messaging - ${spring.framework.version} - - - - + + common + client + diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java deleted file mode 100644 index 5d7d8242db..0000000000 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.core.websocket; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.nio.ByteBuffer; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A web socket client implementation supporting pong and text messages. - * - *

- * Once connection is established, a ping/pong thread is set up to check peer availability. This should be - * able to handle both remote peer being shut down and network issues. Ping messages are dispatched once - * per minute. A reconnection loop is started if a pong message is not received from peer within three seconds. - *

- */ -public class WebSocketClient implements WebSocket.Listener { - - private WebSocket webSocket; - private final Logger logger = Logger.getLogger(WebSocketClient.class.getName()); - private Runnable connectCallback; - private Runnable disconnectCallback; - private final URI uri; - private final Consumer onTextCallback; - - private final AtomicBoolean attemptReconnect = new AtomicBoolean(); - private final AtomicBoolean keepPinging = new AtomicBoolean(); - private CountDownLatch pingCountdownLatch; - - /** - * @param uri The URI of the web socket peer. - * @param onTextCallback A callback method the API client will use to process web socket messages. - */ - public WebSocketClient(URI uri, Consumer onTextCallback) { - this.uri = uri; - this.onTextCallback = onTextCallback; - } - - /** - * Attempts to connect to the remote web socket. - */ - public void connect() { - doConnect(); - } - - /** - * Internal connect implementation. This is done in a loop with 10 s intervals until - * connection is established. - */ - private void doConnect() { - attemptReconnect.set(true); - new Thread(() -> { - while (attemptReconnect.get()) { - logger.log(Level.INFO, "Attempting web socket connection to " + uri); - HttpClient.newBuilder() - .build() - .newWebSocketBuilder() - .buildAsync(uri, this); - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - logger.log(Level.WARNING, "Got interrupted exception"); - } - } - }).start(); - } - - /** - * Called when connection has been established. An API client may optionally register a - * {@link #connectCallback} which is called when connection is opened. - * - * @param webSocket the WebSocket that has been connected - */ - @Override - public void onOpen(WebSocket webSocket) { - WebSocket.Listener.super.onOpen(webSocket); - attemptReconnect.set(false); - this.webSocket = webSocket; - if (connectCallback != null) { - connectCallback.run(); - } - logger.log(Level.INFO, "Connected to " + uri); - keepPinging.set(true); - new Thread(new PingRunnable()).start(); - } - - /** - * Send a text message to peer. - * - * @param message The actual message. In practice a JSON formatted string that peer can evaluate - * to take proper action. - */ - public void sendText(String message) { - try { - webSocket.sendText(message, true).get(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - /** - * Called when connection has been closed, e.g. by remote peer. An API client may optionally register a - * {@link #disconnectCallback} which is called when connection is opened. - * - *

- * Note that reconnection will be attempted immediately. - *

- * - * @param webSocket the WebSocket that has been connected - */ - @Override - public CompletionStage onClose(WebSocket webSocket, - int statusCode, - String reason) { - logger.log(Level.INFO, "Web socket closed, status code=" + statusCode + ", reason: " + reason); - if (disconnectCallback != null) { - disconnectCallback.run(); - } - return null; - } - - /** - * Utility method to check connectivity. Peer should respond such that {@link #onPong(WebSocket, ByteBuffer)} - * is called. - */ - public void sendPing() { - logger.log(Level.FINE, Thread.currentThread().getName() + " Sending ping"); - webSocket.sendPing(ByteBuffer.allocate(0)); - } - - @Override - public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - pingCountdownLatch.countDown(); - logger.log(Level.FINE, "Got pong"); - return WebSocket.Listener.super.onPong(webSocket, message); - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - logger.log(Level.WARNING, "Got web socket error", error); - WebSocket.Listener.super.onError(webSocket, error); - } - - @Override - public CompletionStage onText(WebSocket webSocket, - CharSequence data, - boolean last) { - webSocket.request(1); - if (onTextCallback != null) { - onTextCallback.accept(data); - } - return WebSocket.Listener.super.onText(webSocket, data, last); - } - - /** - * NOTE: this must be called by the API client when web socket messages are no longer - * needed, otherwise reconnect attempts will continue as these run on a separate thread. - * - *

- * The status code 1000 is used when calling the {@link WebSocket#sendClose(int, String)} method. See - * list of common web socket status codes - * here. - *

- * - * @param reason Custom reason text. - */ - public void close(String reason) { - keepPinging.set(false); - attemptReconnect.set(false); - // webSocket is null if never connected - if(webSocket != null){ - webSocket.sendClose(1000, reason); - } - } - - /** - * @param connectCallback A {@link Runnable} invoked when web socket connects successfully. - */ - public void setConnectCallback(Runnable connectCallback) { - this.connectCallback = connectCallback; - } - - /** - * @param disconnectCallback A {@link Runnable} invoked when web socket disconnects, either - * when closed explicitly, or if remote peer goes away. - */ - public void setDisconnectCallback(Runnable disconnectCallback) { - this.disconnectCallback = disconnectCallback; - } - - private class PingRunnable implements Runnable { - - @Override - public void run() { - while (keepPinging.get()) { - pingCountdownLatch = new CountDownLatch(1); - sendPing(); - try { - if (!pingCountdownLatch.await(3, TimeUnit.SECONDS)) { - if (disconnectCallback != null) { - disconnectCallback.run(); - } - logger.log(Level.WARNING, "No pong response within three seconds"); - doConnect(); - return; - } else { - Thread.sleep(60000); - } - } catch (InterruptedException e) { - logger.log(Level.WARNING, "Got interrupted exception"); - return; - } - } - } - } -} diff --git a/dependencies/phoebus-target/pom.xml b/dependencies/phoebus-target/pom.xml index 4f7543c0cd..6b141c726a 100644 --- a/dependencies/phoebus-target/pom.xml +++ b/dependencies/phoebus-target/pom.xml @@ -8,7 +8,6 @@ phoebus-target ${project.groupId}:${project.artifactId} - 2.7.3 ${project.basedir}/release_classpath.py @@ -444,7 +443,7 @@ org.springframework.boot spring-boot-starter - ${spring.boot-version} + ${spring.boot.version} org.springframework.boot @@ -455,23 +454,20 @@ org.springframework.boot spring-boot-starter-web - ${spring.boot-version} + ${spring.boot.version} + org.springframework.boot spring-boot-starter-test - ${spring.boot-version} + ${spring.boot.version} test + - org.springframework - spring-websocket - ${spring.framework.version} - - - org.springframework - spring-messaging - ${spring.framework.version} + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} diff --git a/pom.xml b/pom.xml index 242479548b..7e46e34688 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ 5.18.4 1.26.1 5.3.22 + 2.7.3 diff --git a/services/alarm-config-logger/pom.xml b/services/alarm-config-logger/pom.xml index d3b483faa7..deca1db494 100644 --- a/services/alarm-config-logger/pom.xml +++ b/services/alarm-config-logger/pom.xml @@ -6,7 +6,6 @@ 5.0.3-SNAPSHOT - 2.7.3 17 service-alarm-config-logger @@ -17,7 +16,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot-version} + ${spring.boot.version} pom import diff --git a/services/alarm-logger/pom.xml b/services/alarm-logger/pom.xml index 5980d534f9..83fd85448c 100644 --- a/services/alarm-logger/pom.xml +++ b/services/alarm-logger/pom.xml @@ -7,7 +7,6 @@ 17 - 2.7.3 8.2.0 service-alarm-logger @@ -18,7 +17,7 @@ org.springframework.boot spring-boot-dependencies - ${spring.boot-version} + ${spring.boot.version} pom import diff --git a/services/save-and-restore/pom.xml b/services/save-and-restore/pom.xml index d0e5a2360e..df45fbee11 100644 --- a/services/save-and-restore/pom.xml +++ b/services/save-and-restore/pom.xml @@ -13,7 +13,6 @@ 5.0.3-SNAPSHOT - 2.7.3 4.4 8.2.0 @@ -46,11 +45,6 @@ spring-boot-starter-data-elasticsearch - - org.springframework.boot - spring-boot-starter-websocket - - co.elastic.clients elasticsearch-java @@ -88,9 +82,8 @@ org.phoebus - core-websocket + core-websocket-common 5.0.3-SNAPSHOT - test diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/application/Application.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/application/Application.java index 5242d9bd27..907748ea15 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/application/Application.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/application/Application.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.ArrayList; @@ -34,7 +35,8 @@ /** * Save-and-restore service main class. */ -@SpringBootApplication(scanBasePackages = "org.phoebus.service.saveandrestore") +@SpringBootApplication +@ComponentScan(basePackages = {"org.phoebus.service.saveandrestore"}) @EnableScheduling @EnableAutoConfiguration public class Application { diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java index bb10c87ca2..63fe8a2dc1 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebConfiguration.java @@ -20,15 +20,14 @@ import org.phoebus.saveandrestore.util.SnapshotUtil; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocket; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.Scope; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; +import javax.servlet.ServletContext; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -42,9 +41,12 @@ public class WebConfiguration { @Value("${connection.timeout:5000}") public long connectionTimeout; + @Autowired + private ServletContext servletContext; + @SuppressWarnings("unused") @Bean - public long getConnectionTimeout(){ + public long getConnectionTimeout() { return connectionTimeout; } @@ -71,20 +73,23 @@ public AcceptHeaderResolver acceptHeaderResolver() { @SuppressWarnings("unused") @Bean @Scope("singleton") - public SnapshotUtil snapshotRestorer(){ + public SnapshotUtil snapshotRestorer() { return new SnapshotUtil(); } @SuppressWarnings("unused") @Bean - public ExecutorService executorService(){ + public ExecutorService executorService() { return Executors.newCachedThreadPool(); } - @SuppressWarnings("unused") - @Bean(name = "sockets") - @Scope("singleton") - public List getSockets() { - return new CopyOnWriteArrayList<>(); + /** + * + * @return The context path if other than "/", otherwise empty string. + */ + @Bean + public String context() { + return servletContext.getContextPath().length() > 1 ? + servletContext.getContextPath() : ""; } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java index 5fd64d7e46..2d23143353 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java @@ -19,11 +19,15 @@ package org.phoebus.service.saveandrestore.web.controllers; -import org.phoebus.applications.saveandrestore.model.*; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; @@ -48,13 +52,16 @@ public class CompositeSnapshotController extends BaseController { private NodeDAO nodeDAO; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; + + /** * Creates a new {@link CompositeSnapshot} {@link Node}. - * @param parentNodeId Valid id of the {@link Node}s intended parent. + * + * @param parentNodeId Valid id of the {@link Node}s intended parent. * @param compositeSnapshot {@link CompositeSnapshot} data. - * @param principal User {@link Principal} injected by Spring. + * @param principal User {@link Principal} injected by Spring. * @return The new {@link CompositeSnapshot}. */ @PutMapping(value = "/composite-snapshot", produces = JSON) @@ -62,31 +69,32 @@ public class CompositeSnapshotController extends BaseController { public CompositeSnapshot createCompositeSnapshot(@RequestParam(value = "parentNodeId") String parentNodeId, @RequestBody CompositeSnapshot compositeSnapshot, Principal principal) { - if(!compositeSnapshot.getCompositeSnapshotNode().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)){ + if (!compositeSnapshot.getCompositeSnapshotNode().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)) { throw new IllegalArgumentException("Composite snapshot node of wrong type"); } compositeSnapshot.getCompositeSnapshotNode().setUserName(principal.getName()); CompositeSnapshot newCompositeSnapshot = nodeDAO.createCompositeSnapshot(parentNodeId, compositeSnapshot); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newCompositeSnapshot.getCompositeSnapshotNode().getUniqueId())); + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_ADDED, compositeSnapshot.getCompositeSnapshotNode().getUniqueId())); return newCompositeSnapshot; } /** * Updates/overwrites a {@link CompositeSnapshot} {@link Node}. + * * @param compositeSnapshot {@link CompositeSnapshot} data. - * @param principal User {@link Principal} injected by Spring. + * @param principal User {@link Principal} injected by Spring. * @return The new {@link CompositeSnapshot}. */ @PostMapping(value = "/composite-snapshot", produces = JSON) @PreAuthorize("@authorizationHelper.mayUpdate(#compositeSnapshot, #root)") public CompositeSnapshot updateCompositeSnapshot(@RequestBody CompositeSnapshot compositeSnapshot, Principal principal) { - if(!compositeSnapshot.getCompositeSnapshotNode().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)){ + if (!compositeSnapshot.getCompositeSnapshotNode().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)) { throw new IllegalArgumentException("Composite snapshot node of wrong type"); } compositeSnapshot.getCompositeSnapshotNode().setUserName(principal.getName()); CompositeSnapshot updatedCompositeSnapshot = nodeDAO.updateCompositeSnapshot(compositeSnapshot); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedCompositeSnapshot.getCompositeSnapshotNode())); + webSocketService.sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_UPDATED, updatedCompositeSnapshot.getCompositeSnapshotNode())); return updatedCompositeSnapshot; } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java index 45a0bc52d3..671fde1032 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java @@ -21,10 +21,10 @@ import org.phoebus.applications.saveandrestore.model.Configuration; import org.phoebus.applications.saveandrestore.model.ConfigurationData; import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; @@ -49,15 +49,15 @@ public class ConfigurationController extends BaseController { @Autowired private NodeDAO nodeDAO; - @SuppressWarnings("unused") @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; /** * Creates new {@link Configuration} {@link Node}. - * @param parentNodeId Valid id of the {@link Node}s intended parent. + * + * @param parentNodeId Valid id of the {@link Node}s intended parent. * @param configuration {@link Configuration} data. - * @param principal User {@link Principal} injected by Spring. + * @param principal User {@link Principal} injected by Spring. * @return The new {@link Configuration}. */ @SuppressWarnings("unused") @@ -66,25 +66,26 @@ public class ConfigurationController extends BaseController { public Configuration createConfiguration(@RequestParam(value = "parentNodeId") String parentNodeId, @RequestBody Configuration configuration, Principal principal) { - for(ConfigPv configPv : configuration.getConfigurationData().getPvList()){ + for (ConfigPv configPv : configuration.getConfigurationData().getPvList()) { // Compare mode is set, verify tolerance is non-null - if(configPv.getComparison() != null && (configPv.getComparison().getComparisonMode() == null || configPv.getComparison().getTolerance() == null)){ + if (configPv.getComparison() != null && (configPv.getComparison().getComparisonMode() == null || configPv.getComparison().getTolerance() == null)) { throw new IllegalArgumentException("PV item \"" + configPv.getPvName() + "\" specifies comparison but no comparison or tolerance value"); } // Tolerance is set... - if(configPv.getComparison() != null && configPv.getComparison().getTolerance() < 0){ + if (configPv.getComparison() != null && configPv.getComparison().getTolerance() < 0) { //Tolerance is less than zero, which does not make sense as comparison considers tolerance as upper and lower limit. throw new IllegalArgumentException("PV item \"" + configPv.getPvName() + "\" specifies zero tolerance"); - } + } } configuration.getConfigurationNode().setUserName(principal.getName()); Configuration newConfiguration = nodeDAO.createConfiguration(parentNodeId, configuration); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newConfiguration.getConfigurationNode().getUniqueId())); + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_ADDED, newConfiguration.getConfigurationNode().getUniqueId())); return newConfiguration; } /** * Retrieves data associated with a {@link Configuration} {@link Node}-. + * * @param uniqueId unique {@link Node} id of a {@link Configuration}. * @return A {@link ConfigurationData} object. */ @@ -96,8 +97,9 @@ public ConfigurationData getConfigurationData(@PathVariable String uniqueId) { /** * Updates/overwrites an existing {@link Configuration} + * * @param configuration The {@link Configuration} subject to update. - * @param principal User {@link Principal} injected by Spring. + * @param principal User {@link Principal} injected by Spring. * @return The updated {@link Configuration}. */ @SuppressWarnings("unused") @@ -107,7 +109,7 @@ public Configuration updateConfiguration(@RequestBody Configuration configuratio Principal principal) { configuration.getConfigurationNode().setUserName(principal.getName()); Configuration updatedConfiguration = nodeDAO.updateConfiguration(configuration); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedConfiguration.getConfigurationNode())); + webSocketService.sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_UPDATED, updatedConfiguration.getConfigurationNode())); return updatedConfiguration; } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java index d06e120eef..c99b96ec28 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java @@ -20,10 +20,10 @@ package org.phoebus.service.saveandrestore.web.controllers; import org.phoebus.applications.saveandrestore.model.search.Filter; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -47,7 +47,7 @@ public class FilterController extends BaseController { private NodeDAO nodeDAO; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; /** * Saves a new or updated {@link Filter}. @@ -63,7 +63,7 @@ public Filter saveFilter(@RequestBody final Filter filter, Principal principal) { filter.setUser(principal.getName()); Filter savedFilter = nodeDAO.saveFilter(filter); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.FILTER_ADDED_OR_UPDATED, filter)); + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.FILTER_ADDED_OR_UPDATED, filter)); return savedFilter; } @@ -87,6 +87,6 @@ public List getAllFilters() { @PreAuthorize("@authorizationHelper.maySaveOrDeleteFilter(#name, #root)") public void deleteFilter(@PathVariable final String name, Principal principal) { nodeDAO.deleteFilter(name); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.FILTER_REMOVED, name)); + webSocketService.sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.FILTER_REMOVED, name)); } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java index 41005b1185..87281910cd 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java @@ -20,10 +20,10 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Tag; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -51,8 +51,7 @@ public class NodeController extends BaseController { private NodeDAO nodeDAO; @Autowired - private WebSocketHandler webSocketHandler; - + private WebSocketService webSocketService; /** * Create a new folder in the tree structure. @@ -84,7 +83,7 @@ public Node createNode(@RequestParam(name = "parentNodeId") String parentsUnique } node.setUserName(principal.getName()); Node savedNode = nodeDAO.createNode(parentsUniqueId, node); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_ADDED, + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_ADDED, savedNode.getUniqueId())); return savedNode; } @@ -159,7 +158,7 @@ public List getChildNodes(@PathVariable final String uniqueNodeId) { public void deleteNodes(@RequestBody List nodeIds) { nodeDAO.deleteNodes(nodeIds); nodeIds.forEach(id -> - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, id))); + webSocketService.sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_REMOVED, id))); } /** @@ -221,7 +220,7 @@ public Node updateNode(@RequestParam(value = "customTimeForMigration", required } nodeToUpdate.setUserName(principal.getName()); Node updatedNode = nodeDAO.updateNode(nodeToUpdate, Boolean.parseBoolean(customTimeForMigration)); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedNode)); + webSocketService.sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_UPDATED, updatedNode)); return updatedNode; } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java index 5f9ed63217..6e124b9780 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java @@ -21,13 +21,19 @@ import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.model.SnapshotData; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.List; @@ -43,7 +49,8 @@ public class SnapshotController extends BaseController { private NodeDAO nodeDAO; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; + /** * * @param uniqueId Unique {@link Node} id of a snapshot. @@ -65,28 +72,30 @@ public List getAllSnapshots() { /** * Creates a new {@link Snapshot} + * * @param parentNodeId Unique {@link Node} id of the new {@link Snapshot}. - * @param snapshot {@link Snapshot} data. - * @param principal User {@link Principal} as injected by Spring. + * @param snapshot {@link Snapshot} data. + * @param principal User {@link Principal} as injected by Spring. * @return The new {@link Snapshot}. */ @PutMapping(value = "/snapshot", produces = JSON) @PreAuthorize("@authorizationHelper.mayCreate(#root)") public Snapshot createSnapshot(@RequestParam(value = "parentNodeId") String parentNodeId, - @RequestBody Snapshot snapshot, - Principal principal) { - if(!snapshot.getSnapshotNode().getNodeType().equals(NodeType.SNAPSHOT)){ + @RequestBody Snapshot snapshot, + Principal principal) { + if (!snapshot.getSnapshotNode().getNodeType().equals(NodeType.SNAPSHOT)) { throw new IllegalArgumentException("Snapshot node of wrong type"); } snapshot.getSnapshotNode().setUserName(principal.getName()); Snapshot newSnapshot = nodeDAO.createSnapshot(parentNodeId, snapshot); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newSnapshot.getSnapshotNode().getUniqueId())); + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_ADDED, newSnapshot.getSnapshotNode().getUniqueId())); return newSnapshot; } /** * Updates a {@link Snapshot}. - * @param snapshot The {@link Snapshot} subject to update. + * + * @param snapshot The {@link Snapshot} subject to update. * @param principal User {@link Principal} as injected by Spring. * @return The updated {@link Snapshot} */ @@ -94,12 +103,12 @@ public Snapshot createSnapshot(@RequestParam(value = "parentNodeId") String pare @PreAuthorize("@authorizationHelper.mayUpdate(#snapshot, #root)") public Snapshot updateSnapshot(@RequestBody Snapshot snapshot, Principal principal) { - if(!snapshot.getSnapshotNode().getNodeType().equals(NodeType.SNAPSHOT)){ + if (!snapshot.getSnapshotNode().getNodeType().equals(NodeType.SNAPSHOT)) { throw new IllegalArgumentException("Snapshot node of wrong type"); } snapshot.getSnapshotNode().setUserName(principal.getName()); Snapshot updatedSnapshot = nodeDAO.updateSnapshot(snapshot); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedSnapshot.getSnapshotNode())); + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_UPDATED, updatedSnapshot.getSnapshotNode())); return updatedSnapshot; } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java index cf90c4714a..e5fb1a0c63 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java @@ -18,14 +18,19 @@ package org.phoebus.service.saveandrestore.web.controllers; import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import java.security.Principal; @@ -45,7 +50,7 @@ public class StructureController extends BaseController { private NodeDAO nodeDAO; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; /** * Moves a list of source nodes to a new target (parent) node. @@ -71,9 +76,9 @@ public Node moveNodes(@RequestParam(value = "to") String to, Logger.getLogger(StructureController.class.getName()).info(Thread.currentThread().getName() + " " + (new Date()) + " move"); Node targetNode = nodeDAO.moveNodes(nodes, to, principal.getName()); // Update clients - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_UPDATED, + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_UPDATED, targetNode)); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_UPDATED, + webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_UPDATED, sourceParentNode)); return targetNode; } @@ -101,7 +106,7 @@ public Node copyNodes(@RequestParam(value = "to") String to, Logger.getLogger(StructureController.class.getName()).info(Thread.currentThread().getName() + " " + (new Date()) + " copy"); Node targetNode = nodeDAO.getNode(to); List newNodes = nodeDAO.copyNodes(nodes, to, principal.getName()); - newNodes.forEach(n -> webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_ADDED, n.getUniqueId()))); + newNodes.forEach(n -> webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_ADDED, n.getUniqueId()))); return targetNode; } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java index cb4b306a20..d9b87093a3 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java @@ -24,10 +24,10 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.Tag; import org.phoebus.applications.saveandrestore.model.TagData; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -35,7 +35,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.socket.WebSocketMessage; import java.security.Principal; import java.util.List; @@ -54,7 +53,7 @@ public class TagController extends BaseController { private NodeDAO nodeDAO; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; /** * @return A {@link List} of all {@link Tag}s. @@ -78,7 +77,7 @@ public List addTag(@RequestBody TagData tagData, Principal principal) { tagData.getTag().setUserName(principal.getName()); List taggedNodes = nodeDAO.addTag(tagData); - taggedNodes.forEach(n -> webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, n))); + taggedNodes.forEach(n -> webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_UPDATED, n))); return taggedNodes; } @@ -93,7 +92,7 @@ public List addTag(@RequestBody TagData tagData, @PreAuthorize("@authorizationHelper.mayAddOrDeleteTag(#tagData, #root)") public List deleteTag(@RequestBody TagData tagData) { List untaggedNodes = nodeDAO.deleteTag(tagData); - untaggedNodes.forEach(n -> webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, n))); + untaggedNodes.forEach(n -> webSocketService.sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_UPDATED, n))); return untaggedNodes; } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocket.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocket.java deleted file mode 100644 index c97e944c16..0000000000 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocket.java +++ /dev/null @@ -1,212 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2019-2022 UT-Battelle, LLC. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the LICENSE - * which accompanies this distribution - ******************************************************************************/ -package org.phoebus.service.saveandrestore.websocket; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.web.socket.PingMessage; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.time.Instant; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Utility class for handling web socket messages. - */ -@SuppressWarnings("nls") -public class WebSocket { - - /** - * Is the queue full? - */ - private final AtomicBoolean stuffed = new AtomicBoolean(); - - /** - * Queue of messages for the client. - * - *

Multiple threads concurrently writing to the socket results in - * IllegalStateException "remote endpoint was in state [TEXT_FULL_WRITING]" - * All writes are thus performed by just one thread off this queue. - */ - private final ArrayBlockingQueue writeQueue = new ArrayBlockingQueue<>(2048); - - private static final String EXIT_MESSAGE = "EXIT"; - - private final WebSocketSession session; - private final String id; - private final String description; - private final Logger logger = Logger.getLogger(WebSocket.class.getName()); - private final ObjectMapper objectMapper; - - /** - * Keeps track of when this session was used for a ping/pong exchange. Should be set to non-null value ONLY - * when an actual pong was received by {@link WebSocketHandler}. - */ - private Instant lastPinged; - - /** - * Constructor - */ - public WebSocket(ObjectMapper objectMapper, WebSocketSession webSocketSession) { - this.session = webSocketSession; - logger.log(Level.INFO, () -> "Creating web socket " + session.getUri() + " ID " + session.getId()); - this.objectMapper = objectMapper; - this.id = webSocketSession.getId(); - Thread writeThread = new Thread(this::writeQueuedMessages, "Web Socket Write Thread"); - writeThread.setName("Web Socket Write Thread " + this.id); - writeThread.setDaemon(true); - writeThread.start(); - InetSocketAddress inetSocketAddress = webSocketSession.getRemoteAddress(); - this.description = this.id + "/" + (inetSocketAddress != null ? inetSocketAddress.getAddress().toString() : "IP address unknown"); - } - - /** - * @return Session ID - */ - public String getId() { - if (session == null) - return "(" + id + ")"; - else - return id; - } - - /** - * - * @return A description containing the session ID and - if available - the associated IP address. - */ - public String getDescription() { - return description; - } - - /** - * @param message Potentially long message - * @return Message shorted to 200 chars - */ - private String shorten(final String message) { - if (message == null || message.length() < 200) - return message; - return message.substring(0, 200) + " ..."; - } - - public void queueMessage(final String message) { - // Ignore messages after 'dispose' - if (session == null) - return; - - if (writeQueue.offer(message)) { // Queued OK. Is this a recovery from stuffed queue? - if (stuffed.getAndSet(false)) - logger.log(Level.WARNING, () -> "Un-stuffed message queue for " + id); - } else { // Log, but only for the first message to prevent flooding the log - if (!stuffed.getAndSet(true)) - logger.log(Level.WARNING, () -> "Cannot queue message '" + shorten(message) + "' for " + id); - } - } - - private void writeQueuedMessages() { - try { - while (true) { - final String message; - try { - message = writeQueue.take(); - } catch (final InterruptedException ex) { - return; - } - - // Check if we should exit the thread - if (message.equals(EXIT_MESSAGE)) { - logger.log(Level.FINE, () -> "Exiting write thread " + id); - return; - } - - final WebSocketSession safeSession = session; - try { - if (safeSession == null) - throw new Exception("No session"); - if (!safeSession.isOpen()) - throw new Exception("Session closed"); - safeSession.sendMessage(new TextMessage(message)); - } catch (final Exception ex) { - logger.log(Level.WARNING, ex, () -> "Cannot write '" + shorten(message) + "' for " + id); - - // Clear queue - String drop = writeQueue.take(); - while (drop != null) { - if (drop.equals(EXIT_MESSAGE)) { - logger.log(Level.FINE, () -> "Exiting write thread " + id); - return; - } - drop = writeQueue.take(); - } - } - } - } catch (Throwable ex) { - logger.log(Level.WARNING, "Write thread error for " + id, ex); - } - } - - /** - * Called when client sends a generic message - * - * @param message {@link TextMessage}, its payload is expected to be JSON. - */ - public void handleTextMessage(TextMessage message) throws Exception { - final JsonNode json = objectMapper.readTree(message.getPayload()); - final JsonNode node = json.path("type"); - if (node.isMissingNode()) - throw new Exception("Missing 'type' in " + shorten(message.getPayload())); - final String type = node.asText(); - logger.log(Level.INFO, "Client message type: " + type); - } - - public void dispose() { - // Exit write thread - try { - // Drop queued messages (which might be stuffed): - // We're closing and just need the EXIT_MESSAGE - writeQueue.clear(); - queueMessage(EXIT_MESSAGE); - // TODO: is this needed? - session.close(); - } catch (Throwable ex) { - logger.log(Level.WARNING, "Error disposing " + description, ex); - } - logger.log(Level.INFO, () -> "Web socket " + description + " closed"); - } - - /** - * Sets the time of last received pong message. - * @param instant Time of last received pong message. - */ - public synchronized void setLastPinged(Instant instant) { - this.lastPinged = instant; - } - - /** - * - * @return The time of last received pong message. - */ - public synchronized Instant getLastPinged() { - return lastPinged; - } - - /** - * Sends a {@link PingMessage} to peer. - */ - public void sendPing() { - try { - session.sendMessage(new PingMessage()); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to send ping message", e); - } - } -} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketConfig.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketConfig.java index f0e89df46d..28e6153357 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketConfig.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketConfig.java @@ -18,33 +18,49 @@ package org.phoebus.service.saveandrestore.websocket; import com.fasterxml.jackson.databind.ObjectMapper; +import org.phoebus.core.websocket.common.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.context.annotation.Lazy; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; -import java.util.List; +import java.util.logging.Level; import java.util.logging.Logger; @SuppressWarnings("unused") @Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Autowired public ObjectMapper objectMapper; @Autowired - private List sockets; + private String context; + + private final Logger logger = Logger.getLogger(WebSocketConfig.class.getName()); + + private TaskScheduler messageBrokerTaskScheduler; @Autowired - private WebSocketHandler webSocketHandler; + public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) { + this.messageBrokerTaskScheduler = taskScheduler; + } - private final Logger logger = Logger.getLogger(WebSocketConfig.class.getName()); + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + logger.log(Level.INFO, "Configuring message broker using path " + context + Constants.WEB_SOCKET + Constants.MESSAGES); + config.enableSimpleBroker(context + Constants.WEB_SOCKET + Constants.MESSAGES) + .setHeartbeatValue(new long[]{30000, 30000}) + .setTaskScheduler(this.messageBrokerTaskScheduler); + } @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(webSocketHandler, "/web-socket"); + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint(Constants.WEB_SOCKET); } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketHandler.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketHandler.java deleted file mode 100644 index a539f573f7..0000000000 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketHandler.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - * - */ - -package org.phoebus.service.saveandrestore.websocket; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.lang.NonNull; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.PongMessage; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -import javax.annotation.PreDestroy; -import java.io.EOFException; -import java.net.InetSocketAddress; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Single web socket end-point routing messages to active {@link WebSocket} instances. - * - *

- * In some cases web socket clients may become stale/disconnected for various reasons, e.g. network issues. The - * {@link #afterConnectionClosed(WebSocketSession, CloseStatus)} is not necessarily called in those case. - * To make sure the {@link #sockets} collection does not contain stale clients, a scheduled job runs once per hour to - * ping all clients, and set the time when the pong response was received. Another scheduled job will check - * the last received pong message timestamp and - if older than 70 minutes - consider the client session dead - * and dispose of it. - *

- */ -@Component -public class WebSocketHandler extends TextWebSocketHandler { - - /** - * List of active {@link WebSocket} - */ - @SuppressWarnings("unused") - private List sockets = Collections.synchronizedList(new ArrayList<>()); - - @SuppressWarnings("unused") - @Autowired - private ObjectMapper objectMapper; - - @SuppressWarnings("unused") - - private final Logger logger = Logger.getLogger(WebSocketHandler.class.getName()); - - /** - * Handles text message from web socket client - * - * @param session The {@link WebSocketSession} associated with the remote client. - * @param message Message sent by client - */ - @Override - public void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) { - try { - // Find the WebSocket instance associated with the WebSocketSession - Optional webSocketOptional; - synchronized (sockets){ - webSocketOptional = - sockets.stream().filter(webSocket -> webSocket.getId().equals(session.getId())).findFirst(); - } - if (webSocketOptional.isEmpty()) { - return; // Should only happen in case of timing issues? - } - webSocketOptional.get().handleTextMessage(message); - } catch (final Exception ex) { - logger.log(Level.WARNING, ex, () -> "Error for message " + shorten(message.getPayload())); - } - } - - /** - * Called when client connects. - * - * @param session Associated {@link WebSocketSession} - */ - @Override - public void afterConnectionEstablished(@NonNull WebSocketSession session) { - InetSocketAddress inetSocketAddress = session.getRemoteAddress(); - logger.log(Level.INFO, "Opening web socket session from remote " + (inetSocketAddress != null ? inetSocketAddress.getAddress().toString() : "")); - WebSocket webSocket = new WebSocket(objectMapper, session); - sockets.add(webSocket); - } - - /** - * Called when web socket is closed. Depending on the web browser, {@link #handleTransportError(WebSocketSession, Throwable)} - * may be called first. - * - * @param session Associated {@link WebSocketSession} - * @param status See {@link CloseStatus} - */ - @Override - public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) { - Optional webSocketOptional; - synchronized (sockets){ - webSocketOptional = sockets.stream().filter(webSocket -> webSocket.getId().equals(session.getId())).findFirst(); - } - if (webSocketOptional.isPresent()) { - logger.log(Level.INFO, "Closing web socket session " + webSocketOptional.get().getDescription()); - webSocketOptional.get().dispose(); - sockets.remove(webSocketOptional.get()); - } - } - - /** - * Depending on the web browser, this is called before {@link #afterConnectionClosed(WebSocketSession, CloseStatus)} - * when tab or browser is closes. - * - * @param session Associated {@link WebSocketSession} - * @param ex {@link Throwable} that should indicate reason - */ - @Override - public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable ex) { - if (ex instanceof EOFException) - logger.log(Level.FINE, "Web Socket closed", ex); - else - logger.log(Level.WARNING, "Web Socket error", ex); - } - - /** - * Called when client sends ping message, i.e. a pong message is sent and time for last pong response message - * in the {@link WebSocket} instance is refreshed. - * - * @param session Associated {@link WebSocketSession} - * @param message See {@link PongMessage} - */ - @Override - protected void handlePongMessage(@NonNull WebSocketSession session, @NonNull PongMessage message) { - logger.log(Level.FINE, "Got pong for session " + session.getId()); - // Find the WebSocket instance associated with this WebSocketSession - Optional webSocketOptional; - synchronized (sockets) { - webSocketOptional = sockets.stream().filter(webSocket -> webSocket.getId().equals(session.getId())).findFirst(); - } - if (webSocketOptional.isPresent()) { - webSocketOptional.get().setLastPinged(Instant.now()); - } - } - - /** - * @param message Potentially long message - * @return Message shorted to 200 chars - */ - private String shorten(final String message) { - if (message == null || message.length() < 200) - return message; - return message.substring(0, 200) + " ..."; - } - - @PreDestroy - public void cleanup() { - synchronized (sockets) { - sockets.forEach(s -> { - logger.log(Level.INFO, "Disposing socket " + s.getDescription()); - s.dispose(); - }); - } - } - - public void sendMessage(SaveAndRestoreWebSocketMessage webSocketMessage) { - synchronized (sockets) { - sockets.forEach(ws -> { - try { - ws.queueMessage(objectMapper.writeValueAsString(webSocketMessage)); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - } - } - - /** - * Sends a ping message to all clients contained in {@link #sockets}. - *

- * This is scheduled to run at the top of each hour, i.e. 00.00, 01.00...23.00 - *

- * - */ - @SuppressWarnings("unused") - @Scheduled(cron = "* 0 * * * *") - public void pingClients(){ - synchronized (sockets) { - sockets.forEach(WebSocket::sendPing); - } - } - - /** - * For each client in {@link #sockets}, checks the timestamp of last received pong message. If this is older - * than 70 minutes, the socket is considered dead, and then disposed. - *

- * This is scheduled to run 5 minutes past each hour, i.e. 00.05, 01.05...23.05 - *

- * - */ - @SuppressWarnings("unused") - @Scheduled(cron = "* 5 * * * *") - public void cleanUpDeadSockets(){ - List deadSockets = new ArrayList<>(); - Instant now = Instant.now(); - synchronized (sockets) { - sockets.forEach(s -> { - Instant lastPinged = s.getLastPinged(); - if (lastPinged != null && lastPinged.isBefore(now.minus(70, ChronoUnit.MINUTES))) { - deadSockets.add(s); - } - }); - deadSockets.forEach(d -> { - sockets.remove(d); - d.dispose(); - }); - } - } -} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketService.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketService.java new file mode 100644 index 0000000000..e13252fb8d --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketService.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.phoebus.service.saveandrestore.websocket; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.user.SimpUserRegistry; +import org.springframework.stereotype.Service; + +import org.phoebus.core.websocket.common.WebSocketMessage; + + +import java.util.logging.Level; +import java.util.logging.Logger; + +@Service +public class WebSocketService { + + @SuppressWarnings("unused") + @Autowired + private SimpMessagingTemplate simpMessagingTemplate; + + @SuppressWarnings("unused") + @Autowired + private SimpUserRegistry simpUserRegistry; + + @SuppressWarnings("unused") + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private String context; + + private static final Logger logger = Logger.getLogger(WebSocketService.class.getName()); + + /** + * @param webSocketMessage Non-null {@link WebSocketMessage}, will be converted to a JSON string before + * it is dispatched to clients. + */ + public void sendMessageToClients(@NonNull WebSocketMessage webSocketMessage) { + try { + String message = objectMapper.writeValueAsString(webSocketMessage); + simpMessagingTemplate.convertAndSend(context + "/web-socket/messages", message); + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Failed to write web socket message to json string", e); + } + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java index cfb5058ecd..a6e5bc57af 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java @@ -27,22 +27,24 @@ import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.FilterRepository; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.SnapshotDataRepository; import org.phoebus.service.saveandrestore.search.SearchUtil; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.user.SimpUserRegistry; +import org.springframework.mock.web.MockServletContext; import org.springframework.util.Base64Utils; import org.springframework.web.socket.WebSocketSession; +import javax.servlet.ServletContext; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -@SpringBootConfiguration +@TestConfiguration @ComponentScan(basePackages = "org.phoebus.service.saveandrestore.web.controllers") -@Import(WebSecurityConfig.class) @SuppressWarnings("unused") @Profile("!IT") public class ControllersTestConfig { @@ -133,18 +135,41 @@ public SnapshotUtil snapshotUtil() { } @Bean - public WebSocketSession webSocketSession(){ + public WebSocketSession webSocketSession() { return Mockito.mock(WebSocketSession.class); } @Bean - public WebSocketHandler webSocketHandler(){ - return Mockito.mock(WebSocketHandler.class); + public WebSocketService webSocketService() { + return Mockito.mock(WebSocketService.class); } @Bean - public long connectionTimeout(){ + public SimpMessagingTemplate simpMessagingTemplate() { + return Mockito.mock(SimpMessagingTemplate.class); + } + + @Bean + public SimpUserRegistry simpUserRegistry() { + return Mockito.mock(SimpUserRegistry.class); + } + + @Bean + public long connectionTimeout() { return 5000; } + @Bean + public ServletContext servletContext() { + MockServletContext mockServletContext = new MockServletContext(); + mockServletContext.setContextPath("/"); + return mockServletContext; + } + + @Bean + public String context() { + return servletContext().getContextPath().length() > 1 ? + servletContext().getContextPath() : ""; + } + } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java index da1005650f..67633bc4df 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java @@ -29,7 +29,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) -@ContextHierarchy({@ContextConfiguration(classes = {WebConfiguration.class, ControllersTestConfig.class})}) +@ContextHierarchy({@ContextConfiguration(classes = {WebConfiguration.class, ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application.properties") @SuppressWarnings("unused") public class WebConfigTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java index d49f0912cf..142d9e90da 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -40,7 +41,7 @@ * @author georgweiss * Created 16 May 2019 */ -@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class})}) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(AppMetaDataControllerTest.class) @ExtendWith(SpringExtension.class) @TestPropertySource(locations = "classpath:test_application.properties") diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java index 5afdf55862..99f8a7298c 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ComparisonControllerTest.java @@ -22,9 +22,11 @@ import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -43,7 +45,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application.properties") @WebMvcTest(NodeController.class) public class ComparisonControllerTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java index 19cec3db71..eacf21be4c 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java @@ -30,10 +30,12 @@ import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -51,7 +53,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(CompositeSnapshotController.class) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") public class CompositeSnapshotControllerPermitAllTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java index 1eb9fdfd21..a11883801d 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -32,14 +31,16 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.SnapshotItem; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -60,7 +61,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(CompositeSnapshotController.class) @TestPropertySource(locations = "classpath:test_application.properties") public class CompositeSnapshotControllerTest { @@ -84,7 +85,7 @@ public class CompositeSnapshotControllerTest { private MockMvc mockMvc; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -101,8 +102,8 @@ public static void init() { } @AfterEach - public void resetMocks(){ - reset(nodeDAO, webSocketHandler); + public void resetMocks() { + reset(nodeDAO, webSocketService); } @Test @@ -124,7 +125,7 @@ public void testCreateCompositeSnapshot1() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshot.class); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -141,7 +142,7 @@ public void testCreateCompositeSnapshot2() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -156,7 +157,7 @@ public void testCreateCompositeSnapshot3() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -170,12 +171,12 @@ public void testCreateCompositeSnapshot4() throws Exception { mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testCreateCompositeSnapshotWrongNodeType() throws Exception{ + public void testCreateCompositeSnapshotWrongNodeType() throws Exception { Node node = Node.builder().uniqueId("c").nodeType(NodeType.SNAPSHOT).build(); CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); compositeSnapshot1.setCompositeSnapshotNode(node); @@ -186,7 +187,7 @@ public void testCreateCompositeSnapshotWrongNodeType() throws Exception{ .content(objectMapper.writeValueAsString(compositeSnapshot1)); mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -213,7 +214,7 @@ public void testUpdateCompositeSnapshot1() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshot.class); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -234,7 +235,7 @@ public void testUpdateCompositeSnapshot2() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -261,7 +262,7 @@ public void testUpdateCompositeSnapshot3() throws Exception { mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -284,11 +285,11 @@ public void testUpdateCompositeSnapshot4() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testUpdateCompositeSnapshotWrongNodeType() throws Exception{ + public void testUpdateCompositeSnapshotWrongNodeType() throws Exception { Node node = Node.builder().uniqueId("c").userName(demoUser).nodeType(NodeType.SNAPSHOT).build(); CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); compositeSnapshot1.setCompositeSnapshotNode(node); @@ -301,7 +302,7 @@ public void testUpdateCompositeSnapshotWrongNodeType() throws Exception{ .content(objectMapper.writeValueAsString(compositeSnapshot1)); mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java index ebcdbf45ef..d6d6e64b9b 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java @@ -30,10 +30,12 @@ import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -49,7 +51,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(ConfigurationController.class) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") public class ConfigurationControllerPermitAllTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java index ecfc9de044..32e517f171 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java @@ -23,27 +23,24 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.mockito.Mockito; import org.phoebus.applications.saveandrestore.model.Comparison; +import org.phoebus.applications.saveandrestore.model.ComparisonMode; import org.phoebus.applications.saveandrestore.model.ConfigPv; - import org.phoebus.applications.saveandrestore.model.Configuration; import org.phoebus.applications.saveandrestore.model.ConfigurationData; import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; - -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; - -import org.phoebus.applications.saveandrestore.model.ComparisonMode; - +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -62,7 +59,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(ConfigurationController.class) @TestPropertySource(locations = "classpath:test_application.properties") public class ConfigurationControllerTest { @@ -89,11 +86,11 @@ public class ConfigurationControllerTest { private String demoUser; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; @AfterEach - public void resetMocks(){ - reset(nodeDAO, webSocketHandler); + public void resetMocks() { + reset(nodeDAO, webSocketService); } @Test @@ -114,7 +111,7 @@ public void testCreateConfiguration1() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -135,7 +132,7 @@ public void testCreateConfiguration2() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -151,11 +148,11 @@ public void testCreateConfiguration3() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testCreateConfiguration4() throws Exception{ + public void testCreateConfiguration4() throws Exception { Configuration configuration = new Configuration(); configuration.setConfigurationNode(Node.builder().build()); @@ -165,7 +162,7 @@ public void testCreateConfiguration4() throws Exception{ mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -186,7 +183,7 @@ public void testUpdateConfiguration1() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -207,7 +204,7 @@ public void tesUpdateConfiguration2() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -227,7 +224,7 @@ public void testUpdateConfiguration3() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -259,11 +256,11 @@ public void testUpdateConfiguration5() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testUpdateConfiguration6() throws Exception{ + public void testUpdateConfiguration6() throws Exception { Configuration configuration = new Configuration(); Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); @@ -274,7 +271,7 @@ public void testUpdateConfiguration6() throws Exception{ mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); mockMvc.perform(request).andExpect(status().isUnauthorized()); } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java index 0864cf5f2a..37746e5b8c 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java @@ -26,10 +26,12 @@ import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -47,7 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(FilterController.class) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") public class FilterControllerPermitAllTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java index 3d268e201b..51258ebffe 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java @@ -25,14 +25,16 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.phoebus.applications.saveandrestore.model.search.Filter; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -47,12 +49,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(FilterController.class) @TestPropertySource(locations = "classpath:test_application.properties") public class FilterControllerTest { @@ -79,11 +83,11 @@ public class FilterControllerTest { private String demoUser; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; @AfterEach - public void resetMocks(){ - reset(webSocketHandler,nodeDAO); + public void resetMocks() { + reset(webSocketService, nodeDAO); } @Test @@ -110,7 +114,7 @@ public void testSaveFilter1() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, Filter.class); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -130,12 +134,12 @@ public void testSaveFilter2() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testSaveFiliter3() throws Exception { + public void testSaveFilter3() throws Exception { Filter filter = new Filter(); filter.setName("name"); @@ -150,12 +154,12 @@ public void testSaveFiliter3() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testSaveFiliter4() throws Exception{ + public void testSaveFilter4() throws Exception { Filter filter = new Filter(); filter.setName("name"); @@ -170,7 +174,7 @@ public void testSaveFiliter4() throws Exception{ mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @@ -188,7 +192,7 @@ public void testDeleteFilter() throws Exception { .contentType(JSON); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @@ -200,19 +204,19 @@ public void testDeleteFilter2() throws Exception { .contentType(JSON); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test public void testDeleteFilter3() throws Exception { - MockHttpServletRequestBuilder request = delete("/filter/name") - .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) - .contentType(JSON); - mockMvc.perform(request).andExpect(status().isForbidden()); + MockHttpServletRequestBuilder request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @@ -222,12 +226,12 @@ public void testDeleteFilter4() throws Exception { .contentType(JSON); mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testDeleteFilter5() throws Exception{ + public void testDeleteFilter5() throws Exception { Filter filter = new Filter(); filter.setName("name"); filter.setQueryString("query"); @@ -239,7 +243,7 @@ public void testDeleteFilter5() throws Exception{ .contentType(JSON); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java index 3843053690..995e84570a 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java @@ -22,9 +22,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -35,7 +37,7 @@ @ExtendWith(SpringExtension.class) @WebMvcTest(HelpResource.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application.properties") public class HelpResourceTest{ diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java index 1f591c298e..d646607194 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java @@ -26,10 +26,12 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -46,7 +48,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") @WebMvcTest(NodeController.class) /** diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java index 677225fd74..e0f4f8b6b2 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java @@ -34,16 +34,18 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Tag; -import org.phoebus.applications.saveandrestore.model.websocket.MessageType; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreMessageType; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.NodeNotFoundException; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -53,7 +55,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.reset; @@ -61,7 +62,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -73,7 +77,7 @@ * @author Georg Weiss, European Spallation Source */ @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application.properties") @WebMvcTest(NodeController.class) public class NodeControllerTest { @@ -85,7 +89,7 @@ public class NodeControllerTest { private MockMvc mockMvc; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; private static Node folderFromClient; @@ -118,8 +122,8 @@ public static void setUp() { } @AfterEach - public void resetMocks(){ - reset(webSocketHandler, nodeDAO); + public void resetMocks() { + reset(webSocketService, nodeDAO); } @Test @@ -254,7 +258,7 @@ public void testCreateConfigWithBadToleranceData1() throws Exception { .contentType(JSON) .content(objectMapper.writeValueAsString(configuration)); - mockMvc.perform(request).andExpect(status().isBadRequest()); + mockMvc.perform(request).andExpect(status().isBadRequest()); } @Test @@ -436,7 +440,7 @@ public void testDeleteFolder() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -450,7 +454,7 @@ public void testDeleteForbiddenAccess() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); + verify(webSocketService, times(0)).sendMessageToClients(new WebSocketMessage<>(SaveAndRestoreMessageType.NODE_REMOVED, "b")); } @Test @@ -465,12 +469,12 @@ public void testDeleteForbiddenAccess2() throws Exception { .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); + verify(webSocketService, times(0)).sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_REMOVED, "b")); when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build()); when(nodeDAO.getChildNodes("a")).thenReturn(Collections.emptyList()); - verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); + verify(webSocketService, times(0)).sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_REMOVED, "b")); } @@ -487,7 +491,7 @@ public void testDeleteFolder2() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @@ -504,11 +508,11 @@ public void testDeleteFolder3() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testDeleteForbidden3() throws Exception{ + public void testDeleteForbidden3() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build()); when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); @@ -519,11 +523,11 @@ public void testDeleteForbidden3() throws Exception{ .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); + verify(webSocketService, times(0)).sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_REMOVED, "b")); } @Test - public void testDeleteForbidden4() throws Exception{ + public void testDeleteForbidden4() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.FOLDER).userName(demoUser).build()); when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); @@ -534,12 +538,12 @@ public void testDeleteForbidden4() throws Exception{ .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); + verify(webSocketService, times(0)).sendMessageToClients(new WebSocketMessage(SaveAndRestoreMessageType.NODE_REMOVED, "b")); } @Test - public void testDeleteFolder5() throws Exception{ + public void testDeleteFolder5() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build()); when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java index 07ee27333e..05113d1fb4 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java @@ -30,10 +30,12 @@ import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.search.SearchUtil; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -53,7 +55,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(SearchController.class) @TestPropertySource(locations = "classpath:test_application.properties") public class SearchControllerTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java index a56b005c04..b1ef17fa87 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java @@ -29,10 +29,12 @@ import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -49,7 +51,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") @WebMvcTest(SnapshotController.class) public class SnapshotControllerPermitAllTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java index 8aaddec51c..9b956fe7a3 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java @@ -28,14 +28,16 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Snapshot; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -43,7 +45,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import java.util.List; -import java.util.Set; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -57,7 +58,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application.properties") @WebMvcTest(SnapshotController.class) public class SnapshotControllerTest { @@ -83,11 +84,11 @@ public class SnapshotControllerTest { private String demoUser; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; @AfterEach - public void resetMocks(){ - reset(webSocketHandler, nodeDAO); + public void resetMocks() { + reset(webSocketService, nodeDAO); } @@ -108,7 +109,7 @@ public void testSaveSnapshotWrongNodeType() throws Exception { mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -123,7 +124,7 @@ public void testSaveSnapshotNoParentNodeId() throws Exception { mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -149,7 +150,7 @@ public void testCreateSnapshot1() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -172,11 +173,11 @@ public void testCreateSnapshot2() throws Exception { .content(snapshotString); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testCreateSnapshot3() throws Exception{ + public void testCreateSnapshot3() throws Exception { Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); Snapshot snapshot = new Snapshot(); @@ -193,7 +194,7 @@ public void testCreateSnapshot3() throws Exception{ .content(snapshotString); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -219,7 +220,7 @@ public void testUpdateSnapshot1() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -236,7 +237,7 @@ public void testUpdateSnapshot2() throws Exception { .content(snapshotString); mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -254,11 +255,11 @@ public void testUpdateSnapshot3() throws Exception { .content(snapshotString); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testUpdateSnapshot4() throws Exception{ + public void testUpdateSnapshot4() throws Exception { Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); Snapshot snapshot = new Snapshot(); @@ -275,7 +276,7 @@ public void testUpdateSnapshot4() throws Exception{ .content(snapshotString); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -296,7 +297,7 @@ public void testDeleteSnapshot1() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -309,7 +310,7 @@ public void testDeleteSnapshot2() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -329,11 +330,11 @@ public void testDeleteSnapshot3() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(1)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testDeleteSnapshot4() throws Exception{ + public void testDeleteSnapshot4() throws Exception { MockHttpServletRequestBuilder request = delete("/node") @@ -341,6 +342,6 @@ public void testDeleteSnapshot4() throws Exception{ mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java index 5b15bacf46..1d7485b9da 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotRestorerControllerTest.java @@ -15,10 +15,12 @@ import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -34,7 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") @WebMvcTest(SnapshotRestoreController.class) public class SnapshotRestorerControllerTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java index c82b44e5ae..02344101a7 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java @@ -25,10 +25,12 @@ import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -44,7 +46,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") @WebMvcTest(StructureController.class) public class StructureControllerPermitAllTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java index 67eeb88386..bfc64a40fc 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java @@ -25,14 +25,16 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.core.websocket.common.WebSocketMessage; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; -import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -52,7 +54,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application.properties") @WebMvcTest(StructureController.class) public class StructureControllerTest { @@ -81,11 +83,11 @@ public class StructureControllerTest { private String readOnlyAuthorization; @Autowired - private WebSocketHandler webSocketHandler; + private WebSocketService webSocketService; @AfterEach - public void resetMocks(){ - reset(webSocketHandler, nodeDAO); + public void resetMocks() { + reset(webSocketService, nodeDAO); } @Test @@ -106,11 +108,11 @@ public void testMoveNode1() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); - verify(webSocketHandler, times(2)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(2)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testMoveNode2() throws Exception{ + public void testMoveNode2() throws Exception { when(nodeDAO.moveNodes(List.of("a"), "b", demoUser)) .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoUser).build()); @@ -124,11 +126,11 @@ public void testMoveNode2() throws Exception{ mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testMoveNode3() throws Exception{ + public void testMoveNode3() throws Exception { MockHttpServletRequestBuilder request = post("/move") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) @@ -147,7 +149,7 @@ public void testMoveNode3() throws Exception{ mockMvc.perform(request).andExpect(status().isUnauthorized()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -161,7 +163,7 @@ public void testMoveNodeSourceNodeListEmpty() throws Exception { mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -175,7 +177,7 @@ public void testMoveNodeTargetIdEmpty() throws Exception { mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -194,11 +196,11 @@ public void testCopyNodes1() throws Exception { .param("to", "target"); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test - public void testCopyNodes2() throws Exception{ + public void testCopyNodes2() throws Exception { MockHttpServletRequestBuilder request = post("/copy") .contentType(JSON) @@ -217,7 +219,7 @@ public void testCopyNodesBadRequest1() throws Exception { .param("to", ""); mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } @Test @@ -241,6 +243,6 @@ public void testCopyNodesBadRequest3() throws Exception { .param("to", "target"); mockMvc.perform(request).andExpect(status().isBadRequest()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketService, times(0)).sendMessageToClients(Mockito.any(WebSocketMessage.class)); } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java index bc11cfc137..df0f938952 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java @@ -27,10 +27,12 @@ import org.phoebus.applications.saveandrestore.model.TagData; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -47,7 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(TagController.class) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") public class TagControllerPermitAllTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java index b7f1afb71c..f3dbd629de 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java @@ -27,10 +27,12 @@ import org.phoebus.applications.saveandrestore.model.TagData; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -46,7 +48,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @WebMvcTest(TagController.class) @TestPropertySource(locations = "classpath:test_application.properties") public class TagControllerTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java index 06c6f3d8b2..c8aef1b267 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TakeSnapshotControllerTest.java @@ -13,9 +13,11 @@ import org.phoebus.service.saveandrestore.NodeNotFoundException; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.web.config.WebSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -32,7 +34,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) +@ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class, WebSecurityConfig.class})}) @TestPropertySource(locations = "classpath:test_application_permit_all.properties") @WebMvcTest(TakeSnapshotController.class) public class TakeSnapshotControllerTest {