From 549be41c70aa6ecf5d60326ea13c9f26bed00f6c Mon Sep 17 00:00:00 2001 From: georgweiss Date: Sat, 8 Mar 2025 17:43:53 +0100 Subject: [PATCH 01/43] Adding initial code for save&restore web socket API --- .../client/WebSocketClient.java | 27 ++ .../model/websocket/MessageType.java | 9 + .../model/websocket/WebSocketMessage.java | 8 + services/save-and-restore/pom.xml | 5 + .../saveandrestore/websocket/WebSocket.java | 269 ++++++++++++++++++ .../websocket/WebSocketConfig.java | 50 ++++ .../websocket/WebSocketHandler.java | 159 +++++++++++ 7 files changed, 527 insertions(+) create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java create mode 100644 app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/MessageType.java create mode 100644 app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebSocketMessage.java create mode 100644 services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocket.java create mode 100644 services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketConfig.java create mode 100644 services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketHandler.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java new file mode 100644 index 0000000000..a6f3512d53 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.client; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.util.concurrent.ExecutionException; + +public class WebSocketClient implements WebSocket.Listener { + + + public WebSocketClient(URI endpointURI) { + try { + WebSocket webSocket = + HttpClient.newHttpClient().newWebSocketBuilder() + .buildAsync(endpointURI, this).get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + + } +} 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/MessageType.java new file mode 100644 index 0000000000..d9f9e6d827 --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/MessageType.java @@ -0,0 +1,9 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.model.websocket; + +public enum MessageType { + UPDATE_NODE; +} diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebSocketMessage.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebSocketMessage.java new file mode 100644 index 0000000000..11eaca51e1 --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebSocketMessage.java @@ -0,0 +1,8 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.model.websocket; + +public record WebSocketMessage(MessageType messageType, String payload) { +} diff --git a/services/save-and-restore/pom.xml b/services/save-and-restore/pom.xml index bf1d2c6ec6..09983d0612 100644 --- a/services/save-and-restore/pom.xml +++ b/services/save-and-restore/pom.xml @@ -46,6 +46,11 @@ spring-boot-starter-data-elasticsearch + + org.springframework.boot + spring-boot-starter-websocket + + co.elastic.clients elasticsearch-java 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 new file mode 100644 index 0000000000..1cf9756a4e --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocket.java @@ -0,0 +1,269 @@ +/******************************************************************************* + * 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 com.fasterxml.jackson.databind.node.JsonNodeType; +import org.epics.vtype.VType; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("nls") +public class WebSocket { + /** + * Time when web socket was created + */ + private final long created = System.currentTimeMillis(); + + /** + * Track when the last message was received by web client + */ + private volatile long last_client_message = 0; + + /** + * Track when the last message was sent to web client + */ + private volatile long last_message_sent = 0; + + /** + * 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 write_queue = new ArrayBlockingQueue<>(2048); + + private static final String EXIT_MESSAGE = "EXIT"; + + private volatile WebSocketSession session; + private volatile String id; + + + private final Logger logger = Logger.getLogger(WebSocket.class.getName()); + + private final ObjectMapper objectMapper; + + /** + * Constructor + */ + public WebSocket(ObjectMapper objectMapper, WebSocketSession webSocketSession) { + logger.log(Level.FINE, () -> "Opening web socket " + session.getUri() + " ID " + session.getId()); + this.session = webSocketSession; + this.objectMapper = objectMapper; + this.id = webSocketSession.getId(); + Thread write_thread = new Thread(this::writeQueuedMessages, "Web Socket Write Thread"); + write_thread.setName("Web Socket Write Thread " + this.id); + write_thread.setDaemon(true); + write_thread.start(); + trackClientUpdate(); + } + + /** + * @return Session ID + */ + public String getId() { + if (session == null) + return "(" + id + ")"; + else + return id; + } + + /** + * @return Timestamp (ms since epoch) when socket was created + */ + public long getCreateTime() { + return created; + } + + /** + * @return Timestamp (ms since epoch) of last client message + */ + public long getLastClientMessage() { + return last_client_message; + } + + /** + * @return Timestamp (ms since epoch) of last message sent to client + */ + public long getLastMessageSent() { + return last_message_sent; + } + + /** + * @return Number of queued messages + */ + public int getQueuedMessageCount() { + return write_queue.size(); + } + + /** + * @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) + " ..."; + } + + private void queueMessage(final String message) { + // Ignore messages after 'dispose' + if (session == null) + return; + + if (write_queue.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 = write_queue.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 safe_session = session; + try { + if (safe_session == null) + throw new Exception("No session"); + if (!safe_session.isOpen()) + throw new Exception("Session closed"); + safe_session.sendMessage(new TextMessage(message)); + last_message_sent = System.currentTimeMillis(); + } catch (final Exception ex) { + logger.log(Level.WARNING, ex, () -> "Cannot write '" + shorten(message) + "' for " + id); + + // Clear queue + String drop = write_queue.take(); + while (drop != null) { + if (drop.equals(EXIT_MESSAGE)) { + logger.log(Level.FINE, () -> "Exiting write thread " + id); + return; + } + drop = write_queue.take(); + } + } + } + } catch (Throwable ex) { + logger.log(Level.WARNING, "Write thread error for " + id, ex); + } + } + + public void trackClientUpdate() { + last_client_message = System.currentTimeMillis(); + } + + private List getPVs(final String message, final JsonNode json) throws Exception { + final JsonNode node = json.path("pvs"); + if (node.isMissingNode()) + throw new Exception("Missing 'pvs' in " + shorten(message)); + final Iterator nodes = node.elements(); + final List pvs = new ArrayList<>(); + while (nodes.hasNext()) + pvs.add(nodes.next().asText()); + return pvs; + } + + /** + * Called when client sends a general 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(); + + switch (type) { + case "monitor": + default: + throw new Exception("Unknown message type: " + shorten(message.getPayload())); + } + } + + /** + * @param name PV name for which to send an update + * @param value Current value + * @param last_value Previous value + * @param last_readonly Was the PV read-only? + * @param readonly Is the PV read-only? + */ + public void sendUpdate(final String name, final VType value, final VType last_value, final boolean last_readonly, final boolean readonly) { + + } + + /** + * Clears all PVs + * + *

Web socket calls this onClose(), + * but context may also call this again just in case + */ + public void dispose() { + // Exit write thread + try { + // Drop queued messages (which might be stuffed): + // We're closing and just need the EXIT_MESSAGE + write_queue.clear(); + queueMessage(EXIT_MESSAGE); + // TODO: is this needed? + session.close(); + } catch (Throwable ex) { + logger.log(Level.WARNING, "Error disposing " + getId(), ex); + } + logger.log(Level.FINE, () -> "Web socket " + session.getId() + " closed"); + last_client_message = 0; + } + + + private void write(String message, JsonNode json) throws Exception { + JsonNode n = json.path("pv"); + if (n.isMissingNode()) + throw new Exception("Missing 'pv' in " + shorten(message)); + final String pv_name = n.asText(); + + n = json.path("value"); + if (n.isMissingNode()) + throw new Exception("Missing 'value' in " + shorten(message)); + final Object value; + if (n.getNodeType() == JsonNodeType.NUMBER) + value = n.asDouble(); + else + value = n.asText(); + + } +} 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 new file mode 100644 index 0000000000..f0e89df46d --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.databind.ObjectMapper; +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 java.util.List; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Autowired + public ObjectMapper objectMapper; + + @Autowired + private List sockets; + + @Autowired + private WebSocketHandler webSocketHandler; + + private final Logger logger = Logger.getLogger(WebSocketConfig.class.getName()); + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(webSocketHandler, "/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 new file mode 100644 index 0000000000..0ebd4c6b2d --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketHandler.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 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.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +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.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. + */ +@Component +public class WebSocketHandler extends TextWebSocketHandler { + + /** + * List of active {@link WebSocket} + */ + @SuppressWarnings("unused") + @Autowired + private List sockets; + + @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 = + 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) { + sockets.add(new WebSocket(objectMapper, session)); + } + + /** + * 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 = + sockets.stream().filter(webSocket -> webSocket.getId().equals(session.getId())).findFirst(); + if (webSocketOptional.isPresent()) { + 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 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.FINER, "Got pong"); + // Find the WebSocket instance associated with this WebSocketSession + Optional 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().trackClientUpdate(); + } + + /** + * @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() { + sockets.forEach(s -> { + logger.log(Level.INFO, "Disposing socket " + s.getId()); + s.dispose(); + }); + } +} From f9647112956edcf71bad825db09af1f3bc4e7e04 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 10 Mar 2025 12:51:43 +0100 Subject: [PATCH 02/43] Added missing web socket code, initial end-to-end test --- .../client/WebSocketClient.java | 33 ++++++++++++++++--- .../ui/SaveAndRestoreController.java | 3 ++ .../web/config/WebConfiguration.java | 9 +++++ .../saveandrestore/websocket/WebSocket.java | 2 +- .../websocket/WebSocketHandler.java | 2 +- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java index a6f3512d53..230fa1bfd4 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java @@ -7,21 +7,46 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.WebSocket; +import java.nio.ByteBuffer; import java.util.concurrent.ExecutionException; public class WebSocketClient implements WebSocket.Listener { + private static WebSocketClient instance; - public WebSocketClient(URI endpointURI) { + public static WebSocketClient getInstance(){ + if(instance == null){ + instance = new WebSocketClient(); + } + return instance; + } + + private WebSocketClient(){ try { - WebSocket webSocket = - HttpClient.newHttpClient().newWebSocketBuilder() - .buildAsync(endpointURI, this).get(); + WebSocket webSocket = HttpClient.newBuilder() + .build() + .newWebSocketBuilder() + .buildAsync(URI.create("ws://localhost:8080/web-socket"), this).get(); + //webSocket.sendText("Wake Up", true); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); } + } + @Override + public void onOpen(WebSocket webSocket){ + ByteBuffer byteBuffer = ByteBuffer.allocate(100); + byteBuffer.put("Hello".getBytes()); + try { + webSocket.sendText("{'a':771}", true).get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } } + + } 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 ffc9f4444d..e287fcac91 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 @@ -67,6 +67,7 @@ import org.phoebus.applications.saveandrestore.RestoreUtil; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; import org.phoebus.applications.saveandrestore.actions.OpenNodeAction; +import org.phoebus.applications.saveandrestore.client.WebSocketClient; import org.phoebus.applications.saveandrestore.filehandler.csv.CSVExporter; import org.phoebus.applications.saveandrestore.filehandler.csv.CSVImporter; import org.phoebus.applications.saveandrestore.model.Node; @@ -362,6 +363,8 @@ public Filter fromString(String s) { treeView.setContextMenu(contextMenu); loadTreeData(); + + WebSocketClient webSocketClient = WebSocketClient.getInstance(); } 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 dc7b2eeab6..864977d16c 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,10 +20,13 @@ 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -64,4 +67,10 @@ public SnapshotUtil snapshotRestorer(){ public ExecutorService executorService(){ return Executors.newCachedThreadPool(); } + + @Bean(name = "sockets") + @Scope("singleton") + public List getSockets() { + return new CopyOnWriteArrayList<>(); + } } 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 index 1cf9756a4e..4e17297b1c 100644 --- 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 @@ -66,8 +66,8 @@ public class WebSocket { * Constructor */ public WebSocket(ObjectMapper objectMapper, WebSocketSession webSocketSession) { - logger.log(Level.FINE, () -> "Opening web socket " + session.getUri() + " ID " + session.getId()); this.session = webSocketSession; + logger.log(Level.INFO, () -> "Opening web socket " + session.getUri() + " ID " + session.getId()); this.objectMapper = objectMapper; this.id = webSocketSession.getId(); Thread write_thread = new Thread(this::writeQueuedMessages, "Web Socket Write Thread"); 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 index 0ebd4c6b2d..3339edb685 100644 --- 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 @@ -129,7 +129,7 @@ public void handleTransportError(@NonNull WebSocketSession session, @NonNull Thr */ @Override protected void handlePongMessage(@NonNull WebSocketSession session, @NonNull PongMessage message) { - logger.log(Level.FINER, "Got pong"); + logger.log(Level.INFO, "Got pong"); // Find the WebSocket instance associated with this WebSocketSession Optional webSocketOptional = sockets.stream().filter(webSocket -> webSocket.getId().equals(session.getId())).findFirst(); From 2905ea74f3f500073754d3b8697d121e913145bb Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 17 Mar 2025 09:04:09 +0100 Subject: [PATCH 03/43] Ping/pong thread in web socket client --- services/save-and-restore/pom.xml | 11 +++++++++++ .../saveandrestore/websocket/WebSocketHandler.java | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/services/save-and-restore/pom.xml b/services/save-and-restore/pom.xml index 09983d0612..efe20b015b 100644 --- a/services/save-and-restore/pom.xml +++ b/services/save-and-restore/pom.xml @@ -86,6 +86,17 @@ 4.7.4-SNAPSHOT + + org.apache.logging.log4j + log4j-api + 2.23.1 + + + org.apache.logging.log4j + log4j-core + 2.23.1 + + org.springframework.boot spring-boot-starter 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 index 3339edb685..dcd065735d 100644 --- 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 @@ -24,6 +24,7 @@ import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.PingMessage; import org.springframework.web.socket.PongMessage; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; @@ -31,6 +32,8 @@ import javax.annotation.PreDestroy; import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; import java.util.List; import java.util.Optional; import java.util.logging.Level; @@ -85,7 +88,8 @@ public void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMe */ @Override public void afterConnectionEstablished(@NonNull WebSocketSession session) { - sockets.add(new WebSocket(objectMapper, session)); + WebSocket webSocket = new WebSocket(objectMapper, session); + sockets.add(webSocket); } /** @@ -137,6 +141,7 @@ protected void handlePongMessage(@NonNull WebSocketSession session, @NonNull Pon return; // Should only happen in case of timing issues? } webSocketOptional.get().trackClientUpdate(); + } /** From 29e98e915eaef2fd641c65bca6b390852480a789 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 17 Mar 2025 09:04:53 +0100 Subject: [PATCH 04/43] Ping/pong thread in web socket client --- .../client/WebSocketClient.java | 143 +++++++++++++++--- .../ui/SaveAndRestoreController.java | 12 +- .../saveandrestore/ui/SaveAndRestoreUI.fxml | 9 +- 3 files changed, 132 insertions(+), 32 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java index 230fa1bfd4..db0277e8a1 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java @@ -8,44 +8,139 @@ import java.net.http.HttpClient; import java.net.http.WebSocket; import java.nio.ByteBuffer; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; public class WebSocketClient implements WebSocket.Listener { private static WebSocketClient instance; + private WebSocket webSocket; + private boolean pingThreadRunning; + private boolean connectThreadRunning; + private CountDownLatch countDownLatch; + private final Logger logger = Logger.getLogger(WebSocketClient.class.getName()); - public static WebSocketClient getInstance(){ - if(instance == null){ - instance = new WebSocketClient(); - } - return instance; + public WebSocketClient() { + connect(); + } + + public void connect() { + new Thread(() -> { + long waitTime = 5000; + int connectAttempt = 0; + connectThreadRunning = true; + while (connectThreadRunning) { + logger.log(Level.INFO, "Connecting to ws://localhost:8080/web-socket"); + try { + webSocket = HttpClient.newBuilder() + .build() + .newWebSocketBuilder() + .buildAsync(URI.create("ws://localhost:8080/web-socket"), this) + .join(); + logger.log(Level.INFO, "Successfully connected to ws://localhost:8080/web-socket"); + connectThreadRunning = false; + break; + } catch (Exception e) { + logger.log(Level.INFO, "Failed to connect to ws://localhost:8080/web-socket"); + } + try { + Thread.sleep(Math.round(Math.pow(2, connectAttempt++) * waitTime)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }).start(); + } + + @Override + public void onOpen(WebSocket webSocket) { + WebSocket.Listener.super.onOpen(webSocket); + pingThreadRunning = true; + startPingThread(); + logger.log(Level.INFO, "onOpen called"); } - private WebSocketClient(){ + public void sendText(String message) { try { - WebSocket webSocket = HttpClient.newBuilder() - .build() - .newWebSocketBuilder() - .buildAsync(URI.create("ws://localhost:8080/web-socket"), this).get(); - //webSocket.sendText("Wake Up", true); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { + webSocket.sendText(message, true).get(); + } catch (Exception e) { throw new RuntimeException(e); } } + public void close() { + webSocket.sendClose(771, "Fed up"); + webSocket.abort(); + } + + @Override - public void onOpen(WebSocket webSocket){ - ByteBuffer byteBuffer = ByteBuffer.allocate(100); - byteBuffer.put("Hello".getBytes()); - try { - webSocket.sendText("{'a':771}", true).get(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + logger.log(Level.INFO, "onClose called"); + return null; + } + + public void sendPing() { + webSocket.sendPing(ByteBuffer.allocate(0)); + } + + @Override + public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { + logger.log(Level.INFO, "Got pong "); + if (countDownLatch != null) { + countDownLatch.countDown(); } + return WebSocket.Listener.super.onPong(webSocket, message); + } + + @Override + public CompletionStage onBinary(WebSocket webSocket, + ByteBuffer data, + boolean last) { + webSocket.request(1); + return WebSocket.Listener.super.onBinary(webSocket, data, last); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + error.printStackTrace(); + logger.log(Level.INFO, "onError called"); + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence data, + boolean last) { + webSocket.request(1); + + return WebSocket.Listener.super.onText(webSocket, data, last); + } + + private void startPingThread() { + new Thread(() -> { + while (pingThreadRunning) { + countDownLatch = new CountDownLatch(1); + logger.log(Level.INFO, "Sending ping"); + sendPing(); + try { + countDownLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (countDownLatch.getCount() == 0) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + }).start(); } 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 e287fcac91..eb22093d0c 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 @@ -25,6 +25,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; @@ -37,6 +38,7 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Menu; @@ -194,6 +196,9 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @FXML private VBox errorPane; + @FXML + private Label webSocketTrackerLabel; + private final ObservableList searchResultNodes = FXCollections.observableArrayList(); private final ObservableList filtersList = FXCollections.observableArrayList(); @@ -248,6 +253,9 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController new ExportToCSVMenuItem(this, selectedItemsProperty, () -> exportToCSV()) ); + private final SimpleStringProperty webSocketTrackerText = new SimpleStringProperty(); + + WebSocketClient webSocketClient = new WebSocketClient(); @Override public void initialize(URL url, ResourceBundle resourceBundle) { @@ -362,9 +370,7 @@ public Filter fromString(String s) { treeView.setContextMenu(contextMenu); - loadTreeData(); - - WebSocketClient webSocketClient = WebSocketClient.getInstance(); + //loadTreeData(); } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml index 2de20f3770..f3348a7d46 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml @@ -39,14 +39,13 @@ - - - - - + From d1f1c0c12d0d36995308aefe202dfe244210a847 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 25 Mar 2025 12:59:36 +0100 Subject: [PATCH 05/43] Some rework to handle data changes from save&restore --- .../client/WebSocketClient.java | 129 +++++++----------- .../saveandrestore/ui/DataChangeListener.java | 23 ++++ .../saveandrestore/ui/NodeAddedListener.java | 25 +--- .../ui/SaveAndRestoreController.java | 52 ++++--- .../ui/SaveAndRestoreService.java | 117 ++++++++-------- .../saveandrestore/ui/SaveAndRestoreTab.java | 2 +- .../ConfigurationController.java | 5 +- .../ui/configuration/ConfigurationTab.java | 7 +- .../ui/search/SearchAndFilterTab.java | 7 +- .../search/SearchAndFilterViewController.java | 7 +- .../ui/snapshot/CompositeSnapshotTab.java | 5 - .../ui/snapshot/SnapshotTab.java | 7 +- .../saveandrestore/ui/SaveAndRestoreUI.fxml | 2 +- .../model/websocket/MessageType.java | 6 +- ...va => SaveAndRestoreWebSocketMessage.java} | 2 +- .../websocket/WebMessageDeserializer.java | 56 ++++++++ ...storeWebSocketMessageDeserializerTest.java | 93 +++++++++++++ .../src/test/resources/websocketexample1.json | 4 + .../src/test/resources/websocketexample2.json | 9 ++ .../src/test/resources/websocketexample3.json | 8 ++ .../src/test/resources/websocketexample4.json | 4 + .../src/test/resources/websocketexample5.json | 3 + .../persistence/dao/NodeDAO.java | 7 +- .../impl/elasticsearch/ElasticsearchDAO.java | 20 ++- .../controllers/ConfigurationController.java | 11 +- .../web/controllers/NodeController.java | 20 ++- .../web/controllers/SnapshotController.java | 13 +- .../saveandrestore/websocket/WebSocket.java | 57 ++++---- .../websocket/WebSocketHandler.java | 20 ++- .../persistence/dao/impl/DAOTestIT.java | 9 +- .../web/config/ControllersTestConfig.java | 12 ++ .../web/config/WebSocketConfig.java | 81 +++++++++++ .../web/controllers/NodeControllerTest.java | 84 +++++++++++- 33 files changed, 647 insertions(+), 260 deletions(-) create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java rename app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/{WebSocketMessage.java => SaveAndRestoreWebSocketMessage.java} (60%) create mode 100644 app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java create mode 100644 app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java create mode 100644 app/save-and-restore/model/src/test/resources/websocketexample1.json create mode 100644 app/save-and-restore/model/src/test/resources/websocketexample2.json create mode 100644 app/save-and-restore/model/src/test/resources/websocketexample3.json create mode 100644 app/save-and-restore/model/src/test/resources/websocketexample4.json create mode 100644 app/save-and-restore/model/src/test/resources/websocketexample5.json create mode 100644 services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebSocketConfig.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java index db0277e8a1..1ccb325ebb 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java @@ -9,60 +9,53 @@ 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.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; +/** + * A web socket client implementation supporting pong and text messages. + */ public class WebSocketClient implements WebSocket.Listener { - private static WebSocketClient instance; private WebSocket webSocket; - private boolean pingThreadRunning; - private boolean connectThreadRunning; - private CountDownLatch countDownLatch; private final Logger logger = Logger.getLogger(WebSocketClient.class.getName()); - - public WebSocketClient() { - connect(); - } - - public void connect() { - new Thread(() -> { - long waitTime = 5000; - int connectAttempt = 0; - connectThreadRunning = true; - while (connectThreadRunning) { - logger.log(Level.INFO, "Connecting to ws://localhost:8080/web-socket"); - try { - webSocket = HttpClient.newBuilder() - .build() - .newWebSocketBuilder() - .buildAsync(URI.create("ws://localhost:8080/web-socket"), this) - .join(); - logger.log(Level.INFO, "Successfully connected to ws://localhost:8080/web-socket"); - connectThreadRunning = false; - break; - } catch (Exception e) { - logger.log(Level.INFO, "Failed to connect to ws://localhost:8080/web-socket"); - } - try { - Thread.sleep(Math.round(Math.pow(2, connectAttempt++) * waitTime)); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - }).start(); + private Runnable disconnectCallback; + private URI uri; + private Consumer onTextCallback; + + /** + * + * @param uri The URI of the web socket peer. + * @param disconnectCallback An optional {@link Runnable} called if the web socket is closed, e.g. if + * peer closes it or due to network issues. + */ + public WebSocketClient(URI uri, Runnable disconnectCallback, Consumer onTextCallback) { + this.disconnectCallback = disconnectCallback; + this.uri = uri; + this.onTextCallback = onTextCallback; + try { + webSocket = HttpClient.newBuilder() + .build() + .newWebSocketBuilder() + .buildAsync(uri, this) + .join(); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to connect to " + uri); + } } @Override public void onOpen(WebSocket webSocket) { WebSocket.Listener.super.onOpen(webSocket); - pingThreadRunning = true; - startPingThread(); - logger.log(Level.INFO, "onOpen called"); + logger.log(Level.INFO, "Connected to " + uri); } + /** + * 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(); @@ -71,45 +64,37 @@ public void sendText(String message) { } } - public void close() { - webSocket.sendClose(771, "Fed up"); - webSocket.abort(); - } - @Override public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) { - logger.log(Level.INFO, "onClose called"); + 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() { webSocket.sendPing(ByteBuffer.allocate(0)); } @Override public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - logger.log(Level.INFO, "Got pong "); - if (countDownLatch != null) { - countDownLatch.countDown(); - } + logger.log(Level.FINE, "Got pong"); return WebSocket.Listener.super.onPong(webSocket, message); } - @Override - public CompletionStage onBinary(WebSocket webSocket, - ByteBuffer data, - boolean last) { - webSocket.request(1); - return WebSocket.Listener.super.onBinary(webSocket, data, last); - } - @Override public void onError(WebSocket webSocket, Throwable error) { + logger.log(Level.WARNING, "Got web socket error"); error.printStackTrace(); - logger.log(Level.INFO, "onError called"); + WebSocket.Listener.super.onError(webSocket, error); } @Override @@ -117,31 +102,11 @@ public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { webSocket.request(1); - + onTextCallback.accept(data); return WebSocket.Listener.super.onText(webSocket, data, last); } - private void startPingThread() { - new Thread(() -> { - while (pingThreadRunning) { - countDownLatch = new CountDownLatch(1); - logger.log(Level.INFO, "Sending ping"); - sendPing(); - try { - countDownLatch.await(3, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - if (countDownLatch.getCount() == 0) { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - }).start(); + public void close(String reason){ + webSocket.sendClose(1000, reason); } - - } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java new file mode 100644 index 0000000000..5f264b2f42 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui; + +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.search.Filter; + +public interface DataChangeListener { + + default void nodeAddedOrRemoved(String parentNodeId){ + } + + default void nodeChanged(Node node){ + } + + default void filterAddedOrUpdated(Filter filter){ + } + + default void filterRemoved(Filter filter){ + } +} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java index 6f7305e2d1..ba384b9b77 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java @@ -1,34 +1,15 @@ /* - * 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. + * Copyright (C) 2025 European Spallation Source ERIC. */ package org.phoebus.applications.saveandrestore.ui; -import org.phoebus.applications.saveandrestore.model.Node; - -import java.util.List; - public interface NodeAddedListener { /** * To be called when a new node has been created (typically new snapshot node). * - * @param parentNode The parent of the new node as defined in the back-end data model. - * @param newNodes The list of {@link Node}s added to the parent {@link Node}. + * @param parentNodeId The unique id of the new node's parent node. */ - void nodesAdded(Node parentNode, List newNodes); + void nodeAdded(String parentNodeId); } 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 eb22093d0c..2a44851076 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 @@ -69,6 +69,7 @@ import org.phoebus.applications.saveandrestore.RestoreUtil; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; import org.phoebus.applications.saveandrestore.actions.OpenNodeAction; +import org.phoebus.applications.saveandrestore.client.Preferences; import org.phoebus.applications.saveandrestore.client.WebSocketClient; import org.phoebus.applications.saveandrestore.filehandler.csv.CSVExporter; import org.phoebus.applications.saveandrestore.filehandler.csv.CSVImporter; @@ -79,6 +80,7 @@ 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.ui.configuration.ConfigurationTab; import org.phoebus.applications.saveandrestore.ui.contextmenu.CopyUniqueIdToClipboardMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.CreateSnapshotMenuItem; @@ -137,7 +139,7 @@ * Main controller for the save and restore UI. */ public class SaveAndRestoreController extends SaveAndRestoreBaseController - implements Initializable, NodeChangedListener, NodeAddedListener, FilterChangeListener { + implements Initializable, DataChangeListener { @FXML protected TreeView treeView; @@ -255,7 +257,6 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController private final SimpleStringProperty webSocketTrackerText = new SimpleStringProperty(); - WebSocketClient webSocketClient = new WebSocketClient(); @Override public void initialize(URL url, ResourceBundle resourceBundle) { @@ -292,9 +293,10 @@ public void initialize(URL url, ResourceBundle resourceBundle) { treeView.setShowRoot(true); - saveAndRestoreService.addNodeChangeListener(this); - saveAndRestoreService.addNodeAddedListener(this); - saveAndRestoreService.addFilterChangeListener(this); + //saveAndRestoreService.addNodeChangeListener(this); + //saveAndRestoreService.addNodeAddedListener(this); + //saveAndRestoreService.addFilterChangeListener(this); + saveAndRestoreService.addDataChangeListener(this); treeView.setCellFactory(p -> new BrowserTreeCell(this)); treeViewPane.disableProperty().bind(disabledUi); @@ -370,9 +372,10 @@ public Filter fromString(String s) { treeView.setContextMenu(contextMenu); - //loadTreeData(); - } + loadTreeData(); + webSocketTrackerLabel.textProperty().bind(webSocketTrackerText); + } /** * Loads the data for the tree root as provided (persisted) by the current @@ -789,11 +792,14 @@ private void renameNode() { if (result.isPresent()) { node.getValue().setName(result.get()); try { + String parentNodeIdBefore = saveAndRestoreService.getParentNode(node.getValue().getUniqueId()).getUniqueId(); saveAndRestoreService.updateNode(node.getValue()); - // Since a changed node name may push the node to a different location in the tree view, - // we need to locate it to keep it selected. The tree view will otherwise "select" the node - // at the previous position of the renamed node. This is standard JavaFX TreeView behavior - // where TreeItems are "recycled", and updated by the cell renderer. + // Node updated... Does it have a new parent, i.e. has it been moved in the tree structure? + String parentNodeIdAfter = saveAndRestoreService.getParentNode(node.getValue().getUniqueId()).getUniqueId(); + if(parentNodeIdAfter.equals(parentNodeIdBefore)){ + return; + } + // New parent node, update UI Stack copiedStack = new Stack<>(); DirectoryUtilities.CreateLocationStringAndNodeStack(node.getValue(), false).getValue().forEach(copiedStack::push); locateNode(copiedStack); @@ -826,6 +832,7 @@ public boolean isLeaf() { * * @param node The updated node. */ + @Override public void nodeChanged(Node node) { // Find the node that has changed @@ -835,26 +842,27 @@ public void nodeChanged(Node node) { } nodeSubjectToUpdate.setValue(node); // Folder and configuration node changes may include structure changes, so expand to force update. + /* if (nodeSubjectToUpdate.isExpanded() && (nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.FOLDER) || nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.CONFIGURATION))) { if (nodeSubjectToUpdate.getParent() != null) { // null means root folder as it has no parent nodeSubjectToUpdate.getParent().getChildren().sort(treeNodeComparator); } expandTreeNode(nodeSubjectToUpdate); - } + }*/ } /** * Handles callback in order to update the tree view when a {@link Node} has been added, e.g. when * a snapshot is saved. * - * @param parentNode Parent of the new {@link Node} - * @param newNodes The list of new {@link Node}s + * @param parentNodeId Unique id of the parent {@link Node} */ + @Override - public void nodesAdded(Node parentNode, List newNodes) { + public void nodeAddedOrRemoved(String parentNodeId){ // Find the parent to which the new node is to be added - TreeItem parentTreeItem = recursiveSearch(parentNode.getUniqueId(), treeView.getRoot()); + TreeItem parentTreeItem = recursiveSearch(parentNodeId, treeView.getRoot()); if (parentTreeItem == null) { return; } @@ -965,9 +973,10 @@ public void saveLocalState() { public void handleTabClosed() { saveLocalState(); - saveAndRestoreService.removeNodeChangeListener(this); - saveAndRestoreService.removeNodeAddedListener(this); - saveAndRestoreService.removeFilterChangeListener(this); + //saveAndRestoreService.removeNodeChangeListener(this); + //saveAndRestoreService.removeNodeAddedListener(this); + //saveAndRestoreService.removeFilterChangeListener(this); + //webSocketClient.close("User closing " + SaveAndRestoreApplication.DISPLAY_NAME); } /** @@ -1023,7 +1032,7 @@ protected void addTagToSnapshots() { ObservableList> selectedItems = browserSelectionModel.getSelectedItems(); List selectedNodes = selectedItems.stream().map(TreeItem::getValue).collect(Collectors.toList()); List updatedNodes = TagUtil.addTag(selectedNodes); - updatedNodes.forEach(this::nodeChanged); + //updatedNodes.forEach(this::nodeChanged); } /** @@ -1036,7 +1045,7 @@ public void configureTagContextMenu(final Menu tagMenu) { List selectedNodes = browserSelectionModel.getSelectedItems().stream().map(TreeItem::getValue).collect(Collectors.toList()); - TagUtil.tag(tagMenu, selectedNodes, updatedNodes -> updatedNodes.forEach(this::nodeChanged)); + //TagUtil.tag(tagMenu, selectedNodes, updatedNodes -> updatedNodes.forEach(this::nodeChanged)); } /** @@ -1530,4 +1539,5 @@ private void addOptionalLoggingMenuItem() { }); } } + } 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 a689b817ad..542a244563 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,9 +18,17 @@ 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.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.client.WebSocketClient; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; import org.phoebus.applications.saveandrestore.model.ConfigPv; import org.phoebus.applications.saveandrestore.model.Configuration; @@ -36,6 +44,8 @@ import org.phoebus.applications.saveandrestore.model.UserData; import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchResult; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.model.websocket.WebMessageDeserializer; import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; @@ -43,6 +53,7 @@ import org.phoebus.util.time.TimestampFormats; import javax.ws.rs.core.MultivaluedMap; +import java.net.URI; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -60,20 +71,33 @@ public class SaveAndRestoreService { private final ExecutorService executor; - private final List nodeChangeListeners = Collections.synchronizedList(new ArrayList<>()); - private final List nodeAddedListeners = Collections.synchronizedList(new ArrayList<>()); - - private final List filterChangeListeners = Collections.synchronizedList(new ArrayList<>()); - + private final List dataChangeListeners = Collections.synchronizedList(new ArrayList<>()); private static final Logger LOG = Logger.getLogger(SaveAndRestoreService.class.getName()); private static SaveAndRestoreService instance; private final SaveAndRestoreClient saveAndRestoreClient; + private final ObjectMapper objectMapper; + + private final WebSocketClient webSocketClient; private SaveAndRestoreService() { saveAndRestoreClient = new SaveAndRestoreClientImpl(); + String baseUrl = Preferences.jmasarServiceUrl; + String schema = baseUrl.startsWith("https") ? "wss" : "ws"; + String webSocketUrl = schema + baseUrl.substring(baseUrl.indexOf("://", 0)) + "/web-socket"; + URI webSocketUri = URI.create(webSocketUrl); + webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketDisconnect, this::handleWebSocketMessage); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + 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 SaveAndRestoreService getInstance() { @@ -120,14 +144,12 @@ public Node updateNode(Node nodeToUpdate) throws Exception { public Node updateNode(Node nodeToUpdate, boolean customTimeForMigration) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.updateNode(nodeToUpdate, customTimeForMigration)); Node node = future.get(); - notifyNodeChangeListeners(node); + dataChangeListeners.forEach(l -> l.nodeChanged(node)); return node; } public Node createNode(String parentNodeId, Node newTreeNode) throws Exception { - Future future = executor.submit(() -> saveAndRestoreClient.createNewNode(parentNodeId, newTreeNode)); - notifyNodeAddedListeners(getNode(parentNodeId), Collections.singletonList(newTreeNode)); - return future.get(); + return executor.submit(() -> saveAndRestoreClient.createNewNode(parentNodeId, newTreeNode)).get(); } public void deleteNodes(List nodeIds) throws Exception { @@ -146,7 +168,7 @@ public Node getParentNode(String uniqueNodeId) throws Exception { public Configuration createConfiguration(final Node parentNode, final Configuration configuration) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.createConfiguration(parentNode.getUniqueId(), configuration)); Configuration newConfiguration = future.get(); - notifyNodeChangeListeners(parentNode); + dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved(parentNode.getUniqueId())); return newConfiguration; } @@ -154,7 +176,7 @@ public Configuration updateConfiguration(Configuration configuration) throws Exc Future future = executor.submit(() -> saveAndRestoreClient.updateConfiguration(configuration)); Configuration updatedConfiguration = future.get(); // Associated configuration Node may have a new name - notifyNodeChangeListeners(updatedConfiguration.getConfigurationNode()); + dataChangeListeners.forEach(l -> l.nodeChanged(updatedConfiguration.getConfigurationNode())); return updatedConfiguration; } @@ -163,28 +185,12 @@ public List getAllTags() throws Exception { return future.get(); } - public void addNodeChangeListener(NodeChangedListener nodeChangeListener) { - nodeChangeListeners.add(nodeChangeListener); - } - - public void removeNodeChangeListener(NodeChangedListener nodeChangeListener) { - nodeChangeListeners.remove(nodeChangeListener); + public void addDataChangeListener(DataChangeListener dataChangeListener){ + dataChangeListeners.add(dataChangeListener); } - private void notifyNodeChangeListeners(Node changedNode) { - nodeChangeListeners.forEach(listener -> listener.nodeChanged(changedNode)); - } - - public void addNodeAddedListener(NodeAddedListener nodeAddedListener) { - nodeAddedListeners.add(nodeAddedListener); - } - - public void removeNodeAddedListener(NodeAddedListener nodeAddedListener) { - nodeAddedListeners.remove(nodeAddedListener); - } - - private void notifyNodeAddedListeners(Node parentNode, List newNodes) { - nodeAddedListeners.forEach(listener -> listener.nodesAdded(parentNode, newNodes)); + public void removeDataChangeListener(DataChangeListener dataChangeListener){ + dataChangeListeners.remove(dataChangeListener); } /** @@ -253,7 +259,7 @@ public Snapshot saveSnapshot(Node configurationNode, Snapshot snapshot) throws E }); Snapshot updatedSnapshot = future.get(); // Notify listeners as the configuration node has a new child node. - notifyNodeChangeListeners(configurationNode); + dataChangeListeners.forEach(l -> l.nodeChanged(configurationNode)); return updatedSnapshot; } @@ -273,7 +279,7 @@ public CompositeSnapshot saveCompositeSnapshot(Node parentNode, CompositeSnapsho Future future = executor.submit(() -> saveAndRestoreClient.createCompositeSnapshot(parentNode.getUniqueId(), compositeSnapshot)); CompositeSnapshot newCompositeSnapshot = future.get(); - notifyNodeChangeListeners(parentNode); + dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved(parentNode.getUniqueId())); return newCompositeSnapshot; } @@ -281,7 +287,7 @@ public CompositeSnapshot updateCompositeSnapshot(final CompositeSnapshot composi Future future = executor.submit(() -> saveAndRestoreClient.updateCompositeSnapshot(compositeSnapshot)); CompositeSnapshot updatedCompositeSnapshot = future.get(); // Associated composite snapshot Node may have a new name - notifyNodeChangeListeners(updatedCompositeSnapshot.getCompositeSnapshotNode()); + dataChangeListeners.forEach(l -> l.nodeChanged(updatedCompositeSnapshot.getCompositeSnapshotNode())); return updatedCompositeSnapshot; } @@ -319,7 +325,7 @@ public Filter saveFilter(Filter filter) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.saveFilter(filter)); Filter addedOrUpdatedFilter = future.get(); - notifyFilterAddedOrUpdated(addedOrUpdatedFilter); + dataChangeListeners.forEach(l -> l.filterAddedOrUpdated(filter)); return addedOrUpdatedFilter; } @@ -339,7 +345,7 @@ public List getAllFilters() throws Exception { */ public void deleteFilter(final Filter filter) throws Exception { executor.submit(() -> saveAndRestoreClient.deleteFilter(filter.getName())).get(); - notifyFilterDeleted(filter); + dataChangeListeners.forEach(l -> l.filterRemoved(filter)); } /** @@ -353,7 +359,7 @@ public List addTag(TagData tagData) throws Exception { Future> future = executor.submit(() -> saveAndRestoreClient.addTag(tagData)); List updatedNodes = future.get(); - updatedNodes.forEach(this::notifyNodeChangeListeners); + updatedNodes.forEach(n -> dataChangeListeners.forEach(l -> l.nodeChanged(n))); return updatedNodes; } @@ -368,26 +374,10 @@ public List deleteTag(TagData tagData) throws Exception { Future> future = executor.submit(() -> saveAndRestoreClient.deleteTag(tagData)); List updatedNodes = future.get(); - updatedNodes.forEach(this::notifyNodeChangeListeners); + updatedNodes.forEach(n -> dataChangeListeners.forEach(l -> l.nodeChanged(n))); return updatedNodes; } - public void addFilterChangeListener(FilterChangeListener filterChangeListener) { - filterChangeListeners.add(filterChangeListener); - } - - public void removeFilterChangeListener(FilterChangeListener filterChangeListener) { - filterChangeListeners.remove(filterChangeListener); - } - - private void notifyFilterAddedOrUpdated(Filter filter) { - filterChangeListeners.forEach(l -> l.filterAddedOrUpdated(filter)); - } - - private void notifyFilterDeleted(Filter filter) { - filterChangeListeners.forEach(l -> l.filterRemoved(filter)); - } - /** * Authenticate user, needed for all non-GET endpoints if service requires it * @@ -492,4 +482,23 @@ private VType readFromArchiver(String pvName, Instant time) { return VDisconnectedData.INSTANCE; } } + + private void handleWebSocketDisconnect(){ + System.out.println("Web socket disconnected"); + } + + private void handleWebSocketMessage(CharSequence charSequence){ + try { + SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = + objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); + switch (saveAndRestoreWebSocketMessage.messageType()){ + case NODE_ADDED, NODE_REMOVED -> dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload())); + case NODE_UPDATED -> dataChangeListeners.forEach(l -> l.nodeChanged((Node)saveAndRestoreWebSocketMessage.payload())); + case FILTER_ADDED_OR_UPDATED -> dataChangeListeners.forEach(l -> l.filterAddedOrUpdated((Filter)saveAndRestoreWebSocketMessage.payload())); + case FILTER_REMOVED -> dataChangeListeners.forEach(l -> l.filterRemoved((Filter)saveAndRestoreWebSocketMessage.payload())); + } + } catch (JsonProcessingException e) { + throw new RuntimeException(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 7b7180140c..b1d9b06716 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 @@ -34,7 +34,7 @@ /** * Base class for save-n-restore {@link Tab}s containing common functionality. */ -public abstract class SaveAndRestoreTab extends Tab implements NodeChangedListener { +public abstract class SaveAndRestoreTab extends Tab { protected SaveAndRestoreBaseController controller; 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 602a4c8c79..80917346a1 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 @@ -54,6 +54,7 @@ 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.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.NodeChangedListener; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; @@ -75,7 +76,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -public class ConfigurationController extends SaveAndRestoreBaseController implements NodeChangedListener { +public class ConfigurationController extends SaveAndRestoreBaseController implements DataChangeListener { @FXML private BorderPane root; @@ -264,7 +265,7 @@ public void updateItem(String item, boolean empty) { addPVsPane.disableProperty().bind(userIdentity.isNull()); - SaveAndRestoreService.getInstance().addNodeChangeListener(this); + SaveAndRestoreService.getInstance().addDataChangeListener(this); } @FXML 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 ad4d54cba9..90373ed6c0 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 @@ -23,6 +23,7 @@ import javafx.scene.image.ImageView; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; @@ -32,7 +33,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class ConfigurationTab extends SaveAndRestoreTab { +public class ConfigurationTab extends SaveAndRestoreTab implements DataChangeListener { public ConfigurationTab() { configure(); @@ -69,11 +70,11 @@ private void configure() { if (!((ConfigurationController) controller).handleConfigurationTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeNodeChangeListener(this); + SaveAndRestoreService.getInstance().removeDataChangeListener(this); } }); - SaveAndRestoreService.getInstance().addNodeChangeListener(this); + SaveAndRestoreService.getInstance().addDataChangeListener(this); } /** diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java index 5ac75eca1f..f5a9792df8 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java @@ -27,6 +27,7 @@ import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.NodeChangedListener; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; @@ -44,7 +45,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class SearchAndFilterTab extends SaveAndRestoreTab implements NodeChangedListener { +public class SearchAndFilterTab extends SaveAndRestoreTab implements DataChangeListener { public static final String SEARCH_AND_FILTER_TAB_ID = "SearchAndFilterTab"; private SearchAndFilterViewController searchAndFilterViewController; @@ -92,9 +93,9 @@ else if(clazz.isAssignableFrom(SearchResultTableViewController.class)){ setText(Messages.search); setGraphic(new ImageView(ImageCache.getImage(ImageCache.class, "/icons/sar-search_18x18.png"))); - setOnCloseRequest(event -> SaveAndRestoreService.getInstance().removeNodeChangeListener(this)); + setOnCloseRequest(event -> SaveAndRestoreService.getInstance().removeDataChangeListener(this)); - saveAndRestoreService.addNodeChangeListener(this); + saveAndRestoreService.addDataChangeListener(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 aa547b386d..d8ba92bcc2 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 @@ -51,6 +51,7 @@ 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.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.FilterChangeListener; import org.phoebus.applications.saveandrestore.ui.HelpViewer; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; @@ -78,7 +79,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -public class SearchAndFilterViewController extends SaveAndRestoreBaseController implements Initializable, FilterChangeListener { +public class SearchAndFilterViewController extends SaveAndRestoreBaseController implements Initializable, DataChangeListener { private final SaveAndRestoreController saveAndRestoreController; @@ -365,7 +366,7 @@ public void initialize(URL url, ResourceBundle resourceBundle) { loadFilters(); - saveAndRestoreService.addFilterChangeListener(this); + saveAndRestoreService.addDataChangeListener(this); progressIndicator.visibleProperty().bind(disableUi); disableUi.addListener((observable, oldValue, newValue) -> mainUi.setDisable(newValue)); @@ -633,7 +634,7 @@ public void filterRemoved(Filter filter) { } public void handleSaveAndFilterTabClosed() { - saveAndRestoreService.removeFilterChangeListener(this); + saveAndRestoreService.removeDataChangeListener(this); } @Override 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 f6a18175f3..a42af6d08b 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 @@ -136,9 +136,4 @@ public void editCompositeSnapshot(Node compositeSnapshotNode, List snapsho public void addToCompositeSnapshot(List snapshotNodes) { ((CompositeSnapshotController) controller).addToCompositeSnapshot(snapshotNodes); } - - @Override - public void nodeChanged(Node node) { - - } } 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 cf2d8e794d..32b6ea7934 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 @@ -29,6 +29,7 @@ import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.model.Tag; +import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; @@ -51,7 +52,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 { +public class SnapshotTab extends SaveAndRestoreTab implements DataChangeListener { public SaveAndRestoreService saveAndRestoreService; @@ -110,7 +111,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save if (controller != null && !((SnapshotController) controller).handleSnapshotTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeNodeChangeListener(this); + SaveAndRestoreService.getInstance().removeDataChangeListener(this); } }); @@ -126,7 +127,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save }); getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem); - SaveAndRestoreService.getInstance().addNodeChangeListener(this); + SaveAndRestoreService.getInstance().addDataChangeListener(this); } public void updateTabTitle(String name) { diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml index f3348a7d46..585ab1630c 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml @@ -42,7 +42,7 @@ -
+ + org.phoebus + core-ui + 5.0.1-SNAPSHOT + + + org.phoebus + core-websocket + 5.0.1-SNAPSHOT + org.phoebus save-and-restore-model 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 deea2242a0..8a5b390f6d 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 @@ -69,8 +69,6 @@ import org.phoebus.applications.saveandrestore.RestoreUtil; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; import org.phoebus.applications.saveandrestore.actions.OpenNodeAction; -import org.phoebus.applications.saveandrestore.client.Preferences; -import org.phoebus.applications.saveandrestore.client.WebSocketClient; import org.phoebus.applications.saveandrestore.filehandler.csv.CSVExporter; import org.phoebus.applications.saveandrestore.filehandler.csv.CSVImporter; import org.phoebus.applications.saveandrestore.model.Node; 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 542a244563..6b98ea0d32 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 @@ -28,7 +28,6 @@ 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.client.WebSocketClient; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; import org.phoebus.applications.saveandrestore.model.ConfigPv; import org.phoebus.applications.saveandrestore.model.Configuration; @@ -47,6 +46,7 @@ import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.model.websocket.WebMessageDeserializer; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.core.websocket.WebSocketClient; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.saveandrestore.util.VNoData; 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 index cf716bb300..0fe9d247bf 100644 --- 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 @@ -4,5 +4,6 @@ package org.phoebus.applications.saveandrestore.model.websocket; + public record SaveAndRestoreWebSocketMessage(MessageType messageType, T payload) { } diff --git a/core/pom.xml b/core/pom.xml index d5d3fe851b..4c3f6a6196 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,6 +21,7 @@ email launcher vtype + websocket org.phoebus diff --git a/core/websocket/pom.xml b/core/websocket/pom.xml new file mode 100644 index 0000000000..df4f7f6d93 --- /dev/null +++ b/core/websocket/pom.xml @@ -0,0 +1,20 @@ + + + + + 4.0.0 + core-websocket + + org.phoebus + core + 5.0.1-SNAPSHOT + + + + + + \ No newline at end of file diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java similarity index 98% rename from app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java rename to core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java index 1ccb325ebb..72a9d88e32 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -2,7 +2,7 @@ * Copyright (C) 2025 European Spallation Source ERIC. */ -package org.phoebus.applications.saveandrestore.client; +package org.phoebus.core.websocket; import java.net.URI; import java.net.http.HttpClient; 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 index 696a794d8b..7e20ede0ff 100644 --- 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 @@ -67,7 +67,7 @@ public class WebSocket { */ public WebSocket(ObjectMapper objectMapper, WebSocketSession webSocketSession) { this.session = webSocketSession; - logger.log(Level.INFO, () -> "Opening web socket " + session.getUri() + " ID " + session.getId()); + 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"); @@ -205,17 +205,6 @@ public void handleTextMessage(TextMessage message) throws Exception { } } - /** - * @param name PV name for which to send an update - * @param value Current value - * @param last_value Previous value - * @param last_readonly Was the PV read-only? - * @param readonly Is the PV read-only? - */ - public void sendUpdate(final String name, final VType value, final VType last_value, final boolean last_readonly, final boolean readonly) { - - } - /** * Clears all PVs * @@ -234,7 +223,7 @@ public void dispose() { } catch (Throwable ex) { logger.log(Level.WARNING, "Error disposing " + getId(), ex); } - logger.log(Level.FINE, () -> "Web socket " + session.getId() + " closed"); + logger.log(Level.INFO, () -> "Web socket " + session.getId() + " closed"); lastClientMessage = 0; } 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 index c479280fe4..8f6eef716d 100644 --- 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 @@ -87,7 +87,7 @@ public void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMe */ @Override public void afterConnectionEstablished(@NonNull WebSocketSession session) { - logger.log(Level.INFO, "Opening web socket sesssion from remote " + session.getRemoteAddress().getAddress()); + logger.log(Level.INFO, "Opening web socket session from remote " + session.getRemoteAddress().getAddress()); WebSocket webSocket = new WebSocket(objectMapper, session); sockets.add(webSocket); } @@ -104,6 +104,7 @@ public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull Cl Optional webSocketOptional = sockets.stream().filter(webSocket -> webSocket.getId().equals(session.getId())).findFirst(); if (webSocketOptional.isPresent()) { + logger.log(Level.INFO, "Closing web socket session from remote " + session.getRemoteAddress().getAddress()); webSocketOptional.get().dispose(); sockets.remove(webSocketOptional.get()); } From e2560d84035efcbf475868765c47f43b4cf5320b Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 23 Apr 2025 10:02:31 +0200 Subject: [PATCH 07/43] Minor refactoring --- .../applications/saveandrestore/Messages.java | 3 ++ .../SaveAndRestoreInstance.java | 3 +- .../ui/SaveAndRestoreController.java | 1 + .../ui/SaveAndRestoreService.java | 13 +++++- .../saveandrestore/messages.properties | 2 + .../core/websocket/WebSocketClient.java | 45 +++++++++++++++++-- 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index b7c00bd22b..31c9ccd019 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -184,6 +184,9 @@ public class Messages { public static String updateNodeFailed; + public static String webSocketConnected; + public static String webSocketDisconnected; + static { NLS.initializeMessages(Messages.class); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java index e55c44e555..fe6a0f64b5 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java @@ -20,6 +20,7 @@ import javafx.fxml.FXMLLoader; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; +import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.framework.nls.NLS; import org.phoebus.framework.persistence.Memento; import org.phoebus.framework.spi.AppDescriptor; @@ -55,7 +56,7 @@ public SaveAndRestoreInstance(AppDescriptor appDescriptor) { loader.setLocation(SaveAndRestoreApplication.class.getResource("ui/SaveAndRestoreUI.fxml")); dockItem = new DockItem(this, loader.load()); } catch (Exception e) { - Logger.getLogger(SaveAndRestoreApplication.class.getName()).log(Level.SEVERE, "Failed loading fxml", e); + Logger.getLogger(SaveAndRestoreInstance.class.getName()).log(Level.SEVERE, "Failed loading fxml", e); } saveAndRestoreController = loader.getController(); 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 8a5b390f6d..3c710a548c 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 @@ -955,6 +955,7 @@ public void saveLocalState() { public void handleTabClosed() { saveLocalState(); + saveAndRestoreService.closeWebSocket(); //saveAndRestoreService.removeNodeChangeListener(this); //saveAndRestoreService.removeNodeAddedListener(this); //saveAndRestoreService.removeFilterChangeListener(this); 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 6b98ea0d32..4e0a2ec983 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 @@ -79,7 +79,7 @@ public class SaveAndRestoreService { private final SaveAndRestoreClient saveAndRestoreClient; private final ObjectMapper objectMapper; - private final WebSocketClient webSocketClient; + private WebSocketClient webSocketClient; private SaveAndRestoreService() { saveAndRestoreClient = new SaveAndRestoreClientImpl(); @@ -87,7 +87,8 @@ private SaveAndRestoreService() { String schema = baseUrl.startsWith("https") ? "wss" : "ws"; String webSocketUrl = schema + baseUrl.substring(baseUrl.indexOf("://", 0)) + "/web-socket"; URI webSocketUri = URI.create(webSocketUrl); - webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketDisconnect, this::handleWebSocketMessage); + webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketConnect, this::handleWebSocketDisconnect, this::handleWebSocketMessage); + webSocketClient.connect(); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -487,6 +488,10 @@ private void handleWebSocketDisconnect(){ System.out.println("Web socket disconnected"); } + private void handleWebSocketConnect(){ + System.out.println("Web socket connected"); + } + private void handleWebSocketMessage(CharSequence charSequence){ try { SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = @@ -501,4 +506,8 @@ private void handleWebSocketMessage(CharSequence charSequence){ throw new RuntimeException(e); } } + + public void closeWebSocket(){ + webSocketClient.close("Application shutdown"); + } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 0b54021a05..6376895747 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -263,6 +263,8 @@ unnamedSnapshot= updateConfigurationFailed=Failed to update configuration updateCompositeSnapshotFailed=Failed to update composite snapshot updateNodeFailed=Failed to update node +webSocketConnected=Web Socket Connected +webSocketDisconnected=Web Socket Disconnected 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 index 72a9d88e32..b08b90f8a9 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -9,6 +9,9 @@ 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; @@ -20,9 +23,12 @@ 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 URI uri; private Consumer onTextCallback; + private CountDownLatch countDownLatch; + private AtomicBoolean reconnectAborted = new AtomicBoolean(false); /** * @@ -30,10 +36,14 @@ public class WebSocketClient implements WebSocket.Listener { * @param disconnectCallback An optional {@link Runnable} called if the web socket is closed, e.g. if * peer closes it or due to network issues. */ - public WebSocketClient(URI uri, Runnable disconnectCallback, Consumer onTextCallback) { - this.disconnectCallback = disconnectCallback; + public WebSocketClient(URI uri, Runnable connectCallback, Runnable disconnectCallback, Consumer onTextCallback) { this.uri = uri; + this.connectCallback = connectCallback; + this.disconnectCallback = disconnectCallback; this.onTextCallback = onTextCallback; + } + + public void connect(){ try { webSocket = HttpClient.newBuilder() .build() @@ -48,6 +58,9 @@ public WebSocketClient(URI uri, Runnable disconnectCallback, Consumer onClose(WebSocket webSocket, if (disconnectCallback != null) { disconnectCallback.run(); } + if(statusCode != WebSocket.NORMAL_CLOSURE){ + new Thread(new ReconnectThread()).start(); + } return null; } @@ -81,12 +97,18 @@ public CompletionStage onClose(WebSocket webSocket, * is called. */ public void sendPing() { + logger.log(Level.INFO, "Sending ping"); webSocket.sendPing(ByteBuffer.allocate(0)); } @Override public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - logger.log(Level.FINE, "Got pong"); + logger.log(Level.INFO, "Got pong"); + if(countDownLatch != null){ + countDownLatch.countDown(); + reconnectAborted.set(true); + logger.log(Level.INFO, "Reconnect aborted"); + } return WebSocket.Listener.super.onPong(webSocket, message); } @@ -109,4 +131,21 @@ public CompletionStage onText(WebSocket webSocket, public void close(String reason){ webSocket.sendClose(1000, reason); } + + private class ReconnectThread implements Runnable{ + @Override + public void run(){ + reconnectAborted.set(false); + while(!reconnectAborted.get()){ + logger.log(Level.INFO, "Trying to reconnect"); + countDownLatch = new CountDownLatch(1); + sendPing(); + try { + countDownLatch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } } From 6b2eafc43034caaf360522aa6519024569e04be9 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 23 Apr 2025 10:29:11 +0200 Subject: [PATCH 08/43] Remove web socket reconnect logic --- .../core/websocket/WebSocketClient.java | 28 ------------------- 1 file changed, 28 deletions(-) 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 index b08b90f8a9..da3c2ceafa 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -9,8 +9,6 @@ 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; @@ -27,7 +25,6 @@ public class WebSocketClient implements WebSocket.Listener { private Runnable disconnectCallback; private URI uri; private Consumer onTextCallback; - private CountDownLatch countDownLatch; private AtomicBoolean reconnectAborted = new AtomicBoolean(false); /** @@ -86,9 +83,6 @@ public CompletionStage onClose(WebSocket webSocket, if (disconnectCallback != null) { disconnectCallback.run(); } - if(statusCode != WebSocket.NORMAL_CLOSURE){ - new Thread(new ReconnectThread()).start(); - } return null; } @@ -104,11 +98,6 @@ public void sendPing() { @Override public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { logger.log(Level.INFO, "Got pong"); - if(countDownLatch != null){ - countDownLatch.countDown(); - reconnectAborted.set(true); - logger.log(Level.INFO, "Reconnect aborted"); - } return WebSocket.Listener.super.onPong(webSocket, message); } @@ -131,21 +120,4 @@ public CompletionStage onText(WebSocket webSocket, public void close(String reason){ webSocket.sendClose(1000, reason); } - - private class ReconnectThread implements Runnable{ - @Override - public void run(){ - reconnectAborted.set(false); - while(!reconnectAborted.get()){ - logger.log(Level.INFO, "Trying to reconnect"); - countDownLatch = new CountDownLatch(1); - sendPing(); - try { - countDownLatch.await(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - } } From fbcef08ff7bced39a8cd360005e347d98c18fcda Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 23 Apr 2025 11:11:32 +0200 Subject: [PATCH 09/43] Send web socket message when tagging/untagging --- .../ui/SaveAndRestoreController.java | 2 +- .../web/controllers/TagController.java | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) 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 3c710a548c..59d1d9041a 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 @@ -1028,7 +1028,7 @@ public void configureTagContextMenu(final Menu tagMenu) { List selectedNodes = browserSelectionModel.getSelectedItems().stream().map(TreeItem::getValue).collect(Collectors.toList()); - //TagUtil.tag(tagMenu, selectedNodes, updatedNodes -> updatedNodes.forEach(this::nodeChanged)); + TagUtil.tag(tagMenu, selectedNodes, updatedNodes -> updatedNodes.forEach(this::nodeChanged)); } /** 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 56c9b2057a..cb4b306a20 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,7 +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.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -32,6 +35,7 @@ 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; @@ -49,6 +53,9 @@ public class TagController extends BaseController { @Autowired private NodeDAO nodeDAO; + @Autowired + private WebSocketHandler webSocketHandler; + /** * @return A {@link List} of all {@link Tag}s. */ @@ -70,7 +77,9 @@ public List getTags() { public List addTag(@RequestBody TagData tagData, Principal principal) { tagData.getTag().setUserName(principal.getName()); - return nodeDAO.addTag(tagData); + List taggedNodes = nodeDAO.addTag(tagData); + taggedNodes.forEach(n -> webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, n))); + return taggedNodes; } /** @@ -83,6 +92,8 @@ public List addTag(@RequestBody TagData tagData, @DeleteMapping("/tags") @PreAuthorize("@authorizationHelper.mayAddOrDeleteTag(#tagData, #root)") public List deleteTag(@RequestBody TagData tagData) { - return nodeDAO.deleteTag(tagData); + List untaggedNodes = nodeDAO.deleteTag(tagData); + untaggedNodes.forEach(n -> webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, n))); + return untaggedNodes; } } From 80233d7ee6409c6f9febc002870178ac57f51a2a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 23 Apr 2025 11:26:42 +0200 Subject: [PATCH 10/43] Let SaveAndRestoreController request web socket connection on initialization --- .../saveandrestore/ui/SaveAndRestoreController.java | 1 + .../saveandrestore/ui/SaveAndRestoreService.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 59d1d9041a..af1501edc3 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 @@ -263,6 +263,7 @@ public void initialize(URL url, ResourceBundle resourceBundle) { treeNodeComparator = Comparator.comparing(TreeItem::getValue); saveAndRestoreService = SaveAndRestoreService.getInstance(); + saveAndRestoreService.openWebSocket(); treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); treeView.getStylesheets().add(getClass().getResource("/save-and-restore-style.css").toExternalForm()); 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 4e0a2ec983..34b84c219c 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 @@ -88,7 +88,6 @@ private SaveAndRestoreService() { String webSocketUrl = schema + baseUrl.substring(baseUrl.indexOf("://", 0)) + "/web-socket"; URI webSocketUri = URI.create(webSocketUrl); webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketConnect, this::handleWebSocketDisconnect, this::handleWebSocketMessage); - webSocketClient.connect(); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -510,4 +509,8 @@ private void handleWebSocketMessage(CharSequence charSequence){ public void closeWebSocket(){ webSocketClient.close("Application shutdown"); } + + public void openWebSocket(){ + webSocketClient.connect(); + } } From 892196b095b93d9c47c5a7e0dc1b88133a5616f4 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 23 Apr 2025 13:42:45 +0200 Subject: [PATCH 11/43] Web socket messages for filters (add/update/remove) --- .../saveandrestore/ui/DataChangeListener.java | 2 +- .../saveandrestore/ui/SaveAndRestoreController.java | 9 ++++++--- .../saveandrestore/ui/SaveAndRestoreService.java | 4 ++-- .../ui/search/SearchAndFilterViewController.java | 2 +- .../model/websocket/WebMessageDeserializer.java | 8 +++----- .../web/controllers/FilterController.java | 11 ++++++++++- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java index 5f264b2f42..262d6baf29 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java @@ -18,6 +18,6 @@ default void nodeChanged(Node node){ default void filterAddedOrUpdated(Filter filter){ } - default void filterRemoved(Filter filter){ + default void filterRemoved(String filterName){ } } 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 af1501edc3..0c387c7172 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 @@ -1269,9 +1269,12 @@ public void filterAddedOrUpdated(Filter filter) { } @Override - public void filterRemoved(Filter filter) { - if (filtersList.contains(filter)) { - filtersList.remove(filter); + public void filterRemoved(String name) { + Optional filterOptional = filtersList.stream().filter(f -> f.getName().equals(name)).findFirst(); + if (filterOptional.isPresent()) { + Filter filterToRemove = new Filter(); + filterToRemove.setName(name); + filtersList.remove(filterToRemove); // If this is the active filter, de-select filter completely filterEnabledProperty.set(false); filtersComboBox.getSelectionModel().select(null); 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 34b84c219c..6f9ff0efee 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 @@ -345,7 +345,7 @@ public List getAllFilters() throws Exception { */ public void deleteFilter(final Filter filter) throws Exception { executor.submit(() -> saveAndRestoreClient.deleteFilter(filter.getName())).get(); - dataChangeListeners.forEach(l -> l.filterRemoved(filter)); + dataChangeListeners.forEach(l -> l.filterRemoved(filter.getName())); } /** @@ -499,7 +499,7 @@ private void handleWebSocketMessage(CharSequence charSequence){ case NODE_ADDED, NODE_REMOVED -> dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload())); case NODE_UPDATED -> dataChangeListeners.forEach(l -> l.nodeChanged((Node)saveAndRestoreWebSocketMessage.payload())); case FILTER_ADDED_OR_UPDATED -> dataChangeListeners.forEach(l -> l.filterAddedOrUpdated((Filter)saveAndRestoreWebSocketMessage.payload())); - case FILTER_REMOVED -> dataChangeListeners.forEach(l -> l.filterRemoved((Filter)saveAndRestoreWebSocketMessage.payload())); + case FILTER_REMOVED -> dataChangeListeners.forEach(l -> l.filterRemoved((String)saveAndRestoreWebSocketMessage.payload())); } } catch (JsonProcessingException e) { throw new RuntimeException(e); 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 6c8f09b4d6..880b42904b 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 @@ -646,7 +646,7 @@ public void filterAddedOrUpdated(Filter filter) { } @Override - public void filterRemoved(Filter filter) { + public void filterRemoved(String name) { loadFilters(); } 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 index 423aaa7440..49bd41ef1c 100644 --- 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 @@ -28,10 +28,9 @@ public SaveAndRestoreWebSocketMessage deserialize(JsonParser j JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); String messageType = rootNode.get("messageType").asText(); switch (MessageType.valueOf(messageType)) { - case NODE_ADDED, NODE_REMOVED -> { + case NODE_ADDED, NODE_REMOVED, FILTER_REMOVED-> { SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = - objectMapper.readValue(rootNode.toString(), new TypeReference<>() { - }); + objectMapper.readValue(rootNode.toString(), SaveAndRestoreWebSocketMessage.class); return saveAndRestoreWebSocketMessage; } case NODE_UPDATED -> { @@ -39,12 +38,11 @@ public SaveAndRestoreWebSocketMessage deserialize(JsonParser j }); return saveAndRestoreWebSocketMessage; } - case FILTER_REMOVED, FILTER_ADDED_OR_UPDATED -> { + case FILTER_ADDED_OR_UPDATED -> { SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { }); return saveAndRestoreWebSocketMessage; } - } } catch (Exception e) { return null; 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 cac134bfad..57e34face3 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,7 +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.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -39,6 +42,9 @@ public class FilterController extends BaseController { @Autowired private NodeDAO nodeDAO; + @Autowired + private WebSocketHandler webSocketHandler; + /** * Saves a new or updated {@link Filter}. * @@ -52,7 +58,9 @@ public class FilterController extends BaseController { public Filter saveFilter(@RequestBody final Filter filter, Principal principal) { filter.setUser(principal.getName()); - return nodeDAO.saveFilter(filter); + Filter savedFilter = nodeDAO.saveFilter(filter); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.FILTER_ADDED_OR_UPDATED, filter)); + return savedFilter; } /** @@ -75,5 +83,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)); } } From 749c2784d76d7ea907d6bcbde7bb3d5f5c655f14 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 24 Apr 2025 07:55:38 +0200 Subject: [PATCH 12/43] Update unit tests --- services/save-and-restore/pom.xml | 7 ++ .../websocket/WebSocketHandler.java | 4 + .../web/config/ControllersTestConfig.java | 1 + .../web/controllers/FilterControllerTest.java | 112 ++++++++++++++++-- .../web/controllers/NodeControllerTest.java | 38 +++--- 5 files changed, 126 insertions(+), 36 deletions(-) diff --git a/services/save-and-restore/pom.xml b/services/save-and-restore/pom.xml index 47ddba0c57..e87b61d6fa 100644 --- a/services/save-and-restore/pom.xml +++ b/services/save-and-restore/pom.xml @@ -86,6 +86,13 @@ 5.0.1-SNAPSHOT + + org.phoebus + core-websocket + 5.0.1-SNAPSHOT + test + + org.apache.logging.log4j log4j-api 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 index 8f6eef716d..ae99b57d1e 100644 --- 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 @@ -59,6 +59,10 @@ public class WebSocketHandler extends TextWebSocketHandler { private final Logger logger = Logger.getLogger(WebSocketHandler.class.getName()); + public WebSocketHandler(){ + System.out.println(); + } + /** * Handles text message from web socket client * 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 cea7756d80..fa8b973151 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 @@ -141,4 +141,5 @@ public WebSocketSession webSocketSession(){ public WebSocketHandler webSocketHandler(){ return Mockito.mock(WebSocketHandler.class); } + } 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 dccca5a33e..3d268e201b 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 @@ -20,12 +20,15 @@ package org.phoebus.service.saveandrestore.web.controllers; import com.fasterxml.jackson.databind.ObjectMapper; +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.search.Filter; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; @@ -40,6 +43,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +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.*; @@ -73,10 +78,16 @@ public class FilterControllerTest { @Autowired private String demoUser; - @Test - public void testSaveFilter() throws Exception { + @Autowired + private WebSocketHandler webSocketHandler; - reset(nodeDAO); + @AfterEach + public void resetMocks(){ + reset(webSocketHandler,nodeDAO); + } + + @Test + public void testSaveFilter1() throws Exception { Filter filter = new Filter(); filter.setName("name"); @@ -99,25 +110,68 @@ public void testSaveFilter() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, Filter.class); - request = put("/filter") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testSaveFilter2() throws Exception { + + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); + filter.setUser("user"); + + String filterString = objectMapper.writeValueAsString(filter); + + MockHttpServletRequestBuilder request = put("/filter") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(filterString); mockMvc.perform(request).andExpect(status().isOk()); - request = put("/filter") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + + } + + @Test + public void testSaveFiliter3() throws Exception { + + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); + filter.setUser("user"); + + String filterString = objectMapper.writeValueAsString(filter); + MockHttpServletRequestBuilder request = put("/filter") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON) .content(filterString); mockMvc.perform(request).andExpect(status().isForbidden()); - request = put("/filter") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + + } + + @Test + public void testSaveFiliter4() throws Exception{ + + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); + filter.setUser("user"); + + String filterString = objectMapper.writeValueAsString(filter); + + MockHttpServletRequestBuilder request = put("/filter") .contentType(JSON) .content(filterString); mockMvc.perform(request).andExpect(status().isUnauthorized()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } @Test @@ -134,28 +188,60 @@ public void testDeleteFilter() throws Exception { .contentType(JSON); mockMvc.perform(request).andExpect(status().isOk()); - request = delete("/filter/name") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + + } + + @Test + public void testDeleteFilter2() throws Exception { + + MockHttpServletRequestBuilder request = delete("/filter/name") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON); mockMvc.perform(request).andExpect(status().isOk()); - request = delete("/filter/name") - .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) - .contentType(JSON); - mockMvc.perform(request).andExpect(status().isForbidden()); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + + } - request = delete("/filter/name") + @Test + public void testDeleteFilter3() throws Exception { + + 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)); + + } + + @Test + public void testDeleteFilter4() throws Exception { + MockHttpServletRequestBuilder request = delete("/filter/name") .contentType(JSON); mockMvc.perform(request).andExpect(status().isUnauthorized()); + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + + } + + @Test + public void testDeleteFilter5() throws Exception{ + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); filter.setUser("notUser"); when(nodeDAO.getAllFilters()).thenReturn(List.of(filter)); - request = delete("/filter/name") + MockHttpServletRequestBuilder request = delete("/filter/name") .header(HttpHeaders.AUTHORIZATION, userAuthorization) .contentType(JSON); mockMvc.perform(request).andExpect(status().isForbidden()); + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + + } @Test 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 9f5b69800d..2f3bec0b5e 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 @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +30,7 @@ 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.service.saveandrestore.NodeNotFoundException; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; @@ -114,6 +116,11 @@ public static void setUp() { folderFromClient = Node.builder().name("SomeFolder").userName("myusername").uniqueId("11").build(); } + @AfterEach + public void resetMocks(){ + reset(webSocketHandler, nodeDAO); + } + @Test public void testCreateFolder() throws Exception { @@ -168,8 +175,6 @@ public void testCreateFolderParentIdDoesNotExist() throws Exception { @Test public void testCreateConfig() throws Exception { - reset(nodeDAO); - Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh") .userName("user").build(); @@ -192,7 +197,6 @@ public void testCreateConfig() throws Exception { @Test public void testUpdateConfig() throws Exception { - reset(nodeDAO); Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh") .userName("user").build(); @@ -247,7 +251,6 @@ public void testUpdateConfig() throws Exception { @Test public void testCreateNodeBadRequests() throws Exception { - reset(nodeDAO); Node config = Node.builder().nodeType(NodeType.FOLDER).uniqueId("hhh") .userName("valid").build(); @@ -268,7 +271,6 @@ public void testCreateNodeBadRequests() throws Exception { @Test public void testGetChildNodes() throws Exception { - reset(nodeDAO); when(nodeDAO.getChildNodes("p")).thenAnswer((Answer>) invocation -> Collections.singletonList(config1)); @@ -286,7 +288,6 @@ public void testGetChildNodes() throws Exception { @Test public void testGetChildNodesNonExistingNode() throws Exception { - reset(nodeDAO); when(nodeDAO.getChildNodes("non-existing")).thenThrow(NodeNotFoundException.class); MockHttpServletRequestBuilder request = get("/node/non-existing/children").contentType(JSON); @@ -347,8 +348,9 @@ public void testDeleteFolder() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b"); + verify(webSocketHandler, times(1)).sendMessage(saveAndRestoreWebSocketMessage); } @Test @@ -362,7 +364,7 @@ public void testDeleteForbiddenAccess() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); } @Test @@ -378,13 +380,12 @@ public void testDeleteForbiddenAccess2() throws Exception { .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.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(Mockito.any(SaveAndRestoreWebSocketMessage.class)); - + verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); } @@ -419,7 +420,7 @@ 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(webSocketHandler, times(1)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); } @Test @@ -434,7 +435,7 @@ public void testDeleteForbidden3() throws Exception{ .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); } @Test @@ -449,7 +450,7 @@ public void testDeleteForbidden4() throws Exception{ .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); } @@ -491,8 +492,6 @@ public void testGetFolder() throws Exception { @Test public void testGetConfiguration() throws Exception { - Mockito.reset(nodeDAO); - when(nodeDAO.getNode("a")).thenReturn(Node.builder().build()); MockHttpServletRequestBuilder request = get("/node/a"); @@ -506,7 +505,6 @@ public void testGetConfiguration() throws Exception { @Test public void testGetNonExistingConfiguration() throws Exception { - Mockito.reset(nodeDAO); when(nodeDAO.getNode("a")).thenThrow(NodeNotFoundException.class); MockHttpServletRequestBuilder request = get("/node/a"); @@ -518,8 +516,6 @@ public void testGetNonExistingConfiguration() throws Exception { @Test public void testGetNonExistingFolder() throws Exception { - - Mockito.reset(nodeDAO); when(nodeDAO.getNode("a")).thenThrow(NodeNotFoundException.class); MockHttpServletRequestBuilder request = get("/node/a"); @@ -548,8 +544,6 @@ public void testGetFolderIllegalArgument() throws Exception { @Test public void testUpdateNode() throws Exception { - reset(nodeDAO); - Node node = Node.builder().name("foo").uniqueId("a").userName(demoUser).build(); when(nodeDAO.getNode("a")).thenReturn(node); @@ -704,7 +698,6 @@ public void testCreateNodeWithValidTags1() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - reset(nodeDAO); } @Test @@ -728,6 +721,5 @@ public void testCreateNodeWithValidTags2() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - reset(nodeDAO); } } From a2be6e207ba59bf259a108e3f44a4eedb7274fed Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 24 Apr 2025 11:07:22 +0200 Subject: [PATCH 13/43] Fix unit test configuration --- .../persistence/dao/impl/DAOTestIT.java | 53 +++++++----- .../web/config/WebSocketConfig.java | 81 ------------------- .../web/controllers/NodeControllerTest.java | 3 - 3 files changed, 35 insertions(+), 102 deletions(-) delete mode 100644 services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebSocketConfig.java diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java index dcf3b05e5a..c36dcee8c6 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java @@ -22,20 +22,35 @@ import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest; import co.elastic.clients.elasticsearch.indices.ExistsRequest; import co.elastic.clients.transport.endpoints.BooleanResponse; -import org.epics.vtype.*; +import org.epics.vtype.Alarm; +import org.epics.vtype.AlarmSeverity; +import org.epics.vtype.AlarmStatus; +import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VDouble; +import org.epics.vtype.VInt; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.phoebus.applications.saveandrestore.model.*; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; +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.Snapshot; +import org.phoebus.applications.saveandrestore.model.SnapshotData; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.applications.saveandrestore.model.Tag; +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.service.saveandrestore.NodeNotFoundException; import org.phoebus.service.saveandrestore.persistence.config.ElasticConfig; -import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ConfigurationDataRepository; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchDAO; -import org.phoebus.service.saveandrestore.web.config.WebSocketConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @@ -44,15 +59,23 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.socket.client.WebSocketClient; -import org.springframework.web.socket.client.WebSocketConnectionManager; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; import java.io.IOException; import java.time.Instant; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** * Integration test to be executed against a running Elasticsearch 8.x instance. @@ -60,7 +83,7 @@ */ @TestInstance(Lifecycle.PER_CLASS) @SpringBootTest -@ContextConfiguration(classes = {ElasticConfig.class, WebSocketConfig.class}) +@ContextConfiguration(classes = {ElasticConfig.class}) @TestPropertySource(locations = "classpath:test_application.properties") @Profile("IT") @SuppressWarnings("unused") @@ -69,12 +92,6 @@ public class DAOTestIT { @Autowired private ElasticsearchDAO nodeDAO; - @Autowired - private ConfigurationDataRepository configurationDataRepository; - - @Autowired - private WebSocketConfig.TestWebSocketHandler testWebSocketHandler; - @Autowired private ElasticsearchClient client; @@ -2266,7 +2283,7 @@ public void testFilters() { } @Test - public void testSearchForPvs() { + public void testSearchForPvs() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = Node.builder().name("folder").build(); diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebSocketConfig.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebSocketConfig.java deleted file mode 100644 index 33683f842f..0000000000 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebSocketConfig.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.service.saveandrestore.web.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.service.saveandrestore.websocket.WebSocket; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.lang.NonNull; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.client.WebSocketConnectionManager; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -import java.util.logging.Level; -import java.util.logging.Logger; - -@Configuration -@ComponentScan(basePackages = {"org.phoebus.service.saveandrestore"}) -@PropertySource("classpath:application.properties") -public class WebSocketConfig { - - private WebSocketConnectionManager webSocketConnectionManager; - private TestWebSocketHandler testWebSocketHandler; - - @Bean - public WebSocketConnectionManager webSocketConnectionManager() { - testWebSocketHandler = new TestWebSocketHandler(); - webSocketConnectionManager = - new WebSocketConnectionManager(new StandardWebSocketClient(), - testWebSocketHandler, - "ws://localhost:8080/web-socket", - new Object[]{}); - return webSocketConnectionManager; - } - - @Bean - public TestWebSocketHandler testWebSocketHandler() { - return testWebSocketHandler; - } - - public static class TestWebSocketHandler extends TextWebSocketHandler { - - private WebSocket webSocket; - - private final Logger logger = Logger.getLogger(org.phoebus.service.saveandrestore.websocket.WebSocketHandler.class.getName()); - private ObjectMapper objectMapper = new ObjectMapper(); - - - @Override - public void afterConnectionEstablished(@NonNull WebSocketSession session) { - logger.log(Level.INFO, "Opening web socket sesssion from remote " + session.getRemoteAddress().getAddress()); - webSocket = new WebSocket(objectMapper, session); - } - - @Override - public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) { - if (webSocket != null) { - webSocket.dispose(); - } - } - - @Override - public void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) { - try { - if (webSocket != null) { - webSocket.handleTextMessage(message); - } - } catch (final Exception ex) { - logger.log(Level.WARNING, ex, () -> "Error for message " + message.getPayload()); - } - } - - } -} 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 2f3bec0b5e..a5f6de6f1e 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 @@ -35,7 +35,6 @@ 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.WebSocketConfig; import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -46,8 +45,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; import java.util.Arrays; import java.util.Collections; From ed7b628be53ef8139df8d712e9a52f642b892b11 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 24 Apr 2025 12:58:44 +0200 Subject: [PATCH 14/43] Handle configuration update web socket messages, update unit tests --- .../ui/SaveAndRestoreController.java | 30 +++-- .../ui/SaveAndRestoreService.java | 18 +++ .../ui/WebSocketMessageHandler.java | 12 ++ .../ConfigurationController.java | 20 ++- .../ui/configuration/ConfigurationTab.java | 28 +++-- .../controllers/ConfigurationController.java | 4 +- .../ConfigurationControllerTest.java | 119 +++++++++++++++--- 7 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketMessageHandler.java 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 0c387c7172..7fa4ab34ae 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 @@ -137,7 +137,7 @@ * Main controller for the save and restore UI. */ public class SaveAndRestoreController extends SaveAndRestoreBaseController - implements Initializable, DataChangeListener { + implements Initializable, WebSocketMessageHandler { @FXML protected TreeView treeView; @@ -295,7 +295,8 @@ public void initialize(URL url, ResourceBundle resourceBundle) { //saveAndRestoreService.addNodeChangeListener(this); //saveAndRestoreService.addNodeAddedListener(this); //saveAndRestoreService.addFilterChangeListener(this); - saveAndRestoreService.addDataChangeListener(this); + //saveAndRestoreService.addDataChangeListener(this); + saveAndRestoreService.addWebSocketMessageHandler(this); treeView.setCellFactory(p -> new BrowserTreeCell(this)); treeViewPane.disableProperty().bind(disabledUi); @@ -816,8 +817,7 @@ public boolean isLeaf() { * @param node The updated node. */ - @Override - public void nodeChanged(Node node) { + private void nodeChanged(Node node) { // Find the node that has changed TreeItem nodeSubjectToUpdate = recursiveSearch(node.getUniqueId(), treeView.getRoot()); if (nodeSubjectToUpdate == null) { @@ -842,8 +842,7 @@ public void nodeChanged(Node node) { * @param parentNodeId Unique id of the parent {@link Node} */ - @Override - public void nodeAddedOrRemoved(String parentNodeId){ + private void nodeAddedOrRemoved(String parentNodeId){ // Find the parent to which the new node is to be added TreeItem parentTreeItem = recursiveSearch(parentNodeId, treeView.getRoot()); if (parentTreeItem == null) { @@ -957,6 +956,7 @@ public void saveLocalState() { public void handleTabClosed() { saveLocalState(); saveAndRestoreService.closeWebSocket(); + saveAndRestoreService.removeWebSocketMessageHandler(this); //saveAndRestoreService.removeNodeChangeListener(this); //saveAndRestoreService.removeNodeAddedListener(this); //saveAndRestoreService.removeFilterChangeListener(this); @@ -1251,8 +1251,7 @@ public Node[] getConfigAndSnapshotForActiveSnapshotTab() { return null; } - @Override - public void filterAddedOrUpdated(Filter filter) { + private void filterAddedOrUpdated(Filter filter) { if (!filtersList.contains(filter)) { filtersList.add(filter); } else { @@ -1268,14 +1267,13 @@ public void filterAddedOrUpdated(Filter filter) { } } - @Override - public void filterRemoved(String name) { + private void filterRemoved(String name) { Optional filterOptional = filtersList.stream().filter(f -> f.getName().equals(name)).findFirst(); if (filterOptional.isPresent()) { Filter filterToRemove = new Filter(); filterToRemove.setName(name); filtersList.remove(filterToRemove); - // If this is the active filter, de-select filter completely + // If this is the active filter, unselect it filterEnabledProperty.set(false); filtersComboBox.getSelectionModel().select(null); // And refresh tree view @@ -1527,4 +1525,14 @@ private void addOptionalLoggingMenuItem() { } } + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ + switch (saveAndRestoreWebSocketMessage.messageType()){ + case NODE_ADDED, NODE_REMOVED -> nodeAddedOrRemoved((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()); + } + } + } 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 6f9ff0efee..de18562886 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 @@ -72,6 +72,7 @@ public class SaveAndRestoreService { private final ExecutorService executor; private final List dataChangeListeners = Collections.synchronizedList(new ArrayList<>()); + private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); private static final Logger LOG = Logger.getLogger(SaveAndRestoreService.class.getName()); private static SaveAndRestoreService instance; @@ -492,6 +493,7 @@ private void handleWebSocketConnect(){ } private void handleWebSocketMessage(CharSequence charSequence){ + /* try { SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); @@ -503,6 +505,14 @@ private void handleWebSocketMessage(CharSequence charSequence){ } } catch (JsonProcessingException e) { throw new RuntimeException(e); + }*/ + + try { + SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = + objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); + webSocketMessageHandlers.forEach(w -> w.handleWebSocketMessage(saveAndRestoreWebSocketMessage)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } } @@ -513,4 +523,12 @@ public void closeWebSocket(){ public void openWebSocket(){ webSocketClient.connect(); } + + public void addWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler){ + webSocketMessageHandlers.add(webSocketMessageHandler); + } + + public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler){ + webSocketMessageHandlers.remove(webSocketMessageHandler); + } } 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 new file mode 100644 index 0000000000..18ae939fd6 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketMessageHandler.java @@ -0,0 +1,12 @@ +/* + * 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 80917346a1..48b8ab8537 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 @@ -54,10 +54,13 @@ 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.search.Filter; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.NodeChangedListener; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; +import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.core.types.ProcessVariable; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; @@ -76,7 +79,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -public class ConfigurationController extends SaveAndRestoreBaseController implements DataChangeListener { +public class ConfigurationController extends SaveAndRestoreBaseController implements WebSocketMessageHandler { @FXML private BorderPane root; @@ -265,7 +268,9 @@ public void updateItem(String item, boolean empty) { addPVsPane.disableProperty().bind(userIdentity.isNull()); - SaveAndRestoreService.getInstance().addDataChangeListener(this); + saveAndRestoreService.addWebSocketMessageHandler(this); + + dirty.addListener((obs, o, n) -> configurationTab.annotateDirty(n)); } @FXML @@ -400,8 +405,7 @@ public boolean handleConfigurationTabClosed() { return true; } - @Override - public void nodeChanged(Node node) { + private void nodeChanged(Node node) { if (node.getUniqueId().equals(configurationNode.get().getUniqueId())) { configurationNode.setValue(Node.builder().uniqueId(node.getUniqueId()) .name(node.getName()) @@ -413,4 +417,12 @@ public void nodeChanged(Node node) { .build()); } } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ + switch (saveAndRestoreWebSocketMessage.messageType()){ + //case NODE_ADDED, NODE_REMOVED -> nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload()); + case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); + } + } } 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 90373ed6c0..1a3fddf363 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 @@ -23,17 +23,21 @@ 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.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.ImageRepository; 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 java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; -public class ConfigurationTab extends SaveAndRestoreTab implements DataChangeListener { +public class ConfigurationTab extends SaveAndRestoreTab implements WebSocketMessageHandler { + + private String originalConfigName; public ConfigurationTab() { configure(); @@ -70,11 +74,11 @@ private void configure() { if (!((ConfigurationController) controller).handleConfigurationTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeDataChangeListener(this); + SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); } }); - SaveAndRestoreService.getInstance().addDataChangeListener(this); + SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); } /** @@ -83,20 +87,22 @@ private void configure() { * @param configurationNode non-null configuration {@link Node} */ public void editConfiguration(Node configurationNode) { + originalConfigName = configurationNode.getName(); setId(configurationNode.getUniqueId()); textProperty().set(configurationNode.getName()); ((ConfigurationController) controller).loadConfiguration(configurationNode); } public void configureForNewConfiguration(Node parentNode) { + originalConfigName = Messages.contextMenuNewConfiguration; textProperty().set(Messages.contextMenuNewConfiguration); ((ConfigurationController) controller).newConfiguration(parentNode); } - @Override - public void nodeChanged(Node node) { + private void nodeChanged(Node node) { if (node.getUniqueId().equals(getId())) { textProperty().set(node.getName()); + originalConfigName = node.getName(); } } @@ -112,9 +118,17 @@ public void updateTabTitle(String tabTitle) { public void annotateDirty(boolean dirty) { String tabTitle = textProperty().get(); if (dirty && !tabTitle.contains("*")) { - updateTabTitle("* " + tabTitle); + updateTabTitle("* " + originalConfigName); } else if (!dirty) { - updateTabTitle(tabTitle.substring(2)); + updateTabTitle(originalConfigName); + } + } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ + switch (saveAndRestoreWebSocketMessage.messageType()){ + //case NODE_ADDED, NODE_REMOVED -> nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload()); + case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); } } } 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 76a355670e..5e188b7965 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 @@ -94,6 +94,8 @@ public ConfigurationData getConfigurationData(@PathVariable String uniqueId) { public Configuration updateConfiguration(@RequestBody Configuration configuration, Principal principal) { configuration.getConfigurationNode().setUserName(principal.getName()); - return nodeDAO.updateConfiguration(configuration); + Configuration updatedConfiguration = nodeDAO.updateConfiguration(configuration); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedConfiguration.getConfigurationNode())); + return updatedConfiguration; } } 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 4ab5544360..020cb8cb95 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 @@ -20,13 +20,17 @@ package org.phoebus.service.saveandrestore.web.controllers; import com.fasterxml.jackson.databind.ObjectMapper; +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.Configuration; 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.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; @@ -37,6 +41,8 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +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.post; @@ -70,10 +76,16 @@ public class ConfigurationControllerTest { @Autowired private String demoUser; - @Test - public void testCreateConfiguration() throws Exception { + @Autowired + private WebSocketHandler webSocketHandler; - reset(nodeDAO); + @AfterEach + public void resetMocks(){ + reset(nodeDAO, webSocketHandler); + } + + @Test + public void testCreateConfiguration1() throws Exception { Configuration configuration = new Configuration(); configuration.setConfigurationNode(Node.builder().build()); @@ -83,26 +95,56 @@ public void testCreateConfiguration() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); - request = put("/config?parentNodeId=a") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateConfiguration2() throws Exception { + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(Node.builder().build()); + + MockHttpServletRequestBuilder request = put("/config?parentNodeId=a") .header(HttpHeaders.AUTHORIZATION, userAuthorization) .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isOk()); - request = put("/config?parentNodeId=a") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateConfiguration3() throws Exception { + + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(Node.builder().build()); + + MockHttpServletRequestBuilder request = put("/config?parentNodeId=a") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isForbidden()); - request = put("/config?parentNodeId=a") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateConfiguration4() throws Exception{ + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(Node.builder().build()); + + MockHttpServletRequestBuilder request = put("/config?parentNodeId=a") .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isUnauthorized()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test - public void testUpdateConfiguration() throws Exception { + public void testUpdateConfiguration1() throws Exception { Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName(demoUser).build(); @@ -117,44 +159,91 @@ public void testUpdateConfiguration() throws Exception { mockMvc.perform(request).andExpect(status().isOk()); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void tesUpdateConfiguration2() throws Exception { + + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName(demoUser).build(); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(configurationNode); + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); - request = post("/config") + MockHttpServletRequestBuilder request = post("/config") .header(HttpHeaders.AUTHORIZATION, userAuthorization) .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isOk()); - configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateConfiguration3() throws Exception { + + Configuration configuration = new Configuration(); + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); configuration.setConfigurationNode(configurationNode); when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); - request = post("/config") + MockHttpServletRequestBuilder request = post("/config") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isOk()); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateConfiguration4() throws Exception { + + Configuration configuration = new Configuration(); + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); + configuration.setConfigurationNode(configurationNode); + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); - request = post("/config") + MockHttpServletRequestBuilder request = post("/config") .header(HttpHeaders.AUTHORIZATION, userAuthorization) .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isForbidden()); + } - request = post("/config") + @Test + public void testUpdateConfiguration5() throws Exception { + + Configuration configuration = new Configuration(); + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); + configuration.setConfigurationNode(configurationNode); + + MockHttpServletRequestBuilder request = post("/config") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); mockMvc.perform(request).andExpect(status().isForbidden()); - request = post("/config") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateConfiguration6() throws Exception{ + + Configuration configuration = new Configuration(); + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); + configuration.setConfigurationNode(configurationNode); + + MockHttpServletRequestBuilder request = post("/config") .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); - mockMvc.perform(request).andExpect(status().isUnauthorized() - ); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } } From 206d7242fef70298361d15cc7b73b6411de132b9 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 24 Apr 2025 14:09:01 +0200 Subject: [PATCH 15/43] Unregister listener when save&restore configuration UI is closed --- .../ConfigurationController.java | 5 +- .../ui/configuration/ConfigurationTab.java | 1 + .../controllers/SnapshotControllerTest.java | 140 ++++++++++++++++-- 3 files changed, 130 insertions(+), 16 deletions(-) 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 48b8ab8537..08f577404f 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 @@ -421,8 +421,11 @@ private void nodeChanged(Node node) { @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ switch (saveAndRestoreWebSocketMessage.messageType()){ - //case NODE_ADDED, NODE_REMOVED -> nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload()); case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); } } + + public void handleTabClosed(){ + saveAndRestoreService.removeWebSocketMessageHandler(this); + } } 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 1a3fddf363..cfed8a61e7 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 @@ -75,6 +75,7 @@ private void configure() { event.consume(); } else { SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); + ((ConfigurationController)controller).handleTabClosed(); } }); 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 5424ce6965..cae0b20b4f 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 @@ -20,6 +20,7 @@ package org.phoebus.service.saveandrestore.web.controllers; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; @@ -27,8 +28,10 @@ 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.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; @@ -40,7 +43,11 @@ 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; +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.delete; @@ -75,6 +82,14 @@ public class SnapshotControllerTest { @Autowired private String demoUser; + @Autowired + private WebSocketHandler webSocketHandler; + + @AfterEach + public void resetMocks(){ + reset(webSocketHandler, nodeDAO); + } + @Test public void testSaveSnapshotWrongNodeType() throws Exception { @@ -92,6 +107,8 @@ public void testSaveSnapshotWrongNodeType() throws Exception { .content(objectMapper.writeValueAsString(snapshot)); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test @@ -105,10 +122,12 @@ public void testSaveSnapshotNoParentNodeId() throws Exception { .content(objectMapper.writeValueAsString(snapshot)); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test - public void testCreateSnapshot() throws Exception { + public void testCreateSnapshot1() throws Exception { Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(node); @@ -130,7 +149,19 @@ public void testCreateSnapshot() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); - request = put("/snapshot?parentNodeId=a") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateSnapshot2() throws Exception { + + Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + MockHttpServletRequestBuilder request = put("/snapshot?parentNodeId=a") .contentType(JSON) .content(snapshotString); mockMvc.perform(request).andExpect(status().isUnauthorized()); @@ -141,15 +172,29 @@ public void testCreateSnapshot() throws Exception { .content(snapshotString); mockMvc.perform(request).andExpect(status().isForbidden()); - request = put("/snapshot?parentNodeId=a") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateSnapshot3() throws Exception{ + + Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + MockHttpServletRequestBuilder request = put("/snapshot?parentNodeId=a") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(snapshotString); mockMvc.perform(request).andExpect(status().isOk()); + + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test - public void testUpdateSnapshot() throws Exception { + public void testUpdateSnapshot1() throws Exception { Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(node); @@ -171,30 +216,72 @@ public void testUpdateSnapshot() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); - request = put("/snapshot") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateSnapshot2() throws Exception { + + Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + MockHttpServletRequestBuilder request = put("/snapshot") .contentType(JSON) .content(snapshotString); mockMvc.perform(request).andExpect(status().isUnauthorized()); - request = post("/snapshot") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateSnapshot3() throws Exception { + + Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + MockHttpServletRequestBuilder request = post("/snapshot") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON) .content(snapshotString); mockMvc.perform(request).andExpect(status().isForbidden()); - request = post("/snapshot") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateSnapshot4() throws Exception{ + + Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + MockHttpServletRequestBuilder request = post("/snapshot") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(snapshotString); mockMvc.perform(request).andExpect(status().isOk()); + + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test - public void testDeleteSnapshot() throws Exception{ + public void testDeleteSnapshot1() throws Exception { - when(nodeDAO.getNode("a")).thenReturn(Node.builder() + Node node = Node.builder() .nodeType(NodeType.SNAPSHOT) - .uniqueId("a").userName(demoUser).build()); + .uniqueId("a").userName(demoUser).build(); + + when(nodeDAO.getNode("a")).thenReturn(node); + + when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("a")); MockHttpServletRequestBuilder request = delete("/node") @@ -203,28 +290,51 @@ public void testDeleteSnapshot() throws Exception{ mockMvc.perform(request).andExpect(status().isOk()); - request = + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testDeleteSnapshot2() throws Exception { + + MockHttpServletRequestBuilder request = delete("/node") .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); mockMvc.perform(request).andExpect(status().isForbidden()); - when(nodeDAO.getNode("a")).thenReturn(Node.builder() + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testDeleteSnapshot3() throws Exception { + + Node node = Node.builder() .nodeType(NodeType.SNAPSHOT) - .uniqueId("a").userName("otherUser").build()); + .uniqueId("a").userName("otherUser").build(); + + when(nodeDAO.getNode("a")).thenReturn(node); + when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("a")); - request = + MockHttpServletRequestBuilder request = delete("/node") .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) .header(HttpHeaders.AUTHORIZATION, adminAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - request = + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testDeleteSnapshot4() throws Exception{ + + MockHttpServletRequestBuilder request = delete("/node") .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))); mockMvc.perform(request).andExpect(status().isUnauthorized()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } } From 09f2fa2b3c03eb94134e04cf861a528c6496d139 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 24 Apr 2025 15:32:48 +0200 Subject: [PATCH 16/43] Fix configuration changes in UI --- .../ui/SaveAndRestoreController.java | 9 ------- .../ConfigurationController.java | 27 +++++++------------ .../ui/configuration/ConfigurationTab.java | 16 +++++------ .../ui/snapshot/SnapshotController.java | 12 ++++++++- 4 files changed, 27 insertions(+), 37 deletions(-) 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 7fa4ab34ae..f8f659f962 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 @@ -824,15 +824,6 @@ private void nodeChanged(Node node) { return; } nodeSubjectToUpdate.setValue(node); - // Folder and configuration node changes may include structure changes, so expand to force update. - /* - if (nodeSubjectToUpdate.isExpanded() && (nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.FOLDER) || - nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.CONFIGURATION))) { - if (nodeSubjectToUpdate.getParent() != null) { // null means root folder as it has no parent - nodeSubjectToUpdate.getParent().getChildren().sort(treeNodeComparator); - } - expandTreeNode(nodeSubjectToUpdate); - }*/ } /** 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 08f577404f..bc64fb5113 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 @@ -294,6 +294,7 @@ public void saveConfiguration() { configurationData = configuration.getConfigurationData(); dirty.set(false); loadConfiguration(configuration.getConfigurationNode()); + configurationTab.updateTabTitle(configuration.getConfigurationNode().getName()); } catch (Exception e1) { ExceptionDetailsErrorDialog.openError(pvTable, Messages.errorActionFailed, @@ -369,15 +370,7 @@ public void loadConfiguration(final Node node) { ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); return; } - // Create a cloned Node object to avoid changes in the Node object contained in the tree view. - configurationNode.set(Node.builder().uniqueId(node.getUniqueId()) - .name(node.getName()) - .nodeType(NodeType.CONFIGURATION) - .description(node.getDescription()) - .userName(node.getUserName()) - .created(node.getCreated()) - .lastModified(node.getLastModified()) - .build()); + configurationNode.set(node); loadConfigurationData(); } @@ -407,21 +400,19 @@ public boolean handleConfigurationTabClosed() { private void nodeChanged(Node node) { if (node.getUniqueId().equals(configurationNode.get().getUniqueId())) { - configurationNode.setValue(Node.builder().uniqueId(node.getUniqueId()) - .name(node.getName()) - .nodeType(NodeType.CONFIGURATION) - .userName(node.getUserName()) - .description(node.getDescription()) - .created(node.getCreated()) - .lastModified(node.getLastModified()) - .build()); + configurationNode.setValue(node); } } @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ switch (saveAndRestoreWebSocketMessage.messageType()){ - case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); + case NODE_UPDATED -> { + Node node = (Node)saveAndRestoreWebSocketMessage.payload(); + if (node.getUniqueId().equals(configurationNode.get().getUniqueId())){ + 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 cfed8a61e7..45bc70b994 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 @@ -100,13 +100,6 @@ public void configureForNewConfiguration(Node parentNode) { ((ConfigurationController) controller).newConfiguration(parentNode); } - private void nodeChanged(Node node) { - if (node.getUniqueId().equals(getId())) { - textProperty().set(node.getName()); - originalConfigName = node.getName(); - } - } - /** * Updates tab title, e.g. if user has renamed the configuration. * @@ -128,8 +121,13 @@ public void annotateDirty(boolean dirty) { @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ switch (saveAndRestoreWebSocketMessage.messageType()){ - //case NODE_ADDED, NODE_REMOVED -> nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload()); - case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); + case NODE_UPDATED -> { + Node node = (Node)saveAndRestoreWebSocketMessage.payload(); + if(node.getUniqueId().equals(getId())){ + textProperty().set(node.getName()); + originalConfigName = node.getName(); + } + } } } } 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 bd47d6b792..71da8555df 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 @@ -31,9 +31,12 @@ import org.phoebus.applications.saveandrestore.model.SnapshotData; import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.model.event.SaveAndRestoreEventReceiver; +import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; 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.WebSocketMessageHandler; import org.phoebus.saveandrestore.util.VNoData; import org.phoebus.framework.jobs.JobManager; import org.phoebus.security.tokens.ScopedAuthenticationToken; @@ -56,7 +59,7 @@ * 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 { +public class SnapshotController extends SaveAndRestoreBaseController implements WebSocketMessageHandler { @SuppressWarnings("unused") @@ -455,4 +458,11 @@ private Snapshot getSnapshotFromService(Node snapshotNode) throws Exception { public void secureStoreChanged(List validTokens) { snapshotControlsViewController.secureStoreChanged(validTokens); } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ + switch (saveAndRestoreWebSocketMessage.messageType()){ + //case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); + } + } } \ No newline at end of file From e8a725676c5dc98e15e8ff87bf145739eaf7a6a7 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 24 Apr 2025 15:56:37 +0200 Subject: [PATCH 17/43] Fix issues in configuration update through web socket message --- .../ui/configuration/ConfigurationController.java | 2 +- .../saveandrestore/ui/configuration/ConfigurationTab.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 bc64fb5113..bf9ba1538b 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 @@ -375,7 +375,7 @@ public void loadConfiguration(final Node node) { } private void loadConfigurationData() { - UI_EXECUTOR.execute(() -> { + Platform.runLater(() -> { try { Collections.sort(configurationData.getPvList()); configurationEntries.setAll(configurationData.getPvList()); 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 45bc70b994..b35cedd43d 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 @@ -124,7 +124,7 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore case NODE_UPDATED -> { Node node = (Node)saveAndRestoreWebSocketMessage.payload(); if(node.getUniqueId().equals(getId())){ - textProperty().set(node.getName()); + updateTabTitle(node.getName()); originalConfigName = node.getName(); } } From 3bc92090cddd631f8cf3e642b630c45661cf11df Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 28 Apr 2025 12:39:39 +0200 Subject: [PATCH 18/43] Fix update configuration issue --- .../ui/SaveAndRestoreService.java | 18 +- .../ConfigurationController.java | 171 +++++++++--------- .../ui/configuration/ConfigurationTab.java | 50 +---- .../impl/elasticsearch/ElasticsearchDAO.java | 3 + 4 files changed, 96 insertions(+), 146 deletions(-) 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 de18562886..164fc09332 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 @@ -485,28 +485,14 @@ private VType readFromArchiver(String pvName, Instant time) { } private void handleWebSocketDisconnect(){ - System.out.println("Web socket disconnected"); + LOG.log(Level.INFO, "Web socket disonnected"); } private void handleWebSocketConnect(){ - System.out.println("Web socket connected"); + LOG.log(Level.INFO, "Web socket connected"); } private void handleWebSocketMessage(CharSequence charSequence){ - /* - try { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = - objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); - switch (saveAndRestoreWebSocketMessage.messageType()){ - case NODE_ADDED, NODE_REMOVED -> dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload())); - case NODE_UPDATED -> dataChangeListeners.forEach(l -> l.nodeChanged((Node)saveAndRestoreWebSocketMessage.payload())); - case FILTER_ADDED_OR_UPDATED -> dataChangeListeners.forEach(l -> l.filterAddedOrUpdated((Filter)saveAndRestoreWebSocketMessage.payload())); - case FILTER_REMOVED -> dataChangeListeners.forEach(l -> l.filterRemoved((String)saveAndRestoreWebSocketMessage.payload())); - } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - }*/ - try { SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); 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 03fc64089d..019b2bd871 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 @@ -23,7 +23,6 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -52,17 +51,19 @@ import javafx.util.converter.DoubleStringConverter; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; +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.MessageType; import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; -import org.phoebus.applications.saveandrestore.model.ComparisonMode; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.core.types.ProcessVariable; +import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -163,8 +164,6 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private SaveAndRestoreService saveAndRestoreService; - private static final Executor UI_EXECUTOR = Platform::runLater; - private final ObservableList configurationEntries = FXCollections.observableArrayList(); private final SimpleBooleanProperty selectionEmpty = new SimpleBooleanProperty(false); @@ -172,19 +171,19 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private final SimpleStringProperty configurationNameProperty = new SimpleStringProperty(); private Node configurationNodeParent; - private final SimpleObjectProperty configurationNode = new SimpleObjectProperty<>(); - - private final ConfigurationTab configurationTab; - - private ConfigurationData configurationData; - private final Logger logger = Logger.getLogger(ConfigurationController.class.getName()); private final BooleanProperty loadInProgress = new SimpleBooleanProperty(); private final BooleanProperty dirty = new SimpleBooleanProperty(); + private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(); + private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); + + private String configurationNodeId; + public ConfigurationController(ConfigurationTab configurationTab) { - this.configurationTab = configurationTab; + configurationTab.textProperty().bind(tabTitleProperty); + configurationTab.idProperty().bind(tabIdProperty); } @FXML @@ -192,8 +191,6 @@ public void initialize() { saveAndRestoreService = SaveAndRestoreService.getInstance(); - dirty.addListener((obs, o, n) -> configurationTab.annotateDirty(n)); - pvTable.editableProperty().bind(userIdentity.isNull().not()); pvTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); pvTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> selectionEmpty.set(nv == null)); @@ -202,7 +199,6 @@ public void initialize() { new ImageView(ImageCache.getImage(ConfigurationController.class, "/icons/delete.png"))); deleteMenuItem.setOnAction(ae -> { configurationEntries.removeAll(pvTable.getSelectionModel().getSelectedItems()); - //configurationTab.annotateDirty(true); pvTable.refresh(); }); @@ -256,10 +252,15 @@ public void initialize() { @Override public void commitEdit(ComparisonMode comparisonMode) { + ComparisonMode currentMode = getTableView().getItems().get(getIndex()).getComparisonModeProperty().get(); getTableView().getItems().get(getIndex()).setComparisonModeProperty(comparisonMode); if (comparisonMode == null) { getTableView().getItems().get(getIndex()).setToleranceProperty(null); } + // User has selected a mode that was previously null -> set tolerance to a default value. + else if (currentMode == null) { + getTableView().getItems().get(getIndex()).setToleranceProperty(0.0); + } setDirty(true); super.commitEdit(comparisonMode); } @@ -296,7 +297,7 @@ public String toString(Double value) { public Double fromString(String string) { try { double value = Double.parseDouble(string); - if(value >= 0){ + if (value >= 0) { // Tolerance must be >= 0. return value; } @@ -334,9 +335,12 @@ public void commitEdit(Double value) { } } }); + pvTable.setItems(configurationEntries); - configurationNameProperty.addListener((observableValue, oldValue, newValue) -> setDirty(!newValue.equals(configurationNode.getName()))); - configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> setDirty(!newValue.equals(configurationNode.get().getDescription()))); + configurationNameProperty.addListener((observableValue, oldValue, newValue) -> + setDirty(newValue != null)); + configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> + setDirty(newValue != null)); saveButton.disableProperty().bind(Bindings.createBooleanBinding(() -> dirty.not().get() || configurationDescriptionProperty.isEmpty().get() || @@ -345,61 +349,48 @@ public void commitEdit(Double value) { dirty, configurationDescriptionProperty, configurationNameProperty, userIdentity)); addPvButton.disableProperty().bind(Bindings.createBooleanBinding(() -> - pvNameField.textProperty().isEmpty().get() && - readbackPvNameField.textProperty().isEmpty().get(), + pvNameField.textProperty().isEmpty().get() && + readbackPvNameField.textProperty().isEmpty().get(), pvNameField.textProperty(), readbackPvNameField.textProperty())); readOnlyCheckBox.selectedProperty().bindBidirectional(readOnlyProperty); - configurationNode.addListener(observable -> { - if (observable != null) { - SimpleObjectProperty simpleObjectProperty = (SimpleObjectProperty) observable; - Node newValue = simpleObjectProperty.get(); - configurationNameProperty.set(newValue.getName()); - Platform.runLater(() -> { - configurationCreatedDateField.textProperty().set(newValue.getCreated() != null ? - TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(newValue.getCreated().getTime())) : null); - configurationLastModifiedDateField.textProperty().set(newValue.getLastModified() != null ? - TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(newValue.getLastModified().getTime())) : null); - createdByField.textProperty().set(newValue.getUserName()); - }); - configurationDescriptionProperty.set(configurationNode.get().getDescription()); - } - }); - addPVsPane.disableProperty().bind(userIdentity.isNull()); saveAndRestoreService.addWebSocketMessageHandler(this); - - dirty.addListener((obs, o, n) -> configurationTab.annotateDirty(n)); } @FXML @SuppressWarnings("unused") public void saveConfiguration() { - UI_EXECUTOR.execute(() -> { + JobManager.schedule("Save save&restore configuration", monitor -> { try { - configurationNode.get().setName(configurationNameProperty.get()); - configurationNode.get().setDescription(configurationDescriptionProperty.get()); + Node configurationNode = + Node.builder().nodeType(NodeType.CONFIGURATION) + .name(configurationNameProperty.get()) + .description(configurationDescriptionProperty.get()) + .uniqueId(configurationNodeId) + .build(); + ConfigurationData configurationData = new ConfigurationData(); configurationData.setPvList(configurationEntries.stream().map(ConfigPvEntry::toConfigPv).toList()); + configurationData.setUniqueId(configurationNodeId); Configuration configuration = new Configuration(); - configuration.setConfigurationNode(configurationNode.get()); + configuration.setConfigurationNode(configurationNode); configuration.setConfigurationData(configurationData); - if (configurationNode.get().getUniqueId() == null) { // New configuration + if (configurationNodeId == null) { // New configuration configuration = saveAndRestoreService.createConfiguration(configurationNodeParent, configuration); - configurationTab.setId(configuration.getConfigurationNode().getUniqueId()); + tabIdProperty.setValue(configuration.getConfigurationNode().getUniqueId()); } else { configuration = saveAndRestoreService.updateConfiguration(configuration); } - configurationData = configuration.getConfigurationData(); loadConfiguration(configuration.getConfigurationNode()); - configurationTab.updateTabTitle(configuration.getConfigurationNode().getName()); + dirty.setValue(false); } catch (Exception e1) { - ExceptionDetailsErrorDialog.openError(pvTable, + Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(pvTable, Messages.errorActionFailed, Messages.errorCreateConfigurationFailed, - e1); + e1)); } }); } @@ -408,12 +399,12 @@ public void saveConfiguration() { @SuppressWarnings("unused") public void addPv() { - UI_EXECUTOR.execute(() -> { + Platform.runLater(() -> { // Process a list of space or semicolon separated pvs String[] pvNames = pvNameProperty.get().trim().split("[\\s;]+"); String[] readbackPvNames = readbackPvNameProperty.get().trim().split("[\\s;]+"); - if(!checkForDuplicatePvNames(pvNames)){ + if (!checkForDuplicatePvNames(pvNames)) { return; } @@ -435,16 +426,17 @@ public void addPv() { /** * Checks that added PV names are not added multiple times + * * @param addedPvNames New PV names added in the UI * @return true if no duplicates are detected, otherwise false */ - private boolean checkForDuplicatePvNames(String[] addedPvNames){ + private boolean checkForDuplicatePvNames(String[] addedPvNames) { List pvNamesAsList = new ArrayList<>(); pvNamesAsList.addAll(Arrays.asList(addedPvNames)); pvTable.itemsProperty().get().forEach(i -> pvNamesAsList.add(i.getPvNameProperty().get())); List duplicatePvNames = pvNamesAsList.stream().filter(n -> Collections.frequency(pvNamesAsList, n) > 1).toList(); - if(duplicatePvNames.size() > 0){ + if (!duplicatePvNames.isEmpty()) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setHeaderText(Messages.duplicatePVNamesNotSupported); alert.showAndWait(); @@ -469,11 +461,8 @@ private void resetAddPv() { */ public void newConfiguration(Node parentNode) { configurationNodeParent = parentNode; - configurationNode.set(Node.builder().nodeType(NodeType.CONFIGURATION).build()); - configurationData = new ConfigurationData(); - pvTable.setItems(configurationEntries); - UI_EXECUTOR.execute(() -> configurationNameField.requestFocus()); - setDirty(false); + tabTitleProperty.setValue(Messages.contextMenuNewConfiguration); + Platform.runLater(() -> configurationNameField.requestFocus()); } /** @@ -482,27 +471,37 @@ public void newConfiguration(Node parentNode) { * @param node An existing {@link Node} of type {@link NodeType#CONFIGURATION}. */ public void loadConfiguration(final Node node) { - try { - configurationData = saveAndRestoreService.getConfiguration(node.getUniqueId()); - } catch (Exception e) { - ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); - return; - } - configurationNode.set(node); - loadConfigurationData(); - } - - private void loadConfigurationData() { - Platform.runLater(() -> { + configurationNodeId = node.getUniqueId(); + loadInProgress.setValue(true); + JobManager.schedule("Load save&restore configuration", monitor -> { + final ConfigurationData configurationData; try { - Collections.sort(configurationData.getPvList()); - configurationEntries.setAll(configurationData.getPvList().stream().map(ConfigPvEntry::new).toList()); - pvTable.setItems(configurationEntries); - //completion.run(); + configurationData = saveAndRestoreService.getConfiguration(node.getUniqueId()); } catch (Exception e) { - logger.log(Level.WARNING, "Unable to load existing configuration"); + Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e)); + return; } + + Platform.runLater(() -> { + try { + Collections.sort(configurationData.getPvList()); + configurationEntries.setAll(configurationData.getPvList().stream().map(ConfigPvEntry::new).toList()); + pvTable.setItems(configurationEntries); + configurationNameProperty.set(node.getName()); + configurationCreatedDateField.textProperty().set(node.getCreated() != null ? + TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(node.getCreated().getTime())) : null); + configurationLastModifiedDateField.textProperty().set(node.getLastModified() != null ? + TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(node.getLastModified().getTime())) : null); + createdByField.textProperty().set(node.getUserName()); + configurationDescriptionProperty.set(node.getDescription()); + tabTitleProperty.setValue(node.getName()); + loadInProgress.setValue(false); + } catch (Exception e) { + logger.log(Level.WARNING, "Unable to load existing configuration"); + } + }); }); + } public boolean handleConfigurationTabClosed() { @@ -516,29 +515,29 @@ public boolean handleConfigurationTabClosed() { return true; } - private void nodeChanged(Node node) { - if (node.getUniqueId().equals(configurationNode.get().getUniqueId())) { - configurationNode.setValue(node); - } - } - @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ - switch (saveAndRestoreWebSocketMessage.messageType()){ - case NODE_UPDATED -> { - Node node = (Node)saveAndRestoreWebSocketMessage.payload(); - if (node.getUniqueId().equals(configurationNode.get().getUniqueId())){ - loadConfiguration(node); - } + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + if(saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { + Node node = (Node) saveAndRestoreWebSocketMessage.payload(); + if (node.getUniqueId().equals(configurationNodeId)) { + loadConfiguration(node); } } } + /** + * Removes this as web socket message listener. + */ public void handleTabClosed() { saveAndRestoreService.removeWebSocketMessageHandler(this); } private void setDirty(boolean dirty) { this.dirty.set(dirty && !loadInProgress.get()); + if (dirty && !tabTitleProperty.get().startsWith("* ")) { + tabTitleProperty.setValue("* " + tabTitleProperty.get()); + } else if (!dirty && tabTitleProperty.get().startsWith("* ")) { + tabTitleProperty.setValue(tabIdProperty.get().substring(2)); + } } } 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 5345e41826..acbe082be9 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 @@ -23,21 +23,15 @@ 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.SaveAndRestoreWebSocketMessage; -import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.ImageRepository; -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 java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; -public class ConfigurationTab extends SaveAndRestoreTab implements WebSocketMessageHandler { - - private String originalConfigName; +public class ConfigurationTab extends SaveAndRestoreTab { public ConfigurationTab() { configure(); @@ -74,12 +68,9 @@ private void configure() { if (!((ConfigurationController) controller).handleConfigurationTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); - ((ConfigurationController)controller).handleTabClosed(); + ((ConfigurationController) controller).handleTabClosed(); } }); - - SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); } /** @@ -88,15 +79,14 @@ private void configure() { * @param configurationNode non-null configuration {@link Node} */ public void editConfiguration(Node configurationNode) { - originalConfigName = configurationNode.getName(); - setId(configurationNode.getUniqueId()); - textProperty().set(configurationNode.getName()); ((ConfigurationController) controller).loadConfiguration(configurationNode); } + /** + * Configures for new configuration + * @param parentNode Parent {@link Node} for the new configuration. + */ public void configureForNewConfiguration(Node parentNode) { - originalConfigName = Messages.contextMenuNewConfiguration; - textProperty().set(Messages.contextMenuNewConfiguration); ((ConfigurationController) controller).newConfiguration(parentNode); } @@ -108,32 +98,4 @@ public void configureForNewConfiguration(Node parentNode) { public void updateTabTitle(String tabTitle) { Platform.runLater(() -> textProperty().set(tabTitle)); } - - /** - * Updates the tab to indicate if the data is dirty and needs to be saved. - * @param dirty If true, an asterisk is prepended, otherwise - * only the name {@link org.phoebus.applications.saveandrestore.model.Configuration} - * is rendered. - */ - public void annotateDirty(boolean dirty) { - String tabTitle = textProperty().get(); - if (dirty && !tabTitle.contains("*")) { - updateTabTitle("* " + originalConfigName); - } else if (!dirty) { - updateTabTitle(originalConfigName); - } - } - - @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ - switch (saveAndRestoreWebSocketMessage.messageType()){ - case NODE_UPDATED -> { - Node node = (Node)saveAndRestoreWebSocketMessage.payload(); - if(node.getUniqueId().equals(getId())){ - updateTabTitle(node.getName()); - originalConfigName = node.getName(); - } - } - } - } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java index 2b5322d21b..6c2e911c47 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java @@ -711,6 +711,9 @@ public Configuration updateConfiguration(Configuration configuration) { Node existingConfigurationNode = getNode(configuration.getConfigurationNode().getUniqueId()); + // Make sure node id is set on ConfigurationData, client may have omitted it. + configuration.getConfigurationNode().setUniqueId(configuration.getConfigurationNode().getUniqueId()); + // Set name, description and user even if unchanged. existingConfigurationNode.setName(configuration.getConfigurationNode().getName()); existingConfigurationNode.setDescription(configuration.getConfigurationNode().getDescription()); From 5519ab87c0dd84c4c769ab58b01fe4c87d958a20 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 28 Apr 2025 14:59:47 +0200 Subject: [PATCH 19/43] Fix 'dirty' behavior --- .../ConfigurationController.java | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) 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 019b2bd871..055f776a6f 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 @@ -77,7 +77,6 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -173,7 +172,6 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private final Logger logger = Logger.getLogger(ConfigurationController.class.getName()); - private final BooleanProperty loadInProgress = new SimpleBooleanProperty(); private final BooleanProperty dirty = new SimpleBooleanProperty(); private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(); @@ -181,6 +179,7 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private String configurationNodeId; + public ConfigurationController(ConfigurationTab configurationTab) { configurationTab.textProperty().bind(tabTitleProperty); configurationTab.idProperty().bind(tabIdProperty); @@ -199,7 +198,8 @@ public void initialize() { new ImageView(ImageCache.getImage(ConfigurationController.class, "/icons/delete.png"))); deleteMenuItem.setOnAction(ae -> { configurationEntries.removeAll(pvTable.getSelectionModel().getSelectedItems()); - pvTable.refresh(); + dirty.setValue(true); + //pvTable.refresh(); }); deleteMenuItem.disableProperty().bind(Bindings.createBooleanBinding(() -> pvTable.getSelectionModel().getSelectedItems().isEmpty() @@ -227,20 +227,20 @@ public void initialize() { pvNameColumn.setCellValueFactory(cell -> cell.getValue().getPvNameProperty()); pvNameColumn.setOnEditCommit(t -> { t.getTableView().getItems().get(t.getTablePosition().getRow()).setPvNameProperty(t.getNewValue()); - setDirty(true); + dirty.setValue(true); }); readbackPvNameColumn.setCellFactory(TextFieldTableCell.forTableColumn()); readbackPvNameColumn.setCellValueFactory(cell -> cell.getValue().getReadBackPvNameProperty()); readbackPvNameColumn.setOnEditCommit(t -> { t.getTableView().getItems().get(t.getTablePosition().getRow()).setReadBackPvNameProperty(t.getNewValue()); - setDirty(true); + dirty.setValue(true); }); readOnlyColumn.setCellFactory(CheckBoxTableCell.forTableColumn(readOnlyColumn)); readOnlyColumn.setCellValueFactory(cell -> { BooleanProperty readOnly = cell.getValue().getReadOnlyProperty(); - readOnly.addListener((obs, o, n) -> setDirty(true)); + readOnly.addListener((obs, o, n) -> dirty.setValue(true)); return readOnly; }); @@ -261,7 +261,7 @@ public void commitEdit(ComparisonMode comparisonMode) { else if (currentMode == null) { getTableView().getItems().get(getIndex()).setToleranceProperty(0.0); } - setDirty(true); + dirty.setValue(true); super.commitEdit(comparisonMode); } }; @@ -314,7 +314,7 @@ public void commitEdit(Double value) { return; } getTableView().getItems().get(getIndex()).setToleranceProperty(value); - setDirty(true); + dirty.setValue(true); super.commitEdit(value); } }); @@ -331,16 +331,24 @@ public void commitEdit(Double value) { while (change.next()) { if (change.wasAdded() || change.wasRemoved()) { FXCollections.sort(configurationEntries); - setDirty(true); + //dirty.setValue(true); } } }); pvTable.setItems(configurationEntries); + dirty.addListener((obs, o, n) -> { + if (n && !tabTitleProperty.get().startsWith("* ")) { + Platform.runLater(() -> tabTitleProperty.setValue("* " + tabTitleProperty.get())); + } else if (!n && tabTitleProperty.get().startsWith("* ")) { + Platform.runLater(() -> tabTitleProperty.setValue(tabIdProperty.get().substring(2))); + } + }); + configurationNameProperty.addListener((observableValue, oldValue, newValue) -> - setDirty(newValue != null)); + dirty.setValue(oldValue != null && newValue != null)); configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> - setDirty(newValue != null)); + dirty.setValue(oldValue != null && newValue != null)); saveButton.disableProperty().bind(Bindings.createBooleanBinding(() -> dirty.not().get() || configurationDescriptionProperty.isEmpty().get() || @@ -431,8 +439,7 @@ public void addPv() { * @return true if no duplicates are detected, otherwise false */ private boolean checkForDuplicatePvNames(String[] addedPvNames) { - List pvNamesAsList = new ArrayList<>(); - pvNamesAsList.addAll(Arrays.asList(addedPvNames)); + List pvNamesAsList = new ArrayList<>(Arrays.asList(addedPvNames)); pvTable.itemsProperty().get().forEach(i -> pvNamesAsList.add(i.getPvNameProperty().get())); List duplicatePvNames = pvNamesAsList.stream().filter(n -> Collections.frequency(pvNamesAsList, n) > 1).toList(); @@ -472,7 +479,6 @@ public void newConfiguration(Node parentNode) { */ public void loadConfiguration(final Node node) { configurationNodeId = node.getUniqueId(); - loadInProgress.setValue(true); JobManager.schedule("Load save&restore configuration", monitor -> { final ConfigurationData configurationData; try { @@ -484,9 +490,10 @@ public void loadConfiguration(final Node node) { Platform.runLater(() -> { try { + tabTitleProperty.setValue(node.getName()); + tabIdProperty.setValue(node.getUniqueId()); Collections.sort(configurationData.getPvList()); configurationEntries.setAll(configurationData.getPvList().stream().map(ConfigPvEntry::new).toList()); - pvTable.setItems(configurationEntries); configurationNameProperty.set(node.getName()); configurationCreatedDateField.textProperty().set(node.getCreated() != null ? TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(node.getCreated().getTime())) : null); @@ -494,8 +501,7 @@ public void loadConfiguration(final Node node) { TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(node.getLastModified().getTime())) : null); createdByField.textProperty().set(node.getUserName()); configurationDescriptionProperty.set(node.getDescription()); - tabTitleProperty.setValue(node.getName()); - loadInProgress.setValue(false); + dirty.setValue(false); } catch (Exception e) { logger.log(Level.WARNING, "Unable to load existing configuration"); } @@ -517,7 +523,7 @@ public boolean handleConfigurationTabClosed() { @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - if(saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { + if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { Node node = (Node) saveAndRestoreWebSocketMessage.payload(); if (node.getUniqueId().equals(configurationNodeId)) { loadConfiguration(node); @@ -531,13 +537,4 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore public void handleTabClosed() { saveAndRestoreService.removeWebSocketMessageHandler(this); } - - private void setDirty(boolean dirty) { - this.dirty.set(dirty && !loadInProgress.get()); - if (dirty && !tabTitleProperty.get().startsWith("* ")) { - tabTitleProperty.setValue("* " + tabTitleProperty.get()); - } else if (!dirty && tabTitleProperty.get().startsWith("* ")) { - tabTitleProperty.setValue(tabIdProperty.get().substring(2)); - } - } } From cb7a5a93d8322cf31d127b02739e1551883e530a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 29 Apr 2025 10:47:13 +0200 Subject: [PATCH 20/43] Snapshot view dirty visualization: same as configuration view --- .../applications/saveandrestore/Messages.java | 1 + .../ui/SaveAndRestoreController.java | 31 +++++------ .../ConfigurationController.java | 24 +++++---- .../ui/configuration/ConfigurationTab.java | 11 +--- .../ui/snapshot/SnapshotController.java | 51 +++++++++++++------ .../ui/snapshot/SnapshotTab.java | 9 ---- .../saveandrestore/messages.properties | 1 + 7 files changed, 65 insertions(+), 63 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index 4588cda6ae..c78868d0ab 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -178,6 +178,7 @@ public class Messages { public static String toolTipConfigurationExists; public static String toolTipConfigurationExistsOption; public static String toolTipMultiplierSpinner; + public static String unnamedConfiguration; public static String unnamedSnapshot; public static String updateCompositeSnapshotFailed; 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 f8f659f962..3bf35d5bd2 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 @@ -505,36 +505,31 @@ private void deleteNodes() { } private void deleteTreeItems(ObservableList> items) { - TreeItem parent = items.get(0).getParent(); disabledUi.set(true); List nodeIds = items.stream().map(item -> item.getValue().getUniqueId()).collect(Collectors.toList()); + List tabsToRemove = new ArrayList<>(); + List visibleTabs = tabPane.getTabs(); + for (Tab tab : visibleTabs) { + for (TreeItem treeItem : items) { + if (treeItem.getValue().getUniqueId().equals(tab.getId())) { + tabsToRemove.add(tab); + } + } + } JobManager.schedule("Delete nodes", monitor -> { try { saveAndRestoreService.deleteNodes(nodeIds); + Platform.runLater(() -> { + disabledUi.set(false); + tabPane.getTabs().removeAll(tabsToRemove); + }); } catch (Exception e) { ExceptionDetailsErrorDialog.openError(Messages.errorGeneric, MessageFormat.format(Messages.errorDeleteNodeFailed, items.get(0).getValue().getName()), e); disabledUi.set(false); - return; } - - Platform.runLater(() -> { - List tabsToRemove = new ArrayList<>(); - List visibleTabs = tabPane.getTabs(); - for (Tab tab : visibleTabs) { - for (TreeItem treeItem : items) { - if (tab.getId().equals(treeItem.getValue().getUniqueId())) { - tabsToRemove.add(tab); - tab.getOnCloseRequest().handle(null); - } - } - } - disabledUi.set(false); - tabPane.getTabs().removeAll(tabsToRemove); - parent.getChildren().removeAll(items); - }); }); } 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 055f776a6f..3de1ff6f3f 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 @@ -175,11 +175,13 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private final BooleanProperty dirty = new SimpleBooleanProperty(); private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(); + /** + * Manages the id of the containing {@link javafx.scene.control.Tab}. This property will + * also indicate if the UI has been configured to edit a new or existing configuration: for a new configuration + * the id is null. + */ private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); - private String configurationNodeId; - - public ConfigurationController(ConfigurationTab configurationTab) { configurationTab.textProperty().bind(tabTitleProperty); configurationTab.idProperty().bind(tabIdProperty); @@ -199,7 +201,6 @@ public void initialize() { deleteMenuItem.setOnAction(ae -> { configurationEntries.removeAll(pvTable.getSelectionModel().getSelectedItems()); dirty.setValue(true); - //pvTable.refresh(); }); deleteMenuItem.disableProperty().bind(Bindings.createBooleanBinding(() -> pvTable.getSelectionModel().getSelectedItems().isEmpty() @@ -341,7 +342,7 @@ public void commitEdit(Double value) { if (n && !tabTitleProperty.get().startsWith("* ")) { Platform.runLater(() -> tabTitleProperty.setValue("* " + tabTitleProperty.get())); } else if (!n && tabTitleProperty.get().startsWith("* ")) { - Platform.runLater(() -> tabTitleProperty.setValue(tabIdProperty.get().substring(2))); + Platform.runLater(() -> tabTitleProperty.setValue(tabTitleProperty.get().substring(2))); } }); @@ -377,15 +378,15 @@ public void saveConfiguration() { Node.builder().nodeType(NodeType.CONFIGURATION) .name(configurationNameProperty.get()) .description(configurationDescriptionProperty.get()) - .uniqueId(configurationNodeId) + .uniqueId(tabIdProperty.get()) .build(); ConfigurationData configurationData = new ConfigurationData(); configurationData.setPvList(configurationEntries.stream().map(ConfigPvEntry::toConfigPv).toList()); - configurationData.setUniqueId(configurationNodeId); + configurationData.setUniqueId(tabIdProperty.get()); Configuration configuration = new Configuration(); configuration.setConfigurationNode(configurationNode); configuration.setConfigurationData(configurationData); - if (configurationNodeId == null) { // New configuration + if (tabIdProperty.get() == null) { // New configuration configuration = saveAndRestoreService.createConfiguration(configurationNodeParent, configuration); tabIdProperty.setValue(configuration.getConfigurationNode().getUniqueId()); @@ -428,6 +429,7 @@ public void addPv() { configPVs.add(new ConfigPvEntry(configPV)); } configurationEntries.addAll(configPVs); + dirty.setValue(true); resetAddPv(); }); } @@ -468,7 +470,7 @@ private void resetAddPv() { */ public void newConfiguration(Node parentNode) { configurationNodeParent = parentNode; - tabTitleProperty.setValue(Messages.contextMenuNewConfiguration); + tabTitleProperty.setValue(Messages.unnamedConfiguration); Platform.runLater(() -> configurationNameField.requestFocus()); } @@ -478,7 +480,7 @@ public void newConfiguration(Node parentNode) { * @param node An existing {@link Node} of type {@link NodeType#CONFIGURATION}. */ public void loadConfiguration(final Node node) { - configurationNodeId = node.getUniqueId(); + //tabIdProperty.setValue(node.getUniqueId()); JobManager.schedule("Load save&restore configuration", monitor -> { final ConfigurationData configurationData; try { @@ -525,7 +527,7 @@ public boolean handleConfigurationTabClosed() { public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { Node node = (Node) saveAndRestoreWebSocketMessage.payload(); - if (node.getUniqueId().equals(configurationNodeId)) { + 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 acbe082be9..12ef4f3691 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,7 +18,6 @@ */ 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; @@ -84,18 +83,10 @@ public void editConfiguration(Node configurationNode) { /** * Configures for new configuration + * * @param parentNode Parent {@link Node} for the new configuration. */ public void configureForNewConfiguration(Node parentNode) { ((ConfigurationController) controller).newConfiguration(parentNode); } - - /** - * Updates tab title, e.g. if user has renamed the configuration. - * - * @param tabTitle The wanted tab title. - */ - public void updateTabTitle(String tabTitle) { - Platform.runLater(() -> textProperty().set(tabTitle)); - } } 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 71da8555df..dd98496a5e 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 @@ -6,6 +6,7 @@ import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Alert; @@ -31,14 +32,13 @@ import org.phoebus.applications.saveandrestore.model.SnapshotData; import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.model.event.SaveAndRestoreEventReceiver; -import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; 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.WebSocketMessageHandler; -import org.phoebus.saveandrestore.util.VNoData; import org.phoebus.framework.jobs.JobManager; +import org.phoebus.saveandrestore.util.VNoData; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -74,6 +74,9 @@ public class SnapshotController extends SaveAndRestoreBaseController implements protected ServiceLoader eventReceivers; + private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(); + private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); + @FXML protected VBox progressIndicator; @@ -81,6 +84,8 @@ public class SnapshotController extends SaveAndRestoreBaseController implements public SnapshotController(SnapshotTab snapshotTab) { this.snapshotTab = snapshotTab; + snapshotTab.textProperty().bind(tabTitleProperty); + snapshotTab.idProperty().bind(tabIdProperty); } /** @@ -114,6 +119,14 @@ public void initialize() { snapshotTableViewController.showSnapshotInTable(n); } }); + + snapshotControlsViewController.snapshotDataDirty.addListener((obs, o, n) -> { + if (n && !tabTitleProperty.get().startsWith("* ")) { + Platform.runLater(() -> tabTitleProperty.setValue("* " + tabTitleProperty.get())); + } else if (!n && tabTitleProperty.get().startsWith("* ")) { + Platform.runLater(() -> tabTitleProperty.setValue(tabTitleProperty.get().substring(2))); + } + }); } /** @@ -124,7 +137,8 @@ public void initialize() { */ public void initializeViewForNewSnapshot(Node configurationNode) { this.configurationNode = configurationNode; - snapshotTab.updateTabTitle(Messages.unnamedSnapshot); + tabTitleProperty.setValue(Messages.unnamedSnapshot); + tabIdProperty.setValue(null); JobManager.schedule("Get configuration", monitor -> { ConfigurationData configuration; try { @@ -149,7 +163,7 @@ public void initializeViewForNewSnapshot(Node configurationNode) { @SuppressWarnings("unused") public void takeSnapshot() { disabledUi.set(true); - snapshotTab.setText(Messages.unnamedSnapshot); + tabTitleProperty.setValue(Messages.unnamedSnapshot); snapshotTableViewController.takeSnapshot(snapshotControlsViewController.getDefaultSnapshotMode(), snapshot -> { disabledUi.set(false); if (snapshot.isPresent()) { @@ -168,7 +182,17 @@ public void saveSnapshot(ActionEvent actionEvent) { SnapshotData snapshotData = new SnapshotData(); snapshotData.setSnapshotItems(snapshotItems); Snapshot snapshot = snapshotProperty.get(); + Node snapshotNode = + Node.builder() + .nodeType(NodeType.SNAPSHOT) + .name(snapshotControlsViewController.getSnapshotNameProperty().get()) + .description(snapshotControlsViewController.getSnapshotCommentProperty().get()) + .uniqueId(tabIdProperty.get()) + .build(); + snapshot.setSnapshotNode(snapshotNode); + // Creating new or updating existing (e.g. name change)? + /* if (snapshot == null) { snapshot = new Snapshot(); snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT) @@ -178,7 +202,7 @@ public void saveSnapshot(ActionEvent actionEvent) { snapshot.getSnapshotNode().setName(snapshotControlsViewController.getSnapshotNameProperty().get()); snapshot.getSnapshotNode().setDescription(snapshotControlsViewController.getSnapshotCommentProperty().get()); } - snapshot.setSnapshotData(snapshotData); + snapshot.setSnapshotData(snapshotData);*/ try { snapshot = SaveAndRestoreService.getInstance().saveSnapshot(configurationNode, snapshot); @@ -188,10 +212,7 @@ public void saveSnapshot(ActionEvent actionEvent) { eventReceivers.forEach(r -> r.snapshotSaved(_snapshotNode, this::showLoggingError)); } snapshotControlsViewController.snapshotDataDirty.set(false); - Platform.runLater(() -> { - // Load snapshot via the tab as that will also update the tab title and id. - snapshotTab.loadSnapshot(_snapshotNode); - }); + loadSnapshot(snapshot.getSnapshotNode()); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to save snapshot", e); Platform.runLater(() -> { @@ -341,7 +362,8 @@ private void loadSnapshotInternal(Node snapshotNode) { Snapshot snapshot = getSnapshotFromService(snapshotNode); snapshotProperty.set(snapshot); Platform.runLater(() -> { - //snapshotTableViewController.showSnapshotInTable(snapshot); + tabTitleProperty.setValue(snapshotNode.getName()); + tabIdProperty.setValue(snapshotNode.getUniqueId()); snapshotControlsViewController.getSnapshotRestorableProperty().set(true); }); } finally { @@ -370,8 +392,7 @@ public void restore(ActionEvent actionEvent) { } if (restoreResultList != null && !restoreResultList.isEmpty()) { showAndLogFailedRestoreResult(snapshotProperty.get(), restoreResultList); - } - else{ + } else { LOGGER.log(Level.INFO, "Successfully restored snapshot \"" + snapshotProperty.get().getSnapshotNode().getName() + "\""); } }); @@ -444,7 +465,7 @@ private Snapshot getSnapshotFromService(Node snapshotNode) throws Exception { snapshotData.setSnapshotItems(snapshotItems); } } catch (Exception e) { - ExceptionDetailsErrorDialog.openError(snapshotTab.getContent(), Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); + ExceptionDetailsErrorDialog.openError(borderPane, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); LOGGER.log(Level.INFO, "Error loading snapshot", e); throw e; } @@ -460,8 +481,8 @@ public void secureStoreChanged(List validTokens) { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ - switch (saveAndRestoreWebSocketMessage.messageType()){ + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + switch (saveAndRestoreWebSocketMessage.messageType()) { //case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); } } 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 32b6ea7934..3e5fdc42bc 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 @@ -103,8 +103,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save imageView.imageProperty().bind(tabGraphicImageProperty); setGraphic(imageView); - - textProperty().set(node.getNodeType().equals(NodeType.SNAPSHOT) ? node.getName() : Messages.unnamedSnapshot); setTabImage(node); setOnCloseRequest(event -> { @@ -130,10 +128,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save SaveAndRestoreService.getInstance().addDataChangeListener(this); } - public void updateTabTitle(String name) { - Platform.runLater(() -> textProperty().set(name)); - } - /** * Set tab image based on node type, and optionally golden tag * @@ -159,7 +153,6 @@ private void setTabImage(Node node) { * a snapshot will be created. */ public void newSnapshot(org.phoebus.applications.saveandrestore.model.Node configurationNode) { - setId(null); ((SnapshotController) controller).initializeViewForNewSnapshot(configurationNode); } @@ -169,8 +162,6 @@ public void newSnapshot(org.phoebus.applications.saveandrestore.model.Node confi * @param snapshotNode The {@link Node} of type {@link NodeType#SNAPSHOT} containing snapshot data. */ public void loadSnapshot(Node snapshotNode) { - updateTabTitle(snapshotNode.getName()); - setId(snapshotNode.getUniqueId()); ((SnapshotController) controller).loadSnapshot(snapshotNode); } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 030da4a5c3..38e45850f9 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -262,6 +262,7 @@ toolTipConfigurationExists=Configuration name already exists in the folder! toolTipConfigurationExistsOption=Choose it with Browse if you want to add PVs in existing configuration. toolTipMultiplierSpinner=This field only takes number. UniqueId=Unique ID +unnamedConfiguration= unnamedSnapshot= updateConfigurationFailed=Failed to update configuration updateCompositeSnapshotFailed=Failed to update composite snapshot From 4a7c1bbedb1c73bb0c73b746aaafb171b00e6bb9 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 29 Apr 2025 12:23:12 +0200 Subject: [PATCH 21/43] Move snapshot view icon handling to controller --- .../ui/snapshot/SnapshotController.java | 42 +++++++++++++------ .../ui/snapshot/SnapshotTab.java | 36 ---------------- 2 files changed, 29 insertions(+), 49 deletions(-) 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 dd98496a5e..032cbb7837 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 @@ -11,6 +11,8 @@ import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import org.epics.vtype.Alarm; @@ -31,8 +33,10 @@ import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.model.SnapshotData; 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.SaveAndRestoreWebSocketMessage; +import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SnapshotMode; @@ -77,6 +81,8 @@ public class SnapshotController extends SaveAndRestoreBaseController implements private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(); private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); + private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); + @FXML protected VBox progressIndicator; @@ -86,6 +92,9 @@ public SnapshotController(SnapshotTab snapshotTab) { this.snapshotTab = snapshotTab; snapshotTab.textProperty().bind(tabTitleProperty); snapshotTab.idProperty().bind(tabIdProperty); + ImageView imageView = new ImageView(); + imageView.imageProperty().bind(tabGraphicImageProperty); + snapshotTab.setGraphic(imageView); } /** @@ -155,6 +164,7 @@ public void initializeViewForNewSnapshot(Node configurationNode) { snapshotData.setSnapshotItems(configurationToSnapshotItems(configPvs)); snapshot.setSnapshotData(snapshotData); snapshotProperty.set(snapshot); + setTabImage(snapshot.getSnapshotNode()); Platform.runLater(() -> snapshotTableViewController.showSnapshotInTable(snapshot)); }); } @@ -191,19 +201,6 @@ public void saveSnapshot(ActionEvent actionEvent) { .build(); snapshot.setSnapshotNode(snapshotNode); - // Creating new or updating existing (e.g. name change)? - /* - if (snapshot == null) { - snapshot = new Snapshot(); - snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT) - .name(snapshotControlsViewController.getSnapshotNameProperty().get()) - .description(snapshotControlsViewController.getSnapshotCommentProperty().get()).build()); - } else { - snapshot.getSnapshotNode().setName(snapshotControlsViewController.getSnapshotNameProperty().get()); - snapshot.getSnapshotNode().setDescription(snapshotControlsViewController.getSnapshotCommentProperty().get()); - } - snapshot.setSnapshotData(snapshotData);*/ - try { snapshot = SaveAndRestoreService.getInstance().saveSnapshot(configurationNode, snapshot); snapshotProperty.set(snapshot); @@ -365,6 +362,7 @@ private void loadSnapshotInternal(Node snapshotNode) { tabTitleProperty.setValue(snapshotNode.getName()); tabIdProperty.setValue(snapshotNode.getUniqueId()); snapshotControlsViewController.getSnapshotRestorableProperty().set(true); + setTabImage(snapshotNode); }); } finally { Platform.runLater(() -> disabledUi.set(false)); @@ -486,4 +484,22 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore //case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); } } + + /** + * Set tab image based on node type, and optionally golden tag + * + * @param node A snapshot {@link Node} + */ + private void setTabImage(Node node) { + if (node.getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)) { + tabGraphicImageProperty.set(ImageRepository.COMPOSITE_SNAPSHOT); + } else { + boolean golden = node.getTags() != null && node.getTags().stream().anyMatch(t -> t.getName().equals(Tag.GOLDEN)); + if (golden) { + tabGraphicImageProperty.set(ImageRepository.GOLDEN_SNAPSHOT); + } else { + tabGraphicImageProperty.set(ImageRepository.SNAPSHOT); + } + } + } } \ No newline at end of file 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 3e5fdc42bc..07a7ef370e 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 @@ -56,8 +56,6 @@ public class SnapshotTab extends SaveAndRestoreTab implements DataChangeListener public SaveAndRestoreService saveAndRestoreService; - private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); - protected Image compareSnapshotIcon = ImageCache.getImage(SaveAndRestoreController.class, "/icons/save-and-restore/compare.png"); public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, SaveAndRestoreService saveAndRestoreService) { @@ -99,12 +97,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save return; } - ImageView imageView = new ImageView(); - imageView.imageProperty().bind(tabGraphicImageProperty); - - setGraphic(imageView); - setTabImage(node); - setOnCloseRequest(event -> { if (controller != null && !((SnapshotController) controller).handleSnapshotTabClosed()) { event.consume(); @@ -128,24 +120,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save SaveAndRestoreService.getInstance().addDataChangeListener(this); } - /** - * Set tab image based on node type, and optionally golden tag - * - * @param node A snapshot {@link Node} - */ - private void setTabImage(Node node) { - if (node.getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)) { - tabGraphicImageProperty.set(ImageRepository.COMPOSITE_SNAPSHOT); - } else { - boolean golden = node.getTags() != null && node.getTags().stream().anyMatch(t -> t.getName().equals(Tag.GOLDEN)); - if (golden) { - tabGraphicImageProperty.set(ImageRepository.GOLDEN_SNAPSHOT); - } else { - tabGraphicImageProperty.set(ImageRepository.SNAPSHOT); - } - } - } - /** * Loads and configures a view for the use case of taking a new snapshot. * @@ -173,16 +147,6 @@ private void addSnapshotFromArchive() { ((SnapshotController) controller).addSnapshotFromArchiver(); } - @Override - public void nodeChanged(Node node) { - if (node.getUniqueId().equals(getId())) { - Platform.runLater(() -> { - ((SnapshotController) controller).setSnapshotNameProperty(node.getName()); - setTabImage(node); - }); - } - } - public Node getSnapshotNode() { return ((SnapshotController) controller).getSnapshot().getSnapshotNode(); } From b6c49472b4d86c790be18fb71f826cf3a5d97032 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 29 Apr 2025 13:14:00 +0200 Subject: [PATCH 22/43] Web socket handling in snapshot view controller --- .../BaseSnapshotTableViewController.java | 17 ++++++++------ .../ui/snapshot/SnapshotController.java | 22 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java index edaa3f00d1..dd93dbd134 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/BaseSnapshotTableViewController.java @@ -44,6 +44,7 @@ import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.ui.VTypePair; import org.phoebus.core.types.TimeStampedProcessVariable; +import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; import org.phoebus.ui.javafx.FocusUtil; @@ -315,13 +316,15 @@ public void updateTable(List entries) { } protected void connectPVs() { - tableEntryItems.values().forEach(e -> { - SaveAndRestorePV pv = pvs.get(getPVKey(e.getConfigPv().getPvName(), e.getConfigPv().isReadOnly())); - if (pv == null) { - pvs.put(getPVKey(e.getConfigPv().getPvName(), e.getConfigPv().isReadOnly()), new SaveAndRestorePV(e)); - } else { - pv.setSnapshotTableEntry(e); - } + JobManager.schedule("Connect PVs", monitor -> { + tableEntryItems.values().forEach(e -> { + SaveAndRestorePV pv = pvs.get(getPVKey(e.getConfigPv().getPvName(), e.getConfigPv().isReadOnly())); + if (pv == null) { + pvs.put(getPVKey(e.getConfigPv().getPvName(), e.getConfigPv().isReadOnly()), new SaveAndRestorePV(e)); + } else { + pv.setSnapshotTableEntry(e); + } + }); }); } } 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 032cbb7837..ec048a9df9 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 @@ -35,6 +35,7 @@ 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.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; @@ -83,6 +84,8 @@ public class SnapshotController extends SaveAndRestoreBaseController implements private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); + private final SaveAndRestoreService saveAndRestoreService; + @FXML protected VBox progressIndicator; @@ -95,6 +98,8 @@ public SnapshotController(SnapshotTab snapshotTab) { ImageView imageView = new ImageView(); imageView.imageProperty().bind(tabGraphicImageProperty); snapshotTab.setGraphic(imageView); + + saveAndRestoreService = SaveAndRestoreService.getInstance(); } /** @@ -136,6 +141,8 @@ public void initialize() { Platform.runLater(() -> tabTitleProperty.setValue(tabTitleProperty.get().substring(2))); } }); + + saveAndRestoreService.addWebSocketMessageHandler(this); } /** @@ -281,6 +288,7 @@ public boolean handleSnapshotTabClosed() { return false; } } + saveAndRestoreService.removeWebSocketMessageHandler(this); dispose(); return true; } @@ -316,10 +324,6 @@ public Node getConfigurationNode() { return configurationNode; } - public void setSnapshotNameProperty(String name) { - snapshotControlsViewController.getSnapshotNameProperty().set(name); - } - /** * Updates snapshot set-point values with user defined multiplier. Note that the stored snapshot * is not affected, only the values shown in the snapshot view. The updated value is used when @@ -383,7 +387,8 @@ public void loadSnapshot(Node snapshotNode) { loadSnapshotInternal(snapshotNode); } - public void restore(ActionEvent actionEvent) { + @FXML + public void restore() { snapshotTableViewController.restore(snapshotControlsViewController.getRestoreMode(), snapshotProperty.get(), restoreResultList -> { if (snapshotControlsViewController.logAction()) { eventReceivers.forEach(r -> r.snapshotRestored(snapshotProperty.get().getSnapshotNode(), restoreResultList, this::showLoggingError)); @@ -480,8 +485,11 @@ public void secureStoreChanged(List validTokens) { @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - switch (saveAndRestoreWebSocketMessage.messageType()) { - //case NODE_UPDATED -> nodeChanged((Node)saveAndRestoreWebSocketMessage.payload()); + if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { + Node node = (Node) saveAndRestoreWebSocketMessage.payload(); + if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { + loadSnapshot(node); + } } } From 5c63f786e0945af362227ebb28e162a58b13ed61 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 29 Apr 2025 13:28:35 +0200 Subject: [PATCH 23/43] Fix build failure --- .../saveandrestore/ui/snapshot/SnapshotController.java | 1 - .../ui/snapshot/SnapshotControlsViewController.java | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 ec048a9df9..7fa79a28aa 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 @@ -387,7 +387,6 @@ public void loadSnapshot(Node snapshotNode) { loadSnapshotInternal(snapshotNode); } - @FXML public void restore() { snapshotTableViewController.restore(snapshotControlsViewController.getRestoreMode(), snapshotProperty.get(), restoreResultList -> { if (snapshotControlsViewController.logAction()) { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java index 0c9b1d2776..046d0507f7 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java @@ -368,8 +368,8 @@ public void saveSnapshot(ActionEvent event) { } @FXML - public void restore(ActionEvent event) { - snapshotController.restore(event); + public void restore() { + snapshotController.restore(); } public SimpleBooleanProperty getSnapshotRestorableProperty() { From 0f70202c5abb4317ee6d11778eeb7949fa5f5bff Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 29 Apr 2025 14:43:34 +0200 Subject: [PATCH 24/43] Fix bug in snapshot update --- .../ui/configuration/ConfigurationController.java | 3 +-- .../ui/snapshot/SnapshotController.java | 2 +- .../dao/impl/elasticsearch/ElasticsearchDAO.java | 13 +++++++++---- .../web/controllers/SnapshotController.java | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) 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 3de1ff6f3f..6aefc899d6 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 @@ -479,8 +479,7 @@ public void newConfiguration(Node parentNode) { * * @param node An existing {@link Node} of type {@link NodeType#CONFIGURATION}. */ - public void loadConfiguration(final Node node) { - //tabIdProperty.setValue(node.getUniqueId()); + public synchronized void loadConfiguration(final Node node) { JobManager.schedule("Load save&restore configuration", monitor -> { final ConfigurationData configurationData; try { 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 7fa79a28aa..d26b226491 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 @@ -379,7 +379,7 @@ private void loadSnapshotInternal(Node snapshotNode) { * * @param snapshotNode An existing {@link Node} of type {@link NodeType#SNAPSHOT} */ - public void loadSnapshot(Node snapshotNode) { + public synchronized void loadSnapshot(Node snapshotNode) { snapshotControlsViewController.setSnapshotNode(snapshotNode); snapshotControlsViewController.setSnapshotRestorableProperty(true); snapshotTableViewController.setSelectionColumnVisible(true); diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java index 6c2e911c47..d1cd5248d6 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java @@ -712,14 +712,13 @@ public Configuration updateConfiguration(Configuration configuration) { Node existingConfigurationNode = getNode(configuration.getConfigurationNode().getUniqueId()); // Make sure node id is set on ConfigurationData, client may have omitted it. - configuration.getConfigurationNode().setUniqueId(configuration.getConfigurationNode().getUniqueId()); + configuration.getConfigurationNode().setUniqueId(existingConfigurationNode.getUniqueId()); // Set name, description and user even if unchanged. existingConfigurationNode.setName(configuration.getConfigurationNode().getName()); existingConfigurationNode.setDescription(configuration.getConfigurationNode().getDescription()); existingConfigurationNode.setUserName(configuration.getConfigurationNode().getUserName()); // Update last modified date - existingConfigurationNode.setLastModified(new Date()); existingConfigurationNode = updateNode(existingConfigurationNode, false); ConfigurationData updatedConfigurationData = configurationDataRepository.save(configuration.getConfigurationData()); @@ -770,12 +769,18 @@ public Snapshot updateSnapshot(Snapshot snapshot) { SnapshotData sanitizedSnapshotData = removeDuplicateSnapshotItems(snapshot.getSnapshotData()); snapshot.setSnapshotData(sanitizedSnapshotData); - snapshot.getSnapshotNode().setNodeType(NodeType.SNAPSHOT); // Force node type + Node existingSnapshotNode = getNode(snapshot.getSnapshotNode().getUniqueId()); + // Make sure node id is set on ConfigurationData, client may have omitted it. + snapshot.getSnapshotNode().setUniqueId(existingSnapshotNode.getUniqueId()); + existingSnapshotNode.setName(snapshot.getSnapshotNode().getName()); + existingSnapshotNode.setDescription(snapshot.getSnapshotNode().getDescription()); + existingSnapshotNode.setUserName(snapshot.getSnapshotNode().getUserName()); + SnapshotData newSnapshotData; Snapshot newSnapshot = new Snapshot(); try { newSnapshotData = snapshotDataRepository.save(snapshot.getSnapshotData()); - Node updatedNode = updateNode(snapshot.getSnapshotNode(), false); + Node updatedNode = updateNode(existingSnapshotNode, false); newSnapshot.setSnapshotData(newSnapshotData); newSnapshot.setSnapshotNode(updatedNode); } catch (Exception e) { 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 7a49eee03d..dd5eb30fa5 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 @@ -99,7 +99,7 @@ public Snapshot updateSnapshot(@RequestBody Snapshot snapshot, } snapshot.getSnapshotNode().setUserName(principal.getName()); Snapshot updatedSnapshot = nodeDAO.updateSnapshot(snapshot); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, snapshot)); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedSnapshot.getSnapshotNode())); return updatedSnapshot; } } From 1a11d4e5c27ef06b592032d81da790bff9d993ee Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 29 Apr 2025 15:44:45 +0200 Subject: [PATCH 25/43] Improved UI update when nodes are added/removed --- .../ui/SaveAndRestoreController.java | 39 ++++++++++++------- .../web/controllers/NodeController.java | 9 ++--- 2 files changed, 30 insertions(+), 18 deletions(-) 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 3bf35d5bd2..9efe7157eb 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 @@ -292,10 +292,6 @@ public void initialize(URL url, ResourceBundle resourceBundle) { treeView.setShowRoot(true); - //saveAndRestoreService.addNodeChangeListener(this); - //saveAndRestoreService.addNodeAddedListener(this); - //saveAndRestoreService.addFilterChangeListener(this); - //saveAndRestoreService.addDataChangeListener(this); saveAndRestoreService.addWebSocketMessageHandler(this); treeView.setCellFactory(p -> new BrowserTreeCell(this)); @@ -823,18 +819,34 @@ private void nodeChanged(Node node) { /** * Handles callback in order to update the tree view when a {@link Node} has been added, e.g. when - * a snapshot is saved. + * a snapshot is saved. The purpose is to update the {@link TreeView} accordingly to reflect the change. * - * @param parentNodeId Unique id of the parent {@link Node} + * @param nodeId Unique id of the added {@link Node} */ + private void nodeAdded(String nodeId){ + Node newNode = saveAndRestoreService.getNode(nodeId); + try { + Node parentNode = saveAndRestoreService.getParentNode(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); + } catch (Exception e) { + throw new RuntimeException(e); + } + } - private void nodeAddedOrRemoved(String parentNodeId){ - // Find the parent to which the new node is to be added - TreeItem parentTreeItem = recursiveSearch(parentNodeId, treeView.getRoot()); - if (parentTreeItem == null) { - return; + /** + * Handles callback in order to update the tree view when a {@link Node} has been deleted. + * The purpose is to update the {@link TreeView} accordingly to reflect the change. + * @param nodeId Unique id of the deleted {@link Node} + */ + private void nodeRemoved(String nodeId){ + TreeItem treeItemToRemove = recursiveSearch(nodeId, treeView.getRoot()); + if(treeItemToRemove != null){ + treeItemToRemove.getParent().getChildren().remove(treeItemToRemove); } - expandTreeNode(parentTreeItem); } /** @@ -1514,7 +1526,8 @@ private void addOptionalLoggingMenuItem() { @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ switch (saveAndRestoreWebSocketMessage.messageType()){ - case NODE_ADDED, NODE_REMOVED -> nodeAddedOrRemoved((String)saveAndRestoreWebSocketMessage.payload()); + 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()); 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 4175235554..41005b1185 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 @@ -38,7 +38,6 @@ import java.security.Principal; import java.util.List; -import java.util.Set; /** * Controller offering endpoints for CRUD operations on {@link Node}s, which represent @@ -86,7 +85,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, - parentsUniqueId)); + savedNode.getUniqueId())); return savedNode; } @@ -158,9 +157,9 @@ public List getChildNodes(@PathVariable final String uniqueNodeId) { @DeleteMapping(value = "/node", produces = JSON) @PreAuthorize("@authorizationHelper.mayDelete(#nodeIds, #root)") public void deleteNodes(@RequestBody List nodeIds) { - Set parentNodeIds = nodeDAO.deleteNodes(nodeIds); - parentNodeIds.forEach(id -> - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, id))); + nodeDAO.deleteNodes(nodeIds); + nodeIds.forEach(id -> + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, id))); } /** From 58db4e5a2590ccb62e8e29ec86209f713196a59d Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 30 Apr 2025 08:02:33 +0200 Subject: [PATCH 26/43] Simplified deletion of node server side --- .../persistence/dao/NodeDAO.java | 4 ++-- .../impl/elasticsearch/ElasticsearchDAO.java | 20 +++++++------------ .../web/controllers/NodeControllerTest.java | 11 +++------- .../controllers/SnapshotControllerTest.java | 4 ++-- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java index 8a40f0a68a..01f42d29d7 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java @@ -76,10 +76,10 @@ public interface NodeDAO { * Checks that each of the node ids passed to this method exist, and that none of them * is the root node. If check passes all nodes are deleted. * @param nodeIds List of (existing) node ids. - * @return The collection of unique node id representing the parent {@link Node}s of the deleted + * @return The collection of unique node id representing the deleted * {@link Node}s. Client may use this to trigger a refresh of the UI. */ - Set deleteNodes(List nodeIds); + void deleteNodes(List nodeIds); /** * Creates a new node in the tree. diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java index d1cd5248d6..43a5cbc5ec 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java @@ -142,9 +142,8 @@ public void deleteNode(String nodeId) { * {@inheritDoc} */ @Override - public Set deleteNodes(List nodeIds){ - Set parentIds = new HashSet<>(); - List nodes = new ArrayList<>(); + public void deleteNodes(List nodeIds){ + List nodesToDelete = new ArrayList<>(); for (String nodeId : nodeIds) { Node nodeToDelete = getNode(nodeId); if (nodeToDelete == null) { @@ -152,13 +151,11 @@ public Set deleteNodes(List nodeIds){ } else if (nodeToDelete.getUniqueId().equals(ROOT_FOLDER_UNIQUE_ID)) { throw new IllegalArgumentException("Root node cannot be deleted"); } - nodes.add(nodeToDelete); + nodesToDelete.add(nodeToDelete); + } + for(Node node : nodesToDelete){ + deleteNode(node); } - nodes.forEach(node -> { - String parentNodeId = deleteNode(node); - parentIds.add(parentNodeId); - }); - return parentIds; } @Override @@ -645,9 +642,8 @@ private void resolvePath(String nodeId, List pathElements) { /** * * @param nodeToDelete - * @return The id of the deleted {@link Node}s parent. */ - private String deleteNode(Node nodeToDelete) { + private void deleteNode(Node nodeToDelete) { for (Node node : getChildNodes(nodeToDelete.getUniqueId())) { deleteNode(node); } @@ -673,8 +669,6 @@ private String deleteNode(Node nodeToDelete) { // Delete the node elasticsearchTreeRepository.deleteById(nodeToDelete.getUniqueId()); - - return parentNode.getNode().getUniqueId(); } @Override 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 802c496d7e..677225fd74 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 @@ -429,7 +429,6 @@ public void testDeleteFolder() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); when(nodeDAO.getParentNode("a")).thenReturn(Node.builder().uniqueId("b").build()); - when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("b")); request = delete("/node") @@ -437,9 +436,7 @@ public void testDeleteFolder() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b"); - - verify(webSocketHandler, times(1)).sendMessage(saveAndRestoreWebSocketMessage); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test @@ -461,7 +458,6 @@ public void testDeleteForbiddenAccess2() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); when(nodeDAO.getParentNode("a")).thenReturn(Node.builder().uniqueId("b").build()); - when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("b")); MockHttpServletRequestBuilder request = delete("/node") @@ -483,7 +479,7 @@ public void testDeleteFolder2() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); when(nodeDAO.getParentNode("a")).thenReturn(Node.builder().uniqueId("b").build()); - when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("b")); + //when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("b")); MockHttpServletRequestBuilder request = delete("/node") @@ -500,7 +496,6 @@ public void testDeleteFolder3() throws Exception { when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.FOLDER).userName(demoUser).build()); when(nodeDAO.getParentNode("a")).thenReturn(Node.builder().uniqueId("b").build()); - when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("b")); when(nodeDAO.getChildNodes("a")).thenReturn(Collections.emptyList()); MockHttpServletRequestBuilder request = @@ -509,7 +504,7 @@ public void testDeleteFolder3() throws Exception { .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); - verify(webSocketHandler, times(1)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b")); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test 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 cae0b20b4f..43dc9378e9 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 @@ -281,7 +281,7 @@ public void testDeleteSnapshot1() throws Exception { when(nodeDAO.getNode("a")).thenReturn(node); - when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("a")); + //when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("a")); MockHttpServletRequestBuilder request = delete("/node") @@ -314,7 +314,7 @@ public void testDeleteSnapshot3() throws Exception { .uniqueId("a").userName("otherUser").build(); when(nodeDAO.getNode("a")).thenReturn(node); - when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("a")); + //when(nodeDAO.deleteNodes(List.of("a"))).thenReturn(Set.of("a")); MockHttpServletRequestBuilder request = delete("/node") From 7f1637e5bffcf2b66eecce042815368feb4acba5 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 30 Apr 2025 13:00:17 +0200 Subject: [PATCH 27/43] Further simplifications to node deletion --- .../ui/SaveAndRestoreController.java | 19 +++----------- .../ui/SaveAndRestoreService.java | 7 ++--- .../ConfigurationController.java | 18 +++++++------ .../ui/configuration/ConfigurationTab.java | 24 ++++++++++++++--- .../ui/snapshot/SnapshotController.java | 26 ++++++++++++------- .../ui/snapshot/SnapshotTab.java | 22 +++++++++++----- .../controllers/ConfigurationController.java | 2 +- .../web/controllers/SnapshotController.java | 2 +- 8 files changed, 71 insertions(+), 49 deletions(-) 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 9efe7157eb..2d24ffd4bc 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 @@ -453,7 +453,7 @@ private String getSavedFilterName() { protected void expandTreeNode(TreeItem targetItem) { List childNodes = saveAndRestoreService.getChildNodes(targetItem.getValue()); List> list = - childNodes.stream().map(n -> createTreeItem(n)).toList(); + childNodes.stream().map(this::createTreeItem).toList(); targetItem.getChildren().setAll(list); targetItem.getChildren().sort(treeNodeComparator); targetItem.setExpanded(true); @@ -470,9 +470,8 @@ private void compareSnapshot() { if (tab == null) { return; } - if (tab instanceof SnapshotTab) { + if (tab instanceof SnapshotTab currentTab) { try { - SnapshotTab currentTab = (SnapshotTab) tab; currentTab.addSnapshot(node); } catch (Exception e) { LOG.log(Level.WARNING, "Failed to compare snapshot", e); @@ -504,22 +503,10 @@ private void deleteTreeItems(ObservableList> items) { disabledUi.set(true); List nodeIds = items.stream().map(item -> item.getValue().getUniqueId()).collect(Collectors.toList()); - List tabsToRemove = new ArrayList<>(); - List visibleTabs = tabPane.getTabs(); - for (Tab tab : visibleTabs) { - for (TreeItem treeItem : items) { - if (treeItem.getValue().getUniqueId().equals(tab.getId())) { - tabsToRemove.add(tab); - } - } - } JobManager.schedule("Delete nodes", monitor -> { try { saveAndRestoreService.deleteNodes(nodeIds); - Platform.runLater(() -> { - disabledUi.set(false); - tabPane.getTabs().removeAll(tabsToRemove); - }); + disabledUi.set(false); } catch (Exception e) { ExceptionDetailsErrorDialog.openError(Messages.errorGeneric, MessageFormat.format(Messages.errorDeleteNodeFailed, items.get(0).getValue().getName()), 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 164fc09332..2ba915cdb9 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 @@ -80,7 +80,7 @@ public class SaveAndRestoreService { private final SaveAndRestoreClient saveAndRestoreClient; private final ObjectMapper objectMapper; - private WebSocketClient webSocketClient; + private final WebSocketClient webSocketClient; private SaveAndRestoreService() { saveAndRestoreClient = new SaveAndRestoreClientImpl(); @@ -198,8 +198,8 @@ public void removeDataChangeListener(DataChangeListener dataChangeListener){ * Moves the sourceNode to the targetNode. The target {@link Node} may not contain * any {@link Node} of same name and type as the source {@link Node}. *

- * Once the move completes successfully in the remote service, this method will updated both the source node's parent - * as well as the target node. This is needed in order to keep the view updated with the changes performed. + * Once the move completes successfully in the remote service, this method will update both the source node's parent + * and the target node. This is needed in order to keep the view updated with the changes performed. * * @param sourceNodes A list of {@link Node}s of type {@link NodeType#FOLDER} or {@link NodeType#CONFIGURATION}. * @param targetNode A {@link Node} of type {@link NodeType#FOLDER}. @@ -497,6 +497,7 @@ private void handleWebSocketMessage(CharSequence charSequence){ SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); webSocketMessageHandlers.forEach(w -> w.handleWebSocketMessage(saveAndRestoreWebSocketMessage)); + } catch (JsonProcessingException e) { throw new RuntimeException(e); } 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 6aefc899d6..513e60a38e 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 @@ -511,6 +511,12 @@ public synchronized void loadConfiguration(final Node node) { } + /** + * Handles clean-up when the associated {@link ConfigurationTab} is closed. + * A check is made if content is dirty, in which case user is prompted to cancel or close anyway. + * @return true if content is not dirty or user chooses to close anyway, + * otherwise false. + */ public boolean handleConfigurationTabClosed() { if (dirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); @@ -519,7 +525,10 @@ public boolean handleConfigurationTabClosed() { Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); } - return true; + else{ + saveAndRestoreService.removeWebSocketMessageHandler(this); + return true; + } } @Override @@ -531,11 +540,4 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore } } } - - /** - * Removes this as web socket message listener. - */ - public void handleTabClosed() { - saveAndRestoreService.removeWebSocketMessageHandler(this); - } } 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 12ef4f3691..ed050cb4ef 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,19 +18,24 @@ */ 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.SaveAndRestoreService; 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 { +public class ConfigurationTab extends SaveAndRestoreTab implements WebSocketMessageHandler { public ConfigurationTab() { configure(); @@ -66,10 +71,13 @@ private void configure() { setOnCloseRequest(event -> { if (!((ConfigurationController) controller).handleConfigurationTabClosed()) { event.consume(); - } else { - ((ConfigurationController) controller).handleTabClosed(); + } + else{ + SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); } }); + + SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); } /** @@ -89,4 +97,14 @@ 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/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index d26b226491..e678eb0c2c 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 @@ -209,14 +209,14 @@ public void saveSnapshot(ActionEvent actionEvent) { snapshot.setSnapshotNode(snapshotNode); try { - snapshot = SaveAndRestoreService.getInstance().saveSnapshot(configurationNode, snapshot); + Snapshot _snapshot = SaveAndRestoreService.getInstance().saveSnapshot(configurationNode, snapshot); snapshotProperty.set(snapshot); Node _snapshotNode = snapshot.getSnapshotNode(); if (snapshotControlsViewController.logAction()) { eventReceivers.forEach(r -> r.snapshotSaved(_snapshotNode, this::showLoggingError)); } snapshotControlsViewController.snapshotDataDirty.set(false); - loadSnapshot(snapshot.getSnapshotNode()); + Platform.runLater(() -> loadSnapshot(_snapshot.getSnapshotNode())); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to save snapshot", e); Platform.runLater(() -> { @@ -277,6 +277,13 @@ public void updateLoadedSnapshot(TableEntry rowValue, VType newValue) { }); } + /** + * Handles clean-up when the associated {@link SnapshotTab} is closed. + * A check is made if content is dirty, in which case user is prompted to cancel or close anyway. + * + * @return true if content is not dirty or user chooses to close anyway, + * otherwise false. + */ public boolean handleSnapshotTabClosed() { if (snapshotControlsViewController.snapshotDataDirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); @@ -284,13 +291,12 @@ public boolean handleSnapshotTabClosed() { alert.setContentText(Messages.promptCloseSnapshotTabContent); DialogHelper.positionDialog(alert, borderPane, -150, -150); Optional result = alert.showAndWait(); - if (result.isPresent() && result.get().equals(ButtonType.CANCEL)) { - return false; - } + return result.isPresent() && result.get().equals(ButtonType.OK); + } else { + saveAndRestoreService.removeWebSocketMessageHandler(this); + dispose(); + return true; } - saveAndRestoreService.removeWebSocketMessageHandler(this); - dispose(); - return true; } /** @@ -357,7 +363,7 @@ public void applyHideEqualItems() { private void loadSnapshotInternal(Node snapshotNode) { - Platform.runLater(() -> disabledUi.set(true)); + disabledUi.set(true); JobManager.schedule("Load snapshot items", monitor -> { try { Snapshot snapshot = getSnapshotFromService(snapshotNode); @@ -369,7 +375,7 @@ private void loadSnapshotInternal(Node snapshotNode) { setTabImage(snapshotNode); }); } finally { - Platform.runLater(() -> disabledUi.set(false)); + disabledUi.set(false); } }); } 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 07a7ef370e..deb2540590 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 @@ -18,7 +18,6 @@ package org.phoebus.applications.saveandrestore.ui.snapshot; import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXMLLoader; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; @@ -28,12 +27,12 @@ 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.Tag; -import org.phoebus.applications.saveandrestore.ui.DataChangeListener; -import org.phoebus.applications.saveandrestore.ui.ImageRepository; +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; @@ -52,7 +51,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 DataChangeListener { +public class SnapshotTab extends SaveAndRestoreTab implements WebSocketMessageHandler { public SaveAndRestoreService saveAndRestoreService; @@ -101,7 +100,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save if (controller != null && !((SnapshotController) controller).handleSnapshotTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeDataChangeListener(this); + SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); } }); @@ -117,7 +116,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save }); getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem); - SaveAndRestoreService.getInstance().addDataChangeListener(this); + SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); } /** @@ -155,4 +154,13 @@ 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/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 f246190a52..45a0bc52d3 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 @@ -79,7 +79,7 @@ public Configuration createConfiguration(@RequestParam(value = "parentNodeId") S } configuration.getConfigurationNode().setUserName(principal.getName()); Configuration newConfiguration = nodeDAO.createConfiguration(parentNodeId, configuration); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, parentNodeId)); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newConfiguration.getConfigurationNode().getUniqueId())); return newConfiguration; } 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 dd5eb30fa5..5f9ed63217 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 @@ -80,7 +80,7 @@ public Snapshot createSnapshot(@RequestParam(value = "parentNodeId") String pare } snapshot.getSnapshotNode().setUserName(principal.getName()); Snapshot newSnapshot = nodeDAO.createSnapshot(parentNodeId, snapshot); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, parentNodeId)); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newSnapshot.getSnapshotNode().getUniqueId())); return newSnapshot; } From f59b888228573758b65d93803fb8613ae8067c51 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 30 Apr 2025 13:42:29 +0200 Subject: [PATCH 28/43] Server side web socket message for composite snapshot --- .../CompositeSnapshotController.java | 14 ++- .../CompositeSnapshotControllerTest.java | 112 +++++++++++++++--- 2 files changed, 107 insertions(+), 19 deletions(-) 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 0b73c05108..5fd64d7e46 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 @@ -20,7 +20,10 @@ 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.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; @@ -44,6 +47,9 @@ public class CompositeSnapshotController extends BaseController { @Autowired private NodeDAO nodeDAO; + @Autowired + private WebSocketHandler webSocketHandler; + /** * Creates a new {@link CompositeSnapshot} {@link Node}. * @param parentNodeId Valid id of the {@link Node}s intended parent. @@ -60,7 +66,9 @@ public CompositeSnapshot createCompositeSnapshot(@RequestParam(value = "parentNo throw new IllegalArgumentException("Composite snapshot node of wrong type"); } compositeSnapshot.getCompositeSnapshotNode().setUserName(principal.getName()); - return nodeDAO.createCompositeSnapshot(parentNodeId, compositeSnapshot); + CompositeSnapshot newCompositeSnapshot = nodeDAO.createCompositeSnapshot(parentNodeId, compositeSnapshot); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newCompositeSnapshot.getCompositeSnapshotNode().getUniqueId())); + return newCompositeSnapshot; } /** @@ -77,7 +85,9 @@ public CompositeSnapshot updateCompositeSnapshot(@RequestBody CompositeSnapshot throw new IllegalArgumentException("Composite snapshot node of wrong type"); } compositeSnapshot.getCompositeSnapshotNode().setUserName(principal.getName()); - return nodeDAO.updateCompositeSnapshot(compositeSnapshot); + CompositeSnapshot updatedCompositeSnapshot = nodeDAO.updateCompositeSnapshot(compositeSnapshot); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedCompositeSnapshot.getCompositeSnapshotNode())); + return updatedCompositeSnapshot; } /** 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 8d4162cf24..1eb9fdfd21 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,6 +21,8 @@ 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; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,8 +32,10 @@ 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.service.saveandrestore.persistence.dao.NodeDAO; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; @@ -45,6 +49,8 @@ import java.util.List; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +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.get; @@ -77,6 +83,9 @@ public class CompositeSnapshotControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private WebSocketHandler webSocketHandler; + private final ObjectMapper objectMapper = new ObjectMapper(); private static CompositeSnapshot compositeSnapshot; @@ -91,8 +100,13 @@ public static void init() { compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); } + @AfterEach + public void resetMocks(){ + reset(nodeDAO, webSocketHandler); + } + @Test - public void testCreateCompositeSnapshot() throws Exception { + public void testCreateCompositeSnapshot1() throws Exception { when(nodeDAO.createCompositeSnapshot(Mockito.any(String.class), Mockito.any(CompositeSnapshot.class))).thenReturn(compositeSnapshot); @@ -110,27 +124,54 @@ public void testCreateCompositeSnapshot() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshot.class); - request = put("/composite-snapshot?parentNodeId=id") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateCompositeSnapshot2() throws Exception { + + when(nodeDAO.createCompositeSnapshot(Mockito.any(String.class), Mockito.any(CompositeSnapshot.class))).thenReturn(compositeSnapshot); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot); + + MockHttpServletRequestBuilder request = put("/composite-snapshot?parentNodeId=id") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(compositeSnapshotString); mockMvc.perform(request).andExpect(status().isOk()); - request = put("/composite-snapshot?parentNodeId=id") + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateCompositeSnapshot3() throws Exception { + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot); + + MockHttpServletRequestBuilder request = put("/composite-snapshot?parentNodeId=id") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON) .content(compositeSnapshotString); mockMvc.perform(request).andExpect(status().isForbidden()); - request = put("/composite-snapshot?parentNodeId=id") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCreateCompositeSnapshot4() throws Exception { + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot); + + MockHttpServletRequestBuilder request = put("/composite-snapshot?parentNodeId=id") .contentType(JSON) .content(compositeSnapshotString); mockMvc.perform(request).andExpect(status().isUnauthorized()); - reset(nodeDAO); + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } @Test @@ -144,10 +185,12 @@ public void testCreateCompositeSnapshotWrongNodeType() throws Exception{ .contentType(JSON) .content(objectMapper.writeValueAsString(compositeSnapshot1)); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test - public void testUpdateCompositeSnapshot() throws Exception { + public void testUpdateCompositeSnapshot1() throws Exception { Node node = Node.builder().uniqueId("c").nodeType(NodeType.COMPOSITE_SNAPSHOT).userName(demoUser).build(); CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); @@ -170,16 +213,42 @@ public void testUpdateCompositeSnapshot() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshot.class); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateCompositeSnapshot2() throws Exception { + + Node node = Node.builder().uniqueId("c").nodeType(NodeType.COMPOSITE_SNAPSHOT).userName(demoUser).build(); when(nodeDAO.getNode("c")).thenReturn(Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).uniqueId("c").userName("notUser").build()); - request = post("/composite-snapshot") + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot1); + + MockHttpServletRequestBuilder request = post("/composite-snapshot") .header(HttpHeaders.AUTHORIZATION, userAuthorization) .contentType(JSON) .content(compositeSnapshotString); mockMvc.perform(request).andExpect(status().isForbidden()); - request = post("/composite-snapshot") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateCompositeSnapshot3() throws Exception { + + Node node = Node.builder().uniqueId("c").nodeType(NodeType.COMPOSITE_SNAPSHOT).userName(demoUser).build(); + when(nodeDAO.getNode("c")).thenReturn(Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).uniqueId("c").userName("notUser").build()); + + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot1); + + MockHttpServletRequestBuilder request = post("/composite-snapshot") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON) .content(compositeSnapshotString); @@ -192,16 +261,30 @@ public void testUpdateCompositeSnapshot() throws Exception { mockMvc.perform(request).andExpect(status().isUnauthorized()); + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testUpdateCompositeSnapshot4() throws Exception { + + Node node = Node.builder().uniqueId("c").nodeType(NodeType.COMPOSITE_SNAPSHOT).userName(demoUser).build(); when(nodeDAO.getNode("c")).thenReturn(Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).uniqueId("c").userName("notUser").build()); - request = post("/composite-snapshot") + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot1); + + when(nodeDAO.updateCompositeSnapshot(compositeSnapshot1)).thenReturn(compositeSnapshot1); + + MockHttpServletRequestBuilder request = post("/composite-snapshot") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(compositeSnapshotString); mockMvc.perform(request).andExpect(status().isOk()); - reset(nodeDAO); + verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test @@ -217,6 +300,8 @@ public void testUpdateCompositeSnapshotWrongNodeType() throws Exception{ .contentType(JSON) .content(objectMapper.writeValueAsString(compositeSnapshot1)); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test @@ -236,7 +321,6 @@ public void testGetCompositeSnapshotData() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshotData.class); - reset(nodeDAO); } @Test @@ -257,8 +341,6 @@ public void testGetCompositeSnapshotNodes() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, new TypeReference>() { }); - - reset(nodeDAO); } @Test @@ -276,8 +358,6 @@ public void testGetCompositeSnapshotItems() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, new TypeReference>() { }); - - reset(nodeDAO); } @Test @@ -317,7 +397,5 @@ public void testGetCompositeSnapshotConsistency() throws Exception { .content(objectMapper.writeValueAsString(List.of("id"))); mockMvc.perform(request).andExpect(status().isOk()); - - reset(nodeDAO); } } From 0677e92ab9d2b76fa58a3f95fb1f3bb2d98f92c5 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 30 Apr 2025 20:22:27 +0200 Subject: [PATCH 29/43] Allow change of composite snapshot name in snapshot view, several fixes --- .../applications/saveandrestore/Messages.java | 1 + .../ConfigurationController.java | 30 ++++++++++++++----- .../snapshot/CompositeSnapshotController.java | 29 ++++++++++-------- .../ui/snapshot/CompositeSnapshotTab.java | 19 ++---------- .../SnapshotControlsViewController.java | 1 - .../saveandrestore/messages.properties | 1 + 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index c78868d0ab..e5ec5bf940 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -179,6 +179,7 @@ public class Messages { public static String toolTipConfigurationExistsOption; public static String toolTipMultiplierSpinner; public static String unnamedConfiguration; + public static String unnamedCompositeSnapshot; public static String unnamedSnapshot; public static String updateCompositeSnapshotFailed; 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 513e60a38e..52caf4dae7 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 @@ -24,6 +24,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -76,6 +77,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -175,6 +177,7 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private final BooleanProperty dirty = new SimpleBooleanProperty(); private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(); + /** * Manages the id of the containing {@link javafx.scene.control.Tab}. This property will * also indicate if the UI has been configured to edit a new or existing configuration: for a new configuration @@ -182,6 +185,9 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem */ private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); + private ChangeListener nodeNameChangeListener; + private ChangeListener descriptionChangeListener; + public ConfigurationController(ConfigurationTab configurationTab) { configurationTab.textProperty().bind(tabTitleProperty); configurationTab.idProperty().bind(tabIdProperty); @@ -332,7 +338,6 @@ public void commitEdit(Double value) { while (change.next()) { if (change.wasAdded() || change.wasRemoved()) { FXCollections.sort(configurationEntries); - //dirty.setValue(true); } } }); @@ -346,10 +351,8 @@ public void commitEdit(Double value) { } }); - configurationNameProperty.addListener((observableValue, oldValue, newValue) -> - dirty.setValue(oldValue != null && newValue != null)); - configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> - dirty.setValue(oldValue != null && newValue != null)); + nodeNameChangeListener = (observableValue, oldValue, newValue) -> dirty.setValue(true); + descriptionChangeListener = (observableValue, oldValue, newValue) -> dirty.setValue(true); saveButton.disableProperty().bind(Bindings.createBooleanBinding(() -> dirty.not().get() || configurationDescriptionProperty.isEmpty().get() || @@ -470,6 +473,7 @@ private void resetAddPv() { */ public void newConfiguration(Node parentNode) { configurationNodeParent = parentNode; + addListeners(); tabTitleProperty.setValue(Messages.unnamedConfiguration); Platform.runLater(() -> configurationNameField.requestFocus()); } @@ -480,6 +484,7 @@ public void newConfiguration(Node parentNode) { * @param node An existing {@link Node} of type {@link NodeType#CONFIGURATION}. */ public synchronized void loadConfiguration(final Node node) { + removeListeners(); JobManager.schedule("Load save&restore configuration", monitor -> { final ConfigurationData configurationData; try { @@ -503,6 +508,7 @@ public synchronized void loadConfiguration(final Node node) { createdByField.textProperty().set(node.getUserName()); configurationDescriptionProperty.set(node.getDescription()); dirty.setValue(false); + addListeners(); } catch (Exception e) { logger.log(Level.WARNING, "Unable to load existing configuration"); } @@ -514,6 +520,7 @@ public synchronized void loadConfiguration(final Node node) { /** * Handles clean-up when the associated {@link ConfigurationTab} is closed. * A check is made if content is dirty, in which case user is prompted to cancel or close anyway. + * * @return true if content is not dirty or user chooses to close anyway, * otherwise false. */ @@ -524,8 +531,7 @@ public boolean handleConfigurationTabClosed() { alert.setContentText(Messages.closeConfigurationWarning); Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); - } - else{ + } else { saveAndRestoreService.removeWebSocketMessageHandler(this); return true; } @@ -540,4 +546,14 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore } } } + + private void addListeners() { + configurationNameProperty.addListener(nodeNameChangeListener); + configurationDescriptionProperty.addListener(descriptionChangeListener); + } + + private void removeListeners() { + configurationNameProperty.removeListener(nodeNameChangeListener); + configurationDescriptionProperty.removeListener(descriptionChangeListener); + } } 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 d1f87eeb7c..1261e4964b 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 @@ -73,6 +73,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Stack; import java.util.function.Consumer; @@ -126,7 +127,7 @@ public class CompositeSnapshotController extends SaveAndRestoreBaseController { private SaveAndRestoreService saveAndRestoreService; - private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(false); + private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(); private final ObservableList snapshotEntries = FXCollections.observableArrayList(); @@ -143,12 +144,13 @@ public class CompositeSnapshotController extends SaveAndRestoreBaseController { private Node compositeSnapshotNode; - private final CompositeSnapshotTab compositeSnapshotTab; - private final SimpleBooleanProperty disabledUi = new SimpleBooleanProperty(false); private final SaveAndRestoreController saveAndRestoreController; + private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(Messages.unnamedCompositeSnapshot); + private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); + @SuppressWarnings("unused") @FXML private VBox progressIndicator; @@ -159,8 +161,9 @@ public class CompositeSnapshotController extends SaveAndRestoreBaseController { public CompositeSnapshotController(CompositeSnapshotTab compositeSnapshotTab, SaveAndRestoreController saveAndRestoreController) { - this.compositeSnapshotTab = compositeSnapshotTab; this.saveAndRestoreController = saveAndRestoreController; + compositeSnapshotTab.textProperty().bind(tabTitleProperty); + compositeSnapshotTab.idProperty().bind(tabIdProperty); } @FXML @@ -297,8 +300,8 @@ public void updateItem(Node item, boolean empty) { snapshotTable.setItems(snapshotEntries); - nodeNameChangeListener = (observableValue, oldValue, newValue) -> dirty.set(true); - descriptionChangeListener = (observableValue, oldValue, newValue) -> dirty.set(true); + nodeNameChangeListener = (observableValue, oldValue, newValue) -> dirty.setValue(true); + descriptionChangeListener = (observableValue, oldValue, newValue) -> dirty.setValue(true); saveButton.disableProperty().bind(Bindings.createBooleanBinding(() -> dirty.not().get() || compositeSnapshotDescriptionProperty.isEmpty().get() || @@ -330,8 +333,10 @@ public void updateItem(Node item, boolean empty) { disabledUi.addListener((observable, oldValue, newValue) -> borderPane.setDisable(newValue)); dirty.addListener((ob, o, n) -> { - if (dirty.get()) { - compositeSnapshotTab.annotateDirty(n); + if (n && !tabTitleProperty.get().startsWith("* ")) { + Platform.runLater(() -> tabTitleProperty.setValue("* " + tabTitleProperty.get())); + } else if (!n && tabTitleProperty.get().startsWith("* ")) { + Platform.runLater(() -> tabTitleProperty.setValue(tabTitleProperty.get().substring(2))); } }); } @@ -358,12 +363,12 @@ private void doSave(Consumer completion) { if (compositeSnapshotNode.getUniqueId() == null) { // New composite snapshot compositeSnapshot = saveAndRestoreService.saveCompositeSnapshot(parentFolder, compositeSnapshot); - compositeSnapshotTab.setId(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); + tabIdProperty.setValue(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); } else { compositeSnapshotData.setUniqueId(compositeSnapshotNode.getUniqueId()); compositeSnapshot = saveAndRestoreService.updateCompositeSnapshot(compositeSnapshot); } - compositeSnapshotTab.setNodeName(compositeSnapshot.getCompositeSnapshotNode().getName()); + //tabTitleProperty.setValue(compositeSnapshot.getCompositeSnapshotNode().getName()); dirty.set(false); completion.accept(compositeSnapshot); } catch (Exception e1) { @@ -396,6 +401,8 @@ public void loadCompositeSnapshot(final Node node, final List snapshotNode // Add change listener added only after the saved entries have been loaded. Collections.sort(snapshotEntries); Platform.runLater(() -> { + tabTitleProperty.setValue(node.getName()); + tabIdProperty.setValue(node.getUniqueId()); snapshotTable.setItems(snapshotEntries); compositeSnapshotNameProperty.set(compositeSnapshotNode.getName()); compositeSnapshotDescriptionProperty.set(compositeSnapshotNode.getDescription()); @@ -406,7 +413,6 @@ public void loadCompositeSnapshot(final Node node, final List snapshotNode createdByProperty.set(compositeSnapshotNode.getUserName()); addToCompositeSnapshot(snapshotNodes); addListeners(); - }); } catch (Exception e) { ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); @@ -442,7 +448,6 @@ public void newCompositeSnapshot(Node parentNode, List snapshotNodes) { dirty.set(false); } else { dirty.set(true); - //snapshotEntries.addAll(snapshotNodes); addToCompositeSnapshot(snapshotNodes); } addListeners(); 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 a42af6d08b..362cca2a43 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 @@ -48,8 +48,6 @@ */ public class CompositeSnapshotTab extends SaveAndRestoreTab { - private final SimpleStringProperty tabTitleProperty = new SimpleStringProperty(Messages.contextMenuNewCompositeSnapshot); - private final SaveAndRestoreController saveAndRestoreController; public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { @@ -89,7 +87,6 @@ private void configure() { setContent(rootNode); setGraphic(new ImageView(ImageRepository.COMPOSITE_SNAPSHOT)); - textProperty().bind(tabTitleProperty); setOnCloseRequest(event -> { if (!((CompositeSnapshotController) controller).handleCompositeSnapshotTabClosed()) { @@ -98,19 +95,9 @@ private void configure() { }); } - public void setNodeName(String nodeName) { - Platform.runLater(() -> tabTitleProperty.set("[" + Messages.Edit + "] " + nodeName)); - } - - public void annotateDirty(boolean dirty) { - String tabTitle = tabTitleProperty.get(); - if (dirty) { - Platform.runLater(() -> tabTitleProperty.set("* " + tabTitle)); - } - } public void configureForNewCompositeSnapshot(Node parentNode, List snapshotNodes) { - tabTitleProperty.set(Messages.contextMenuNewCompositeSnapshot); + //tabTitleProperty.set(Messages.contextMenuNewCompositeSnapshot); ((CompositeSnapshotController) controller).newCompositeSnapshot(parentNode, snapshotNodes); } @@ -122,8 +109,8 @@ public void configureForNewCompositeSnapshot(Node parentNode, List snapsho * be added to the list of references snapshots. */ public void editCompositeSnapshot(Node compositeSnapshotNode, List snapshotNodes) { - setId("edit_" + compositeSnapshotNode.getUniqueId()); - setNodeName(compositeSnapshotNode.getName()); + //setId("edit_" + compositeSnapshotNode.getUniqueId()); + //setNodeName(compositeSnapshotNode.getName()); ((CompositeSnapshotController) controller).loadCompositeSnapshot(compositeSnapshotNode, snapshotNodes); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java index 046d0507f7..f4e3ba338a 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java @@ -193,7 +193,6 @@ public void initialize() { saveSnapshotButton.disableProperty().bind(Bindings.createBooleanBinding(() -> // TODO: support save (=update) a composite snapshot from the snapshot view. In the meanwhile, disable save button. snapshotNodeProperty.isNull().get() || - snapshotNodeProperty.get().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT) || snapshotDataDirty.not().get() || snapshotNameProperty.isEmpty().get() || snapshotCommentProperty.isEmpty().get() || diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 38e45850f9..18ef48875a 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -263,6 +263,7 @@ toolTipConfigurationExistsOption=Choose it with Browse if you want to add PVs in toolTipMultiplierSpinner=This field only takes number. UniqueId=Unique ID unnamedConfiguration= +unnamedCompositeSnapshot= unnamedSnapshot= updateConfigurationFailed=Failed to update configuration updateCompositeSnapshotFailed=Failed to update composite snapshot From 9e7ffcaf8b42327d943c6554b6289f148aafe4bf Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 30 Apr 2025 21:23:41 +0200 Subject: [PATCH 30/43] Add web socket handling in composite snapshot controller --- .../snapshot/CompositeSnapshotController.java | 23 +++++++++++++-- .../ui/snapshot/CompositeSnapshotTab.java | 28 ++++++++++++++----- 2 files changed, 41 insertions(+), 10 deletions(-) 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 1261e4964b..5ab3946d8c 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 @@ -58,10 +58,13 @@ 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.ui.ImageRepository; 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.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -79,7 +82,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -public class CompositeSnapshotController extends SaveAndRestoreBaseController { +public class CompositeSnapshotController extends SaveAndRestoreBaseController implements WebSocketMessageHandler { @SuppressWarnings("unused") @FXML @@ -339,6 +342,8 @@ public void updateItem(Node item, boolean empty) { Platform.runLater(() -> tabTitleProperty.setValue(tabTitleProperty.get().substring(2))); } }); + + saveAndRestoreService.addWebSocketMessageHandler(this); } @FXML @@ -368,7 +373,6 @@ private void doSave(Consumer completion) { compositeSnapshotData.setUniqueId(compositeSnapshotNode.getUniqueId()); compositeSnapshot = saveAndRestoreService.updateCompositeSnapshot(compositeSnapshot); } - //tabTitleProperty.setValue(compositeSnapshot.getCompositeSnapshotNode().getName()); dirty.set(false); completion.accept(compositeSnapshot); } catch (Exception e1) { @@ -430,7 +434,10 @@ public boolean handleCompositeSnapshotTabClosed() { Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); } - return true; + else{ + saveAndRestoreService.removeWebSocketMessageHandler(this); + return true; + } } /** @@ -538,4 +545,14 @@ private void removeListeners() { compositeSnapshotNameProperty.removeListener(nodeNameChangeListener); compositeSnapshotDescriptionProperty.removeListener(descriptionChangeListener); } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { + Node node = (Node) saveAndRestoreWebSocketMessage.payload(); + if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { + loadCompositeSnapshot(node, Collections.emptyList()); + } + } + } } 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 362cca2a43..866279b67f 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 @@ -25,9 +25,13 @@ 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.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; @@ -46,16 +50,13 @@ * {@link SnapshotTab} is used to show actual snapshot data. *

*/ -public class CompositeSnapshotTab extends SaveAndRestoreTab { +public class CompositeSnapshotTab extends SaveAndRestoreTab implements WebSocketMessageHandler { private final SaveAndRestoreController saveAndRestoreController; public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { this.saveAndRestoreController = saveAndRestoreController; - configure(); - } - private void configure() { ResourceBundle resourceBundle = NLS.getMessages(Messages.class); FXMLLoader loader = new FXMLLoader(); loader.setResources(resourceBundle); @@ -92,12 +93,16 @@ private void configure() { if (!((CompositeSnapshotController) controller).handleCompositeSnapshotTabClosed()) { event.consume(); } + else { + SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); + } }); + + SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); } public void configureForNewCompositeSnapshot(Node parentNode, List snapshotNodes) { - //tabTitleProperty.set(Messages.contextMenuNewCompositeSnapshot); ((CompositeSnapshotController) controller).newCompositeSnapshot(parentNode, snapshotNodes); } @@ -109,8 +114,6 @@ public void configureForNewCompositeSnapshot(Node parentNode, List snapsho * be added to the list of references snapshots. */ public void editCompositeSnapshot(Node compositeSnapshotNode, List snapshotNodes) { - //setId("edit_" + compositeSnapshotNode.getUniqueId()); - //setNodeName(compositeSnapshotNode.getName()); ((CompositeSnapshotController) controller).loadCompositeSnapshot(compositeSnapshotNode, snapshotNodes); } @@ -123,4 +126,15 @@ public void editCompositeSnapshot(Node compositeSnapshotNode, List snapsho 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)); + } + } + } + } From 3fd9bc341ae87919f1d7d3d68abd34c79fccb912 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 1 May 2025 09:27:46 +0200 Subject: [PATCH 31/43] Handle concurrent load of composite snapshot --- .../ui/snapshot/CompositeSnapshotController.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 5ab3946d8c..ef5c01ecbd 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 @@ -399,14 +399,13 @@ public void loadCompositeSnapshot(final Node node, final List snapshotNode removeListeners(); JobManager.schedule("Load composite snapshot data", monitor -> { try { - snapshotEntries.clear(); List referencedNodes = saveAndRestoreService.getCompositeSnapshotNodes(compositeSnapshotNode.getUniqueId()); - snapshotEntries.addAll(referencedNodes); // Add change listener added only after the saved entries have been loaded. Collections.sort(snapshotEntries); Platform.runLater(() -> { tabTitleProperty.setValue(node.getName()); tabIdProperty.setValue(node.getUniqueId()); + snapshotEntries.setAll(referencedNodes); snapshotTable.setItems(snapshotEntries); compositeSnapshotNameProperty.set(compositeSnapshotNode.getName()); compositeSnapshotDescriptionProperty.set(compositeSnapshotNode.getDescription()); @@ -415,8 +414,8 @@ public void loadCompositeSnapshot(final Node node, final List snapshotNode lastUpdatedProperty.set(compositeSnapshotNode.getLastModified() != null ? TimestampFormats.SECONDS_FORMAT.format(Instant.ofEpochMilli(compositeSnapshotNode.getLastModified().getTime())) : null); createdByProperty.set(compositeSnapshotNode.getUserName()); - addToCompositeSnapshot(snapshotNodes); addListeners(); + dirty.setValue(false); }); } catch (Exception e) { ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); From 6bcade654f468c42087e2758afcf61ce2929f7c5 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Sun, 4 May 2025 15:58:23 +0200 Subject: [PATCH 32/43] Display web socket message when nodes are moved --- .../saveandrestore/ui/NodeAddedListener.java | 15 --------- .../ui/NodeChangedListener.java | 30 ----------------- .../ui/SaveAndRestoreController.java | 32 ++++--------------- .../ui/SaveAndRestoreService.java | 6 ++-- .../ui/search/SearchAndFilterTab.java | 3 +- .../web/controllers/StructureController.java | 16 +++++++++- 6 files changed, 24 insertions(+), 78 deletions(-) delete mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java delete mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeChangedListener.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java deleted file mode 100644 index ba384b9b77..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.applications.saveandrestore.ui; - -public interface NodeAddedListener { - - /** - * To be called when a new node has been created (typically new snapshot node). - * - * @param parentNodeId The unique id of the new node's parent node. - */ - void nodeAdded(String parentNodeId); -} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeChangedListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeChangedListener.java deleted file mode 100644 index 7fba282bec..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeChangedListener.java +++ /dev/null @@ -1,30 +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.applications.saveandrestore.ui; - -import org.phoebus.applications.saveandrestore.model.Node; - -public interface NodeChangedListener { - - /** - * To be called when an existing node has been changed, e.g. renamed or a property has changed. - * - * @param node The updated node. - */ - void nodeChanged(Node node); -} 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 2d24ffd4bc..0d24ba4061 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 @@ -456,9 +456,6 @@ protected void expandTreeNode(TreeItem targetItem) { childNodes.stream().map(this::createTreeItem).toList(); targetItem.getChildren().setAll(list); targetItem.getChildren().sort(treeNodeComparator); - targetItem.setExpanded(true); - treeView.getSelectionModel().clearSelection(); - treeView.getSelectionModel().select(null); } /** @@ -754,17 +751,7 @@ private void renameNode() { if (result.isPresent()) { node.getValue().setName(result.get()); try { - String parentNodeIdBefore = saveAndRestoreService.getParentNode(node.getValue().getUniqueId()).getUniqueId(); saveAndRestoreService.updateNode(node.getValue()); - // Node updated... Does it have a new parent, i.e. has it been moved in the tree structure? - String parentNodeIdAfter = saveAndRestoreService.getParentNode(node.getValue().getUniqueId()).getUniqueId(); - if(parentNodeIdAfter.equals(parentNodeIdBefore)){ - return; - } - // New parent node, update UI - Stack copiedStack = new Stack<>(); - DirectoryUtilities.CreateLocationStringAndNodeStack(node.getValue(), false).getValue().forEach(copiedStack::push); - locateNode(copiedStack); } catch (Exception e) { Alert alert = new Alert(AlertType.ERROR); alert.setTitle(Messages.errorActionFailed); @@ -802,6 +789,11 @@ private void nodeChanged(Node node) { return; } nodeSubjectToUpdate.setValue(node); + // For updated and expanded folder nodes, refresh with respect to child nodes as + // a move/copy operation may add/remove nodes. + if(nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.FOLDER) && nodeSubjectToUpdate.isExpanded()){ + expandTreeNode(nodeSubjectToUpdate); + } } /** @@ -942,10 +934,6 @@ public void handleTabClosed() { saveLocalState(); saveAndRestoreService.closeWebSocket(); saveAndRestoreService.removeWebSocketMessageHandler(this); - //saveAndRestoreService.removeNodeChangeListener(this); - //saveAndRestoreService.removeNodeAddedListener(this); - //saveAndRestoreService.removeFilterChangeListener(this); - //webSocketClient.close("User closing " + SaveAndRestoreApplication.DISPLAY_NAME); } /** @@ -1046,18 +1034,10 @@ public boolean selectedNodesOfSameType() { protected void moveNodes(List sourceNodes, Node targetNode, TransferMode transferMode) { disabledUi.set(true); JobManager.schedule("Copy Or Move save&restore node(s)", monitor -> { - TreeItem rootTreeItem = treeView.getRoot(); - TreeItem targetTreeItem = recursiveSearch(targetNode.getUniqueId(), rootTreeItem); try { - TreeItem sourceTreeItem = recursiveSearch(sourceNodes.get(0).getUniqueId(), rootTreeItem); - TreeItem sourceParentTreeItem = sourceTreeItem.getParent(); if (transferMode.equals(TransferMode.MOVE)) { saveAndRestoreService.moveNodes(sourceNodes, targetNode); - Platform.runLater(() -> { - removeMovedNodes(sourceParentTreeItem, sourceNodes); - addMovedNodes(targetTreeItem, sourceNodes); - }); - } // TransferMode.COPY not supported + }// TransferMode.COPY not supported } catch (Exception exception) { Logger.getLogger(SaveAndRestoreController.class.getName()) .log(Level.SEVERE, "Failed to move or copy"); 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 2ba915cdb9..1ee2485031 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 @@ -86,7 +86,7 @@ private SaveAndRestoreService() { saveAndRestoreClient = new SaveAndRestoreClientImpl(); String baseUrl = Preferences.jmasarServiceUrl; String schema = baseUrl.startsWith("https") ? "wss" : "ws"; - String webSocketUrl = schema + baseUrl.substring(baseUrl.indexOf("://", 0)) + "/web-socket"; + String webSocketUrl = schema + baseUrl.substring(baseUrl.indexOf("://")) + "/web-socket"; URI webSocketUri = URI.create(webSocketUrl); webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketConnect, this::handleWebSocketDisconnect, this::handleWebSocketMessage); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); @@ -144,9 +144,7 @@ public Node updateNode(Node nodeToUpdate) throws Exception { public Node updateNode(Node nodeToUpdate, boolean customTimeForMigration) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.updateNode(nodeToUpdate, customTimeForMigration)); - Node node = future.get(); - dataChangeListeners.forEach(l -> l.nodeChanged(node)); - return node; + return future.get(); } public Node createNode(String parentNodeId, Node newTreeNode) throws Exception { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java index f5a9792df8..ced3ce55fc 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java @@ -28,7 +28,6 @@ import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.ui.DataChangeListener; -import org.phoebus.applications.saveandrestore.ui.NodeChangedListener; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; @@ -120,7 +119,7 @@ public void showFilter(String filterId) { return; } Optional filterOptional = allFilters.stream().filter(f -> f.getName().equalsIgnoreCase(filterId)).findFirst(); - if (!filterOptional.isPresent()) { + if (filterOptional.isEmpty()) { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setContentText(MessageFormat.format(Messages.filterNotFound, filterId)); 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 4bfc3e1cc8..7a05b37f78 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,7 +18,10 @@ 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.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -41,6 +44,9 @@ public class StructureController extends BaseController { @Autowired private NodeDAO nodeDAO; + @Autowired + private WebSocketHandler webSocketHandler; + /** * Moves a list of source nodes to a new target (parent) node. * @@ -60,8 +66,16 @@ public Node moveNodes(@RequestParam(value = "to") String to, if (to.isEmpty() || nodes.isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target node and list of source nodes must all be non-empty."); } + // Get parent node before move + Node sourceParentNode = nodeDAO.getParentNode(nodes.get(0)); Logger.getLogger(StructureController.class.getName()).info(Thread.currentThread().getName() + " " + (new Date()) + " move"); - return nodeDAO.moveNodes(nodes, to, principal.getName()); + Node targetNode = nodeDAO.moveNodes(nodes, to, principal.getName()); + // Update clients + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, + targetNode)); + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, + sourceParentNode)); + return targetNode; } /** From aa2184009c6a578c9e0967556c8671b25e2a2cb5 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Sun, 4 May 2025 20:15:43 +0200 Subject: [PATCH 33/43] Finalize web socket messages related to filters --- .../saveandrestore/FilterViewInstance.java | 2 + .../ui/FilterChangeListener.java | 33 ----------- .../ui/SaveAndRestoreController.java | 6 +- .../ui/SaveAndRestoreService.java | 50 +++------------- .../ui/WebSocketMessageHandler.java | 2 +- .../ConfigurationController.java | 3 +- .../ui/configuration/ConfigurationTab.java | 2 +- .../ui/search/SearchAndFilterTab.java | 58 +------------------ .../search/SearchAndFilterViewController.java | 45 ++++++-------- .../SearchResultTableViewController.java | 27 +++++---- .../snapshot/CompositeSnapshotController.java | 6 +- .../ui/snapshot/CompositeSnapshotTab.java | 6 +- .../ui/snapshot/SnapshotController.java | 2 +- .../ui/snapshot/SnapshotTab.java | 2 +- 14 files changed, 60 insertions(+), 184 deletions(-) delete mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/FilterChangeListener.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/FilterViewInstance.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/FilterViewInstance.java index 59fd97ad9d..8dcaae5c83 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/FilterViewInstance.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/FilterViewInstance.java @@ -54,6 +54,8 @@ public FilterViewInstance(AppDescriptor appDescriptor) { return CompletableFuture.completedFuture(true); }); + dockItem.setOnCloseRequest(e -> searchResultTableViewController.handleTabClosed()); + DockPane.getActiveDockPane().addTab(dockItem); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/FilterChangeListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/FilterChangeListener.java deleted file mode 100644 index 108f204e5b..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/FilterChangeListener.java +++ /dev/null @@ -1,33 +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.applications.saveandrestore.ui; - -import org.phoebus.applications.saveandrestore.model.search.Filter; - -/** - * Interface to handle changes in save-n-restore {@link Filter}s. - */ -public interface FilterChangeListener { - - void filterAddedOrUpdated(Filter filter); - - void filterRemoved(Filter filter); - -} 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 0d24ba4061..3f8782071b 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 @@ -751,7 +751,7 @@ private void renameNode() { if (result.isPresent()) { node.getValue().setName(result.get()); try { - saveAndRestoreService.updateNode(node.getValue()); + saveAndRestoreService.updateNode(node.getValue(), false); } catch (Exception e) { Alert alert = new Alert(AlertType.ERROR); alert.setTitle(Messages.errorActionFailed); @@ -1422,8 +1422,8 @@ private void openNode(String nodeId) { return; } Stack copiedStack = new Stack<>(); - DirectoryUtilities.CreateLocationStringAndNodeStack(node, false).getValue().forEach(copiedStack::push); Platform.runLater(() -> { + DirectoryUtilities.CreateLocationStringAndNodeStack(node, false).getValue().forEach(copiedStack::push); locateNode(copiedStack); nodeDoubleClicked(node); }); @@ -1491,7 +1491,7 @@ private void addOptionalLoggingMenuItem() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage){ switch (saveAndRestoreWebSocketMessage.messageType()){ case NODE_ADDED -> nodeAdded((String)saveAndRestoreWebSocketMessage.payload()); case NODE_REMOVED -> nodeRemoved((String)saveAndRestoreWebSocketMessage.payload()); 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 1ee2485031..6b6773b5f7 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 @@ -71,7 +71,6 @@ public class SaveAndRestoreService { private final ExecutorService executor; - private final List dataChangeListeners = Collections.synchronizedList(new ArrayList<>()); private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); private static final Logger LOG = Logger.getLogger(SaveAndRestoreService.class.getName()); @@ -138,10 +137,6 @@ public List getChildNodes(Node node) { return null; } - public Node updateNode(Node nodeToUpdate) throws Exception { - return updateNode(nodeToUpdate, false); - } - public Node updateNode(Node nodeToUpdate, boolean customTimeForMigration) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.updateNode(nodeToUpdate, customTimeForMigration)); return future.get(); @@ -166,32 +161,18 @@ public Node getParentNode(String uniqueNodeId) throws Exception { public Configuration createConfiguration(final Node parentNode, final Configuration configuration) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.createConfiguration(parentNode.getUniqueId(), configuration)); - Configuration newConfiguration = future.get(); - dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved(parentNode.getUniqueId())); - return newConfiguration; + return future.get(); } public Configuration updateConfiguration(Configuration configuration) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.updateConfiguration(configuration)); - Configuration updatedConfiguration = future.get(); - // Associated configuration Node may have a new name - dataChangeListeners.forEach(l -> l.nodeChanged(updatedConfiguration.getConfigurationNode())); - return updatedConfiguration; + return future.get(); } public List getAllTags() throws Exception { Future> future = executor.submit(saveAndRestoreClient::getAllTags); return future.get(); } - - public void addDataChangeListener(DataChangeListener dataChangeListener){ - dataChangeListeners.add(dataChangeListener); - } - - public void removeDataChangeListener(DataChangeListener dataChangeListener){ - dataChangeListeners.remove(dataChangeListener); - } - /** * Moves the sourceNode to the targetNode. The target {@link Node} may not contain * any {@link Node} of same name and type as the source {@link Node}. @@ -256,10 +237,7 @@ public Snapshot saveSnapshot(Node configurationNode, Snapshot snapshot) throws E return saveAndRestoreClient.updateSnapshot(snapshot); } }); - Snapshot updatedSnapshot = future.get(); - // Notify listeners as the configuration node has a new child node. - dataChangeListeners.forEach(l -> l.nodeChanged(configurationNode)); - return updatedSnapshot; + return future.get(); } public List getCompositeSnapshotNodes(String compositeSnapshotNodeUniqueId) throws Exception { @@ -277,17 +255,12 @@ public List getCompositeSnapshotItems(String compositeSnapshotNode public CompositeSnapshot saveCompositeSnapshot(Node parentNode, CompositeSnapshot compositeSnapshot) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.createCompositeSnapshot(parentNode.getUniqueId(), compositeSnapshot)); - CompositeSnapshot newCompositeSnapshot = future.get(); - dataChangeListeners.forEach(l -> l.nodeAddedOrRemoved(parentNode.getUniqueId())); - return newCompositeSnapshot; + return future.get(); } public CompositeSnapshot updateCompositeSnapshot(final CompositeSnapshot compositeSnapshot) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.updateCompositeSnapshot(compositeSnapshot)); - CompositeSnapshot updatedCompositeSnapshot = future.get(); - // Associated composite snapshot Node may have a new name - dataChangeListeners.forEach(l -> l.nodeChanged(updatedCompositeSnapshot.getCompositeSnapshotNode())); - return updatedCompositeSnapshot; + return future.get(); } /** @@ -323,9 +296,7 @@ public SearchResult search(MultivaluedMap searchParams) throws E public Filter saveFilter(Filter filter) throws Exception { Future future = executor.submit(() -> saveAndRestoreClient.saveFilter(filter)); - Filter addedOrUpdatedFilter = future.get(); - dataChangeListeners.forEach(l -> l.filterAddedOrUpdated(filter)); - return addedOrUpdatedFilter; + return future.get(); } /** @@ -344,7 +315,6 @@ public List getAllFilters() throws Exception { */ public void deleteFilter(final Filter filter) throws Exception { executor.submit(() -> saveAndRestoreClient.deleteFilter(filter.getName())).get(); - dataChangeListeners.forEach(l -> l.filterRemoved(filter.getName())); } /** @@ -357,9 +327,7 @@ public void deleteFilter(final Filter filter) throws Exception { public List addTag(TagData tagData) throws Exception { Future> future = executor.submit(() -> saveAndRestoreClient.addTag(tagData)); - List updatedNodes = future.get(); - updatedNodes.forEach(n -> dataChangeListeners.forEach(l -> l.nodeChanged(n))); - return updatedNodes; + return future.get(); } /** @@ -372,9 +340,7 @@ public List addTag(TagData tagData) throws Exception { public List deleteTag(TagData tagData) throws Exception { Future> future = executor.submit(() -> saveAndRestoreClient.deleteTag(tagData)); - List updatedNodes = future.get(); - updatedNodes.forEach(n -> dataChangeListeners.forEach(l -> l.nodeChanged(n))); - return updatedNodes; + return future.get(); } /** 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 index 18ae939fd6..3e202fb660 100644 --- 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 @@ -8,5 +8,5 @@ public interface WebSocketMessageHandler { - void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage); + 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 52caf4dae7..816ed17835 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 @@ -77,7 +77,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -538,7 +537,7 @@ public boolean handleConfigurationTabClosed() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { Node node = (Node) saveAndRestoreWebSocketMessage.payload(); if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { 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 ed050cb4ef..43c83993ac 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 @@ -99,7 +99,7 @@ public void configureForNewConfiguration(Node parentNode) { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_REMOVED)) { String nodeId = (String) saveAndRestoreWebSocketMessage.payload(); if (getId() != null && nodeId.equals(getId())) { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java index ced3ce55fc..7a048e309b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java @@ -19,43 +19,30 @@ package org.phoebus.applications.saveandrestore.ui.search; -import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Node; -import javafx.scene.control.Alert; import javafx.scene.image.ImageView; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; -import org.phoebus.applications.saveandrestore.model.search.Filter; -import org.phoebus.applications.saveandrestore.ui.DataChangeListener; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; -import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.nls.NLS; -import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; import java.io.IOException; -import java.text.MessageFormat; -import java.util.List; -import java.util.Optional; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; -public class SearchAndFilterTab extends SaveAndRestoreTab implements DataChangeListener { +public class SearchAndFilterTab extends SaveAndRestoreTab { public static final String SEARCH_AND_FILTER_TAB_ID = "SearchAndFilterTab"; private SearchAndFilterViewController searchAndFilterViewController; - private final SaveAndRestoreService saveAndRestoreService; public SearchAndFilterTab(SaveAndRestoreController saveAndRestoreController) { setId(SEARCH_AND_FILTER_TAB_ID); - - saveAndRestoreService = SaveAndRestoreService.getInstance(); - final ResourceBundle bundle = NLS.getMessages(SaveAndRestoreApplication.class); FXMLLoader loader = new FXMLLoader(); @@ -66,8 +53,7 @@ public SearchAndFilterTab(SaveAndRestoreController saveAndRestoreController) { if (clazz.isAssignableFrom(SearchAndFilterViewController.class)) { return clazz.getConstructor(SaveAndRestoreController.class) .newInstance(saveAndRestoreController); - } - else if(clazz.isAssignableFrom(SearchResultTableViewController.class)){ + } else if (clazz.isAssignableFrom(SearchResultTableViewController.class)) { return clazz.getConstructor() .newInstance(); } @@ -91,45 +77,5 @@ else if(clazz.isAssignableFrom(SearchResultTableViewController.class)){ setText(Messages.search); setGraphic(new ImageView(ImageCache.getImage(ImageCache.class, "/icons/sar-search_18x18.png"))); - - setOnCloseRequest(event -> SaveAndRestoreService.getInstance().removeDataChangeListener(this)); - - saveAndRestoreService.addDataChangeListener(this); - } - - - @Override - public void nodeChanged(org.phoebus.applications.saveandrestore.model.Node updatedNode) { - searchAndFilterViewController.nodeChanged(updatedNode); - } - - /** - * Shows a {@link Filter} in the view. If the filter identified through the specified (unique) id does not - * exist, an error message is show. - * - * @param filterId Unique, case-sensitive name of a persisted {@link Filter}. - */ - public void showFilter(String filterId) { - JobManager.schedule("Show Filter", monitor -> { - List allFilters; - try { - allFilters = saveAndRestoreService.getAllFilters(); - } catch (Exception e) { - Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(Messages.failedGetFilters, e)); - return; - } - Optional filterOptional = allFilters.stream().filter(f -> f.getName().equalsIgnoreCase(filterId)).findFirst(); - if (filterOptional.isEmpty()) { - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setContentText(MessageFormat.format(Messages.filterNotFound, filterId)); - alert.show(); - }); - } else { - Filter filter = filterOptional.get(); - Platform.runLater(() -> - searchAndFilterViewController.setFilter(filter)); - } - }); } } 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 880b42904b..c7cc5eb74d 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 @@ -45,18 +45,17 @@ import javafx.scene.input.KeyCode; import javafx.scene.layout.VBox; import org.phoebus.applications.saveandrestore.Messages; -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.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil.Keys; -import org.phoebus.applications.saveandrestore.ui.DataChangeListener; -import org.phoebus.applications.saveandrestore.ui.FilterChangeListener; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; 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.framework.jobs.JobManager; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.autocomplete.PVAutocompleteMenu; @@ -79,7 +78,8 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -public class SearchAndFilterViewController extends SaveAndRestoreBaseController implements Initializable, DataChangeListener { +public class SearchAndFilterViewController extends SaveAndRestoreBaseController + implements Initializable, WebSocketMessageHandler { private final SaveAndRestoreController saveAndRestoreController; @@ -368,10 +368,10 @@ public void initialize(URL url, ResourceBundle resourceBundle) { filterNameTextField.textProperty().bindBidirectional(filterNameProperty); filterNameTextField.disableProperty().bind(saveAndRestoreController.getUserIdentity().isNull()); saveFilterButton.disableProperty().bind(Bindings.createBooleanBinding(() -> - filterNameProperty.isEmpty().get() || - saveAndRestoreController.getUserIdentity().isNull().get() || - uniqueIdProperty.isNotEmpty().get(), - filterNameProperty, saveAndRestoreController.getUserIdentity(), uniqueIdProperty)); + filterNameProperty.isEmpty().get() || + saveAndRestoreController.getUserIdentity().isNull().get() || + uniqueIdProperty.isNotEmpty().get(), + filterNameProperty, saveAndRestoreController.getUserIdentity(), uniqueIdProperty)); query.addListener((observable, oldValue, newValue) -> { if (newValue == null || newValue.isEmpty()) { @@ -383,7 +383,7 @@ public void initialize(URL url, ResourceBundle resourceBundle) { loadFilters(); - saveAndRestoreService.addDataChangeListener(this); + saveAndRestoreService.addWebSocketMessageHandler(this); progressIndicator.visibleProperty().bind(disableUi); disableUi.addListener((observable, oldValue, newValue) -> mainUi.setDisable(newValue)); @@ -635,28 +635,21 @@ private void updatedQueryEditor() { searchDisabled = false; } - - public void nodeChanged(Node updatedNode) { - searchResultTableViewController.nodeChanged(updatedNode); - } - - @Override - public void filterAddedOrUpdated(Filter filter) { - loadFilters(); - } - - @Override - public void filterRemoved(String name) { - loadFilters(); - } - public void handleSaveAndFilterTabClosed() { - saveAndRestoreService.removeDataChangeListener(this); + saveAndRestoreService.removeWebSocketMessageHandler(this); + searchResultTableViewController.handleTabClosed(); } @Override - public void secureStoreChanged(List validTokens){ + public void secureStoreChanged(List validTokens) { super.secureStoreChanged(validTokens); searchResultTableViewController.secureStoreChanged(validTokens); } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + switch (saveAndRestoreWebSocketMessage.messageType()) { + case FILTER_REMOVED, FILTER_ADDED_OR_UPDATED -> loadFilters(); + } + } } 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 fad2ebe32c..62dd3c3c8f 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,10 +41,12 @@ 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.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; @@ -77,7 +79,8 @@ /** * Controller for the search result table. */ -public class SearchResultTableViewController extends SaveAndRestoreBaseController implements Initializable { +public class SearchResultTableViewController extends SaveAndRestoreBaseController + implements Initializable, WebSocketMessageHandler { @SuppressWarnings("unused") @FXML @@ -287,6 +290,8 @@ protected void updateItem(Node node, boolean empty) { pageSizeTextField.setText(oldValue); } }); + + saveAndRestoreService.addWebSocketMessageHandler(this); } private ImageView getImageView(Node node) { @@ -307,15 +312,6 @@ private ImageView getImageView(Node node) { return null; } - public void nodeChanged(Node updatedNode) { - for (Node node : resultTableView.getItems()) { - if (node.getUniqueId().equals(updatedNode.getUniqueId())) { - node.setTags(updatedNode.getTags()); - resultTableView.refresh(); - } - } - } - public void clearTable() { tableEntries.clear(); hitCountProperty.set(0); @@ -418,4 +414,15 @@ public void loadFilter(String filterId){ ExceptionDetailsErrorDialog.openError(resultTableView, Messages.errorGeneric, MessageFormat.format(Messages.failedGetSpecificFilter, filterId), e); } } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + switch (saveAndRestoreWebSocketMessage.messageType()){ + case NODE_UPDATED, NODE_REMOVED, NODE_ADDED -> search(); + } + } + + public void handleTabClosed(){ + saveAndRestoreService.removeWebSocketMessageHandler(this); + } } 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 ef5c01ecbd..367ef404bd 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 @@ -76,7 +76,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Stack; import java.util.function.Consumer; @@ -432,8 +431,7 @@ public boolean handleCompositeSnapshotTabClosed() { alert.setContentText(Messages.closeCompositeSnapshotWarning); Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); - } - else{ + } else { saveAndRestoreService.removeWebSocketMessageHandler(this); return true; } @@ -546,7 +544,7 @@ private void removeListeners() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { Node node = (Node) saveAndRestoreWebSocketMessage.payload(); if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { 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 866279b67f..b711d130f5 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 @@ -20,7 +20,6 @@ package org.phoebus.applications.saveandrestore.ui.snapshot; import javafx.application.Platform; -import javafx.beans.property.SimpleStringProperty; import javafx.fxml.FXMLLoader; import javafx.scene.image.ImageView; import org.phoebus.applications.saveandrestore.Messages; @@ -92,8 +91,7 @@ public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { setOnCloseRequest(event -> { if (!((CompositeSnapshotController) controller).handleCompositeSnapshotTabClosed()) { event.consume(); - } - else { + } else { SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); } }); @@ -128,7 +126,7 @@ public void addToCompositeSnapshot(List snapshotNodes) { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_REMOVED)) { String nodeId = (String) saveAndRestoreWebSocketMessage.payload(); if (getId() != null && nodeId.equals(getId())) { 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 e678eb0c2c..447f785043 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 @@ -489,7 +489,7 @@ public void secureStoreChanged(List validTokens) { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_UPDATED)) { Node node = (Node) saveAndRestoreWebSocketMessage.payload(); if (tabIdProperty.get() != null && node.getUniqueId().equals(tabIdProperty.get())) { 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 deb2540590..951b86c6cb 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 @@ -155,7 +155,7 @@ public Node getConfigNode() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { if (saveAndRestoreWebSocketMessage.messageType().equals(MessageType.NODE_REMOVED)) { String nodeId = (String) saveAndRestoreWebSocketMessage.payload(); if (getId() != null && nodeId.equals(getId())) { From bc0a73d4ad2186fefde89166a7b7efee848a19ab Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 6 May 2025 10:47:53 +0200 Subject: [PATCH 34/43] Moved web socket client related code out of SaveAndRestoreService --- .../saveandrestore/ui/DataChangeListener.java | 23 ----- .../ui/SaveAndRestoreBaseController.java | 8 ++ .../ui/SaveAndRestoreController.java | 59 ++++++----- .../ui/SaveAndRestoreService.java | 57 +---------- .../ui/WebSocketClientService.java | 99 +++++++++++++++++++ .../ConfigurationController.java | 7 +- .../ui/configuration/ConfigurationTab.java | 5 +- .../search/SearchAndFilterViewController.java | 7 +- .../SearchResultTableViewController.java | 6 +- .../snapshot/CompositeSnapshotController.java | 7 +- .../ui/snapshot/CompositeSnapshotTab.java | 5 +- .../ui/snapshot/SnapshotController.java | 7 +- .../ui/snapshot/SnapshotTab.java | 5 +- .../core/websocket/WebSocketClient.java | 64 ++++++++---- 14 files changed, 213 insertions(+), 146 deletions(-) delete mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketClientService.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java deleted file mode 100644 index 262d6baf29..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/DataChangeListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.applications.saveandrestore.ui; - -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.search.Filter; - -public interface DataChangeListener { - - default void nodeAddedOrRemoved(String parentNodeId){ - } - - default void nodeChanged(Node node){ - } - - default void filterAddedOrUpdated(Filter filter){ - } - - default void filterRemoved(String filterName){ - } -} 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 428ee52127..e8cf41ab61 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 @@ -20,6 +20,7 @@ package org.phoebus.applications.saveandrestore.ui; import javafx.beans.property.SimpleStringProperty; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.security.store.SecureStore; import org.phoebus.security.tokens.AuthenticationScope; import org.phoebus.security.tokens.ScopedAuthenticationToken; @@ -63,4 +64,11 @@ public void secureStoreChanged(List validTokens) { public SimpleStringProperty getUserIdentity() { return userIdentity; } + + /** + * Default no-op implementation of a handler for {@link SaveAndRestoreWebSocketMessage}s. + * @param webSocketMessage See {@link SaveAndRestoreWebSocketMessage} + */ + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage webSocketMessage){ + } } 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 3f8782071b..e1b83b2487 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 @@ -168,24 +168,17 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @FXML private VBox treeViewPane; - protected SaveAndRestoreService saveAndRestoreService; + private SaveAndRestoreService saveAndRestoreService; + private WebSocketClientService webSocketClientService; private final ObjectMapper objectMapper = new ObjectMapper(); - protected MultipleSelectionModel> browserSelectionModel; - private static final String TREE_STATE = "tree_state"; - private static final String FILTER_NAME = "filter_name"; - protected static final Logger LOG = Logger.getLogger(SaveAndRestoreController.class.getName()); - protected Comparator> treeNodeComparator; - protected SimpleBooleanProperty disabledUi = new SimpleBooleanProperty(false); - private final SimpleBooleanProperty filterEnabledProperty = new SimpleBooleanProperty(false); - private static final Logger logger = Logger.getLogger(SaveAndRestoreController.class.getName()); @SuppressWarnings("unused") @@ -263,7 +256,6 @@ public void initialize(URL url, ResourceBundle resourceBundle) { treeNodeComparator = Comparator.comparing(TreeItem::getValue); saveAndRestoreService = SaveAndRestoreService.getInstance(); - saveAndRestoreService.openWebSocket(); treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); treeView.getStylesheets().add(getClass().getResource("/save-and-restore-style.css").toExternalForm()); @@ -291,9 +283,6 @@ public void initialize(URL url, ResourceBundle resourceBundle) { }); treeView.setShowRoot(true); - - saveAndRestoreService.addWebSocketMessageHandler(this); - treeView.setCellFactory(p -> new BrowserTreeCell(this)); treeViewPane.disableProperty().bind(disabledUi); progressIndicator.visibleProperty().bind(disabledUi); @@ -369,6 +358,12 @@ public Filter fromString(String s) { loadTreeData(); webSocketTrackerLabel.textProperty().bind(webSocketTrackerText); + + webSocketClientService = WebSocketClientService.getInstance(); + webSocketClientService.addWebSocketMessageHandler(this); + webSocketClientService.setConnectCallback(() -> Platform.runLater(() -> webSocketTrackerText.setValue("Web Socket Connected"))); + webSocketClientService.setDisconnectCallback(() -> Platform.runLater(() -> webSocketTrackerText.setValue("Web Socket Disconnected"))); + webSocketClientService.connect(); } /** @@ -540,6 +535,7 @@ public SearchAndFilterTab openSearchWindow() { return searchAndFilterTab; } } + /** * Creates a new folder {@link Node}. */ @@ -791,7 +787,7 @@ private void nodeChanged(Node node) { nodeSubjectToUpdate.setValue(node); // For updated and expanded folder nodes, refresh with respect to child nodes as // a move/copy operation may add/remove nodes. - if(nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.FOLDER) && nodeSubjectToUpdate.isExpanded()){ + if (nodeSubjectToUpdate.getValue().getNodeType().equals(NodeType.FOLDER) && nodeSubjectToUpdate.isExpanded()) { expandTreeNode(nodeSubjectToUpdate); } } @@ -802,7 +798,7 @@ private void nodeChanged(Node node) { * * @param nodeId Unique id of the added {@link Node} */ - private void nodeAdded(String nodeId){ + private void nodeAdded(String nodeId) { Node newNode = saveAndRestoreService.getNode(nodeId); try { Node parentNode = saveAndRestoreService.getParentNode(nodeId); @@ -819,11 +815,12 @@ private void nodeAdded(String nodeId){ /** * Handles callback in order to update the tree view when a {@link Node} has been deleted. * The purpose is to update the {@link TreeView} accordingly to reflect the change. + * * @param nodeId Unique id of the deleted {@link Node} */ - private void nodeRemoved(String nodeId){ + private void nodeRemoved(String nodeId) { TreeItem treeItemToRemove = recursiveSearch(nodeId, treeView.getRoot()); - if(treeItemToRemove != null){ + if (treeItemToRemove != null) { treeItemToRemove.getParent().getChildren().remove(treeItemToRemove); } } @@ -932,8 +929,9 @@ public void saveLocalState() { public void handleTabClosed() { saveLocalState(); - saveAndRestoreService.closeWebSocket(); - saveAndRestoreService.removeWebSocketMessageHandler(this); + webSocketClientService.closeWebSocket(); + webSocketClientService.removeWebSocketMessageHandler(this); + webSocketClientService.close(); } /** @@ -1298,10 +1296,10 @@ public boolean mayPaste() { if (clipBoardContent == null || browserSelectionModel.getSelectedItems().size() != 1) { return false; } - if(selectedItemsProperty.size() != 1 || - selectedItemsProperty.get(0).getUniqueId().equals(Node.ROOT_FOLDER_UNIQUE_ID) || - (!selectedItemsProperty.get(0).getNodeType().equals(NodeType.FOLDER) && !selectedItemsProperty.get(0).getNodeType().equals(NodeType.CONFIGURATION))){ - return false; + if (selectedItemsProperty.size() != 1 || + selectedItemsProperty.get(0).getUniqueId().equals(Node.ROOT_FOLDER_UNIQUE_ID) || + (!selectedItemsProperty.get(0).getNodeType().equals(NodeType.FOLDER) && !selectedItemsProperty.get(0).getNodeType().equals(NodeType.CONFIGURATION))) { + return false; } // Check is made if target node is of supported type for the clipboard content. List selectedNodes = (List) clipBoardContent; @@ -1491,14 +1489,13 @@ 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 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()); } } - } 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 6b6773b5f7..76b9422110 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 @@ -19,13 +19,10 @@ 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.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; @@ -43,20 +40,15 @@ import org.phoebus.applications.saveandrestore.model.UserData; import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchResult; -import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; -import org.phoebus.applications.saveandrestore.model.websocket.WebMessageDeserializer; import org.phoebus.core.vtypes.VDisconnectedData; -import org.phoebus.core.websocket.WebSocketClient; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.saveandrestore.util.VNoData; import org.phoebus.util.time.TimestampFormats; import javax.ws.rs.core.MultivaluedMap; -import java.net.URI; 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; @@ -71,7 +63,6 @@ public class SaveAndRestoreService { private final ExecutorService executor; - private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); private static final Logger LOG = Logger.getLogger(SaveAndRestoreService.class.getName()); private static SaveAndRestoreService instance; @@ -79,25 +70,13 @@ public class SaveAndRestoreService { private final SaveAndRestoreClient saveAndRestoreClient; private final ObjectMapper objectMapper; - private final WebSocketClient webSocketClient; - private SaveAndRestoreService() { saveAndRestoreClient = new SaveAndRestoreClientImpl(); - 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::handleWebSocketConnect, this::handleWebSocketDisconnect, this::handleWebSocketMessage); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); 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 SaveAndRestoreService getInstance() { @@ -173,6 +152,7 @@ public List getAllTags() throws Exception { Future> future = executor.submit(saveAndRestoreClient::getAllTags); return future.get(); } + /** * Moves the sourceNode to the targetNode. The target {@link Node} may not contain * any {@link Node} of same name and type as the source {@link Node}. @@ -447,39 +427,4 @@ private VType readFromArchiver(String pvName, Instant time) { return VDisconnectedData.INSTANCE; } } - - private void handleWebSocketDisconnect(){ - LOG.log(Level.INFO, "Web socket disonnected"); - } - - private void handleWebSocketConnect(){ - LOG.log(Level.INFO, "Web socket connected"); - } - - 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 closeWebSocket(){ - webSocketClient.close("Application shutdown"); - } - - public void openWebSocket(){ - webSocketClient.connect(); - } - - public void addWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler){ - webSocketMessageHandlers.add(webSocketMessageHandler); - } - - public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler){ - webSocketMessageHandlers.remove(webSocketMessageHandler); - } } 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 new file mode 100644 index 0000000000..1d3dfb6523 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/WebSocketClientService.java @@ -0,0 +1,99 @@ +/* + * 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; +import java.util.logging.Logger; + +public class WebSocketClientService { + + private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); + private static final Logger LOG = Logger.getLogger(WebSocketClientService.class.getName()); + + 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(){ + webSocketClient.close("Application shutdown"); + } + + public void openWebSocket(){ + webSocketClient.connect(); + } + + 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); + } + + public void close(){ + webSocketClient.close("Application shutdown"); + } +} 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 816ed17835..230db14cd4 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 @@ -62,6 +62,7 @@ import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; +import org.phoebus.applications.saveandrestore.ui.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.core.types.ProcessVariable; import org.phoebus.framework.jobs.JobManager; @@ -163,6 +164,7 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private Pane addPVsPane; private SaveAndRestoreService saveAndRestoreService; + private WebSocketClientService webSocketClientService; private final ObservableList configurationEntries = FXCollections.observableArrayList(); @@ -196,6 +198,7 @@ public ConfigurationController(ConfigurationTab configurationTab) { public void initialize() { saveAndRestoreService = SaveAndRestoreService.getInstance(); + webSocketClientService = WebSocketClientService.getInstance(); pvTable.editableProperty().bind(userIdentity.isNull().not()); pvTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); @@ -368,7 +371,7 @@ public void commitEdit(Double value) { addPVsPane.disableProperty().bind(userIdentity.isNull()); - saveAndRestoreService.addWebSocketMessageHandler(this); + webSocketClientService.addWebSocketMessageHandler(this); } @FXML @@ -531,7 +534,7 @@ public boolean handleConfigurationTabClosed() { Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); } else { - saveAndRestoreService.removeWebSocketMessageHandler(this); + webSocketClientService.removeWebSocketMessageHandler(this); return true; } } 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 43c83993ac..d6de7f04d4 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 @@ -28,6 +28,7 @@ import org.phoebus.applications.saveandrestore.ui.ImageRepository; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; +import org.phoebus.applications.saveandrestore.ui.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; @@ -73,11 +74,11 @@ private void configure() { event.consume(); } else{ - SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); + WebSocketClientService.getInstance().removeWebSocketMessageHandler(this); } }); - SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); + WebSocketClientService.getInstance().addWebSocketMessageHandler(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 c7cc5eb74d..7246e36276 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 @@ -55,6 +55,7 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.security.tokens.ScopedAuthenticationToken; @@ -182,6 +183,7 @@ public class SearchAndFilterViewController extends SaveAndRestoreBaseController private final SimpleStringProperty filterNameProperty = new SimpleStringProperty(); private final SaveAndRestoreService saveAndRestoreService; + private final WebSocketClientService webSocketClientService; private final SimpleStringProperty query = new SimpleStringProperty(); @@ -221,6 +223,7 @@ public class SearchAndFilterViewController extends SaveAndRestoreBaseController public SearchAndFilterViewController(SaveAndRestoreController saveAndRestoreController) { this.saveAndRestoreController = saveAndRestoreController; this.saveAndRestoreService = SaveAndRestoreService.getInstance(); + this.webSocketClientService = WebSocketClientService.getInstance(); } @FXML @@ -383,7 +386,7 @@ public void initialize(URL url, ResourceBundle resourceBundle) { loadFilters(); - saveAndRestoreService.addWebSocketMessageHandler(this); + webSocketClientService.addWebSocketMessageHandler(this); progressIndicator.visibleProperty().bind(disableUi); disableUi.addListener((observable, oldValue, newValue) -> mainUi.setDisable(newValue)); @@ -636,7 +639,7 @@ private void updatedQueryEditor() { } public void handleSaveAndFilterTabClosed() { - saveAndRestoreService.removeWebSocketMessageHandler(this); + webSocketClientService.removeWebSocketMessageHandler(this); searchResultTableViewController.handleTabClosed(); } 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 62dd3c3c8f..0115fdf64c 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 @@ -46,6 +46,7 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.contextmenu.LoginMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.RestoreFromClientMenuItem; @@ -141,6 +142,7 @@ public class SearchResultTableViewController extends SaveAndRestoreBaseControlle private static final Logger LOGGER = Logger.getLogger(SearchResultTableViewController.class.getName()); private SaveAndRestoreService saveAndRestoreService; + private WebSocketClientService webSocketClientService; @Override public void initialize(URL url, ResourceBundle resourceBundle) { @@ -291,7 +293,7 @@ protected void updateItem(Node node, boolean empty) { } }); - saveAndRestoreService.addWebSocketMessageHandler(this); + webSocketClientService.addWebSocketMessageHandler(this); } private ImageView getImageView(Node node) { @@ -423,6 +425,6 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRest } public void handleTabClosed(){ - saveAndRestoreService.removeWebSocketMessageHandler(this); + webSocketClientService.removeWebSocketMessageHandler(this); } } 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 367ef404bd..1fe68fbefe 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 @@ -64,6 +64,7 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; @@ -128,6 +129,7 @@ public class CompositeSnapshotController extends SaveAndRestoreBaseController im private Label createdByField; private SaveAndRestoreService saveAndRestoreService; + private WebSocketClientService webSocketClientService; private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(); @@ -174,6 +176,7 @@ public void initialize() { snapshotTable.getStylesheets().add(CompareSnapshotsController.class.getResource("/save-and-restore-style.css").toExternalForm()); saveAndRestoreService = SaveAndRestoreService.getInstance(); + webSocketClientService = WebSocketClientService.getInstance(); snapshotTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); snapshotTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> selectionEmpty.set(nv == null)); @@ -342,7 +345,7 @@ public void updateItem(Node item, boolean empty) { } }); - saveAndRestoreService.addWebSocketMessageHandler(this); + webSocketClientService.addWebSocketMessageHandler(this); } @FXML @@ -432,7 +435,7 @@ public boolean handleCompositeSnapshotTabClosed() { Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); } else { - saveAndRestoreService.removeWebSocketMessageHandler(this); + webSocketClientService.removeWebSocketMessageHandler(this); return true; } } 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 b711d130f5..17d563fcfa 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 @@ -30,6 +30,7 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -92,11 +93,11 @@ public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { if (!((CompositeSnapshotController) controller).handleCompositeSnapshotTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); + WebSocketClientService.getInstance().removeWebSocketMessageHandler(this); } }); - SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); + WebSocketClientService.getInstance().addWebSocketMessageHandler(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 447f785043..dfaa9082b2 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 @@ -41,6 +41,7 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.saveandrestore.util.VNoData; @@ -85,6 +86,7 @@ public class SnapshotController extends SaveAndRestoreBaseController implements private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); private final SaveAndRestoreService saveAndRestoreService; + private final WebSocketClientService webSocketClientService; @FXML protected VBox progressIndicator; @@ -100,6 +102,7 @@ public SnapshotController(SnapshotTab snapshotTab) { snapshotTab.setGraphic(imageView); saveAndRestoreService = SaveAndRestoreService.getInstance(); + webSocketClientService = WebSocketClientService.getInstance(); } /** @@ -142,7 +145,7 @@ public void initialize() { } }); - saveAndRestoreService.addWebSocketMessageHandler(this); + webSocketClientService.addWebSocketMessageHandler(this); } /** @@ -293,7 +296,7 @@ public boolean handleSnapshotTabClosed() { Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); } else { - saveAndRestoreService.removeWebSocketMessageHandler(this); + webSocketClientService.removeWebSocketMessageHandler(this); dispose(); return true; } 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 951b86c6cb..57b1b789f2 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 @@ -32,6 +32,7 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -100,7 +101,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save if (controller != null && !((SnapshotController) controller).handleSnapshotTabClosed()) { event.consume(); } else { - SaveAndRestoreService.getInstance().removeWebSocketMessageHandler(this); + WebSocketClientService.getInstance().removeWebSocketMessageHandler(this); } }); @@ -116,7 +117,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save }); getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem); - SaveAndRestoreService.getInstance().addWebSocketMessageHandler(this); + WebSocketClientService.getInstance().addWebSocketMessageHandler(this); } /** 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 index da3c2ceafa..86f122358a 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -4,11 +4,15 @@ package org.phoebus.core.websocket; +import java.io.IOException; +import java.net.ConnectException; 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; @@ -25,37 +29,44 @@ public class WebSocketClient implements WebSocket.Listener { private Runnable disconnectCallback; private URI uri; private Consumer onTextCallback; - private AtomicBoolean reconnectAborted = new AtomicBoolean(false); + private AtomicBoolean attemptConnect = new AtomicBoolean(true); /** - * * @param uri The URI of the web socket peer. - * @param disconnectCallback An optional {@link Runnable} called if the web socket is closed, e.g. if - * peer closes it or due to network issues. */ - public WebSocketClient(URI uri, Runnable connectCallback, Runnable disconnectCallback, Consumer onTextCallback) { + public WebSocketClient(URI uri, Consumer onTextCallback) { this.uri = uri; - this.connectCallback = connectCallback; - this.disconnectCallback = disconnectCallback; this.onTextCallback = onTextCallback; } - public void connect(){ - try { - webSocket = HttpClient.newBuilder() - .build() - .newWebSocketBuilder() - .buildAsync(uri, this) - .join(); - } catch (Exception e) { - logger.log(Level.INFO, "Failed to connect to " + uri); - } + public void connect() { + new Thread(() -> { + while(attemptConnect.get()){ + logger.log(Level.INFO, "Attempting web socket connection to " + uri); + try { + webSocket = HttpClient.newBuilder() + .build() + .newWebSocketBuilder() + .buildAsync(uri, this) + .join(); + break; + } + catch (Exception e) { + logger.log(Level.INFO, "Failed to connect to " + uri + " " + (e != null ? e.getMessage() : "")); + } + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Interrupted while sleeping"); + } + } + }).start(); } @Override public void onOpen(WebSocket webSocket) { WebSocket.Listener.super.onOpen(webSocket); - if(connectCallback != null){ + if (connectCallback != null) { connectCallback.run(); } logger.log(Level.INFO, "Connected to " + uri); @@ -63,6 +74,7 @@ public void onOpen(WebSocket webSocket) { /** * 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. */ @@ -83,6 +95,7 @@ public CompletionStage onClose(WebSocket webSocket, if (disconnectCallback != null) { disconnectCallback.run(); } + connect(); return null; } @@ -113,11 +126,22 @@ public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { webSocket.request(1); - onTextCallback.accept(data); + if(onTextCallback != null) { + onTextCallback.accept(data); + } return WebSocket.Listener.super.onText(webSocket, data, last); } - public void close(String reason){ + public void close(String reason) { + attemptConnect.set(false); webSocket.sendClose(1000, reason); } + + public void setConnectCallback(Runnable connectCallback){ + this.connectCallback = connectCallback; + } + + public void setDisconnectCallback(Runnable disconnectCallback){ + this.disconnectCallback = disconnectCallback; + } } From 6219fba3589af63f7fd979a92281f25bb01b99ea Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 6 May 2025 14:35:26 +0200 Subject: [PATCH 35/43] Refacotring to improve maintainability --- .../SaveAndRestoreInstance.java | 5 ++- .../client/SaveAndRestoreClientImpl.java | 10 +++--- .../ui/SaveAndRestoreBaseController.java | 11 ++++++- .../ui/SaveAndRestoreController.java | 11 ++----- .../saveandrestore/ui/SaveAndRestoreTab.java | 21 +++++++++++- .../ui/WebSocketClientService.java | 31 +++++++---------- .../ConfigurationController.java | 11 ++----- .../ui/configuration/ConfigurationTab.java | 18 ---------- .../search/SearchAndFilterViewController.java | 6 ---- .../SearchResultTableViewController.java | 20 +++++------ .../snapshot/CompositeSnapshotController.java | 15 +++------ .../ui/snapshot/CompositeSnapshotTab.java | 14 -------- .../ui/snapshot/SnapshotController.java | 9 +---- .../ui/snapshot/SnapshotTab.java | 11 +------ .../core/websocket/WebSocketClient.java | 33 +++++++++---------- 15 files changed, 82 insertions(+), 144 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java index fe6a0f64b5..e763e3e75b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java @@ -20,7 +20,6 @@ import javafx.fxml.FXMLLoader; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; -import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.framework.nls.NLS; import org.phoebus.framework.persistence.Memento; import org.phoebus.framework.spi.AppDescriptor; @@ -83,11 +82,11 @@ public void openResource(URI uri) { saveAndRestoreController.openResource(uri); } - public void secureStoreChanged(List validTokens){ + public void secureStoreChanged(List validTokens) { saveAndRestoreController.secureStoreChanged(validTokens); } - public void raise(){ + public void raise() { dockItem.select(); } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java index 1df06ff849..786270532e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java @@ -36,11 +36,9 @@ import java.net.CookieManager; import java.net.CookiePolicy; import java.net.URI; -import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; import java.util.List; @@ -588,10 +586,10 @@ public UserData authenticate(String userName, String password) { String stringBuilder = Preferences.jmasarServiceUrl + "/login"; HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(stringBuilder)) - .header("Content-Type", CONTENT_TYPE_JSON) - .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(new LoginCredentials(userName, password)))) - .build(); + .uri(URI.create(stringBuilder)) + .header("Content-Type", CONTENT_TYPE_JSON) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(new LoginCredentials(userName, password)))) + .build(); HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); return OBJECT_MAPPER.readValue(response.body(), UserData.class); } catch (Exception e) { 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 e8cf41ab61..fc30652bcf 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 @@ -33,8 +33,12 @@ public abstract class SaveAndRestoreBaseController { protected final SimpleStringProperty userIdentity = new SimpleStringProperty(); + protected final WebSocketClientService webSocketClientService; + protected final SaveAndRestoreService saveAndRestoreService; public SaveAndRestoreBaseController() { + this.webSocketClientService = WebSocketClientService.getInstance(); + this.saveAndRestoreService = SaveAndRestoreService.getInstance(); try { SecureStore secureStore = new SecureStore(); ScopedAuthenticationToken token = @@ -69,6 +73,11 @@ public SimpleStringProperty getUserIdentity() { * Default no-op implementation of a handler for {@link SaveAndRestoreWebSocketMessage}s. * @param webSocketMessage See {@link SaveAndRestoreWebSocketMessage} */ - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage webSocketMessage){ + protected void handleWebSocketMessage(SaveAndRestoreWebSocketMessage webSocketMessage){ + } + + + protected boolean handleTabClosed(){ + return true; } } 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 e1b83b2487..2745b3f427 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 @@ -168,9 +168,6 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @FXML private VBox treeViewPane; - private SaveAndRestoreService saveAndRestoreService; - private WebSocketClientService webSocketClientService; - private final ObjectMapper objectMapper = new ObjectMapper(); protected MultipleSelectionModel> browserSelectionModel; private static final String TREE_STATE = "tree_state"; @@ -255,7 +252,6 @@ public void initialize(URL url, ResourceBundle resourceBundle) { // Tree items are first compared on type, then on name (case-insensitive). treeNodeComparator = Comparator.comparing(TreeItem::getValue); - saveAndRestoreService = SaveAndRestoreService.getInstance(); treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); treeView.getStylesheets().add(getClass().getResource("/save-and-restore-style.css").toExternalForm()); @@ -359,7 +355,6 @@ public Filter fromString(String s) { webSocketTrackerLabel.textProperty().bind(webSocketTrackerText); - webSocketClientService = WebSocketClientService.getInstance(); webSocketClientService.addWebSocketMessageHandler(this); webSocketClientService.setConnectCallback(() -> Platform.runLater(() -> webSocketTrackerText.setValue("Web Socket Connected"))); webSocketClientService.setDisconnectCallback(() -> Platform.runLater(() -> webSocketTrackerText.setValue("Web Socket Disconnected"))); @@ -927,11 +922,11 @@ public void saveLocalState() { } } - public void handleTabClosed() { + @Override + public boolean handleTabClosed() { saveLocalState(); webSocketClientService.closeWebSocket(); - webSocketClientService.removeWebSocketMessageHandler(this); - webSocketClientService.close(); + return true; } /** 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 b1d9b06716..195532a589 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,6 +24,7 @@ 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.ui.snapshot.SnapshotTab; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.javafx.ImageCache; @@ -34,9 +35,10 @@ /** * Base class for save-n-restore {@link Tab}s containing common functionality. */ -public abstract class SaveAndRestoreTab extends Tab { +public abstract class SaveAndRestoreTab extends Tab implements WebSocketMessageHandler { protected SaveAndRestoreBaseController controller; + protected WebSocketClientService webSocketClientService; public SaveAndRestoreTab() { ContextMenu contextMenu = new ContextMenu(); @@ -60,6 +62,18 @@ public SaveAndRestoreTab() { contextMenu.getItems().addAll(closeAll, closeOthers); setContextMenu(contextMenu); + + webSocketClientService = WebSocketClientService.getInstance(); + + setOnCloseRequest(event -> { + if (!controller.handleTabClosed()) { + event.consume(); + } else { + webSocketClientService.removeWebSocketMessageHandler(this); + } + }); + + webSocketClientService.addWebSocketMessageHandler(this); } /** @@ -70,4 +84,9 @@ public SaveAndRestoreTab() { public void secureStoreChanged(List validTokens) { controller.secureStoreChanged(validTokens); } + + @Override + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + + } } 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 index 1d3dfb6523..0deddbd10d 100644 --- 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 @@ -19,12 +19,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.logging.Logger; public class WebSocketClientService { private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); - private static final Logger LOG = Logger.getLogger(WebSocketClientService.class.getName()); private static WebSocketClientService instance; private final WebSocketClient webSocketClient; @@ -36,7 +34,7 @@ private WebSocketClientService() { 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); + webSocketClient = new WebSocketClient(webSocketUri, this::handleWebSocketMessage); objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.registerModule(new JavaTimeModule()); @@ -47,34 +45,31 @@ private WebSocketClientService() { objectMapper.registerModule(module); } - public static WebSocketClientService getInstance(){ - if(instance == null){ + public static WebSocketClientService getInstance() { + if (instance == null) { instance = new WebSocketClientService(); } return instance; } - public void connect(){ + public void connect() { webSocketClient.connect(); } - public void closeWebSocket(){ + public void closeWebSocket() { + webSocketMessageHandlers.clear(); webSocketClient.close("Application shutdown"); } - public void openWebSocket(){ - webSocketClient.connect(); - } - - public void addWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler){ + public void addWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler) { webSocketMessageHandlers.add(webSocketMessageHandler); } - public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler){ + public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler) { webSocketMessageHandlers.remove(webSocketMessageHandler); } - private void handleWebSocketMessage(CharSequence charSequence){ + private void handleWebSocketMessage(CharSequence charSequence) { try { SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(charSequence.toString(), SaveAndRestoreWebSocketMessage.class); @@ -85,15 +80,11 @@ private void handleWebSocketMessage(CharSequence charSequence){ } } - public void setConnectCallback(Runnable connectCallback){ + public void setConnectCallback(Runnable connectCallback) { webSocketClient.setConnectCallback(connectCallback); } - public void setDisconnectCallback(Runnable disconnectCallback){ + public void setDisconnectCallback(Runnable disconnectCallback) { webSocketClient.setDisconnectCallback(disconnectCallback); } - - public void close(){ - webSocketClient.close("Application shutdown"); - } } 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 230db14cd4..4197c829d5 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 @@ -61,8 +61,6 @@ import org.phoebus.applications.saveandrestore.model.websocket.MessageType; import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; -import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; -import org.phoebus.applications.saveandrestore.ui.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.core.types.ProcessVariable; import org.phoebus.framework.jobs.JobManager; @@ -163,9 +161,6 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem @SuppressWarnings("unused") private Pane addPVsPane; - private SaveAndRestoreService saveAndRestoreService; - private WebSocketClientService webSocketClientService; - private final ObservableList configurationEntries = FXCollections.observableArrayList(); private final SimpleBooleanProperty selectionEmpty = new SimpleBooleanProperty(false); @@ -197,9 +192,6 @@ public ConfigurationController(ConfigurationTab configurationTab) { @FXML public void initialize() { - saveAndRestoreService = SaveAndRestoreService.getInstance(); - webSocketClientService = WebSocketClientService.getInstance(); - pvTable.editableProperty().bind(userIdentity.isNull().not()); pvTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); pvTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> selectionEmpty.set(nv == null)); @@ -526,7 +518,8 @@ public synchronized void loadConfiguration(final Node node) { * @return true if content is not dirty or user chooses to close anyway, * otherwise false. */ - public boolean handleConfigurationTabClosed() { + @Override + public boolean handleTabClosed() { if (dirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle(Messages.closeTabPrompt); 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 d6de7f04d4..dcbb9cb9ca 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 @@ -26,9 +26,7 @@ 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.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; -import org.phoebus.applications.saveandrestore.ui.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; @@ -39,10 +37,6 @@ public class ConfigurationTab extends SaveAndRestoreTab implements WebSocketMessageHandler { public ConfigurationTab() { - configure(); - } - - private void configure() { try { FXMLLoader loader = new FXMLLoader(); ResourceBundle resourceBundle = NLS.getMessages(Messages.class); @@ -66,19 +60,7 @@ private void configure() { } catch (Exception e) { Logger.getLogger(ConfigurationTab.class.getName()) .log(Level.SEVERE, "Failed to load fxml", e); - return; } - - setOnCloseRequest(event -> { - if (!((ConfigurationController) controller).handleConfigurationTabClosed()) { - event.consume(); - } - else{ - WebSocketClientService.getInstance().removeWebSocketMessageHandler(this); - } - }); - - WebSocketClientService.getInstance().addWebSocketMessageHandler(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 7246e36276..9bef48e88c 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 @@ -55,7 +55,6 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.security.tokens.ScopedAuthenticationToken; @@ -182,9 +181,6 @@ public class SearchAndFilterViewController extends SaveAndRestoreBaseController private final SimpleStringProperty filterNameProperty = new SimpleStringProperty(); - private final SaveAndRestoreService saveAndRestoreService; - private final WebSocketClientService webSocketClientService; - private final SimpleStringProperty query = new SimpleStringProperty(); private final SimpleStringProperty pvNamesProperty = new SimpleStringProperty(); @@ -222,8 +218,6 @@ public class SearchAndFilterViewController extends SaveAndRestoreBaseController public SearchAndFilterViewController(SaveAndRestoreController saveAndRestoreController) { this.saveAndRestoreController = saveAndRestoreController; - this.saveAndRestoreService = SaveAndRestoreService.getInstance(); - this.webSocketClientService = WebSocketClientService.getInstance(); } @FXML 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 0115fdf64c..d800ee2e83 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 @@ -46,7 +46,6 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.applications.saveandrestore.ui.contextmenu.LoginMenuItem; import org.phoebus.applications.saveandrestore.ui.contextmenu.RestoreFromClientMenuItem; @@ -141,14 +140,10 @@ public class SearchResultTableViewController extends SaveAndRestoreBaseControlle private String queryString; private static final Logger LOGGER = Logger.getLogger(SearchResultTableViewController.class.getName()); - private SaveAndRestoreService saveAndRestoreService; - private WebSocketClientService webSocketClientService; @Override public void initialize(URL url, ResourceBundle resourceBundle) { - saveAndRestoreService = SaveAndRestoreService.getInstance(); - tableUi.disableProperty().bind(disableUi); progressIndicator.visibleProperty().bind(disableUi); @@ -375,7 +370,7 @@ void uniqueIdSearch(final String uniqueIdString) { tableEntries.setAll(List.of(uniqueIdNode)); hitCountProperty.set(1); }); - /* Clear the results table if no record returned */ + /* Clear the results table if no record returned */ } else { Platform.runLater(tableEntries::clear); hitCountProperty.set(0); @@ -396,16 +391,16 @@ void uniqueIdSearch(final String uniqueIdString) { /** * Retrieves a filter from the service and loads then performs a search for matching {@link Node}s. If * the filter does not exist, or if retrieval fails, an error dialog is shown. + * * @param filterId Unique id of an existing {@link Filter}. */ - public void loadFilter(String filterId){ + public void loadFilter(String filterId) { try { List filters = saveAndRestoreService.getAllFilters(); Optional filter = filters.stream().filter(f -> f.getName().equals(filterId)).findFirst(); - if(filter.isPresent()){ + if (filter.isPresent()) { search(filter.get().getQueryString()); - } - else{ + } else { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle(Messages.errorGeneric); alert.setHeaderText(MessageFormat.format(Messages.failedGetSpecificFilter, filterId)); @@ -419,12 +414,13 @@ public void loadFilter(String filterId){ @Override public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { - switch (saveAndRestoreWebSocketMessage.messageType()){ + switch (saveAndRestoreWebSocketMessage.messageType()) { case NODE_UPDATED, NODE_REMOVED, NODE_ADDED -> search(); } } - public void handleTabClosed(){ + public boolean handleTabClosed() { webSocketClientService.removeWebSocketMessageHandler(this); + 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 1fe68fbefe..9eb5101bcf 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 @@ -63,8 +63,6 @@ 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.SaveAndRestoreService; -import org.phoebus.applications.saveandrestore.ui.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; @@ -128,9 +126,6 @@ public class CompositeSnapshotController extends SaveAndRestoreBaseController im @FXML private Label createdByField; - private SaveAndRestoreService saveAndRestoreService; - private WebSocketClientService webSocketClientService; - private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(); private final ObservableList snapshotEntries = FXCollections.observableArrayList(); @@ -175,9 +170,6 @@ public void initialize() { snapshotTable.getStylesheets().add(CompareSnapshotsController.class.getResource("/save-and-restore-style.css").toExternalForm()); - saveAndRestoreService = SaveAndRestoreService.getInstance(); - webSocketClientService = WebSocketClientService.getInstance(); - snapshotTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); snapshotTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> selectionEmpty.set(nv == null)); @@ -238,7 +230,7 @@ public void updateItem(Node item, boolean empty) { snapshotTable.setContextMenu(contextMenu); snapshotTable.setOnContextMenuRequested(event -> { - if (snapshotTable.getSelectionModel().getSelectedItems().size() == 0) { + if (snapshotTable.getSelectionModel().getSelectedItems().isEmpty()) { contextMenu.hide(); event.consume(); } @@ -427,7 +419,8 @@ public void loadCompositeSnapshot(final Node node, final List snapshotNode }); } - public boolean handleCompositeSnapshotTabClosed() { + @Override + public boolean handleTabClosed() { if (dirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle(Messages.closeTabPrompt); @@ -482,7 +475,7 @@ public void addToCompositeSnapshot(List sourceNodes) { JobManager.schedule("Check snapshot PV duplicates", monitor -> { disabledUi.set(true); List allSnapshotIds = snapshotEntries.stream().map(Node::getUniqueId).collect(Collectors.toList()); - allSnapshotIds.addAll(sourceNodes.stream().map(Node::getUniqueId).collect(Collectors.toList())); + allSnapshotIds.addAll(sourceNodes.stream().map(Node::getUniqueId).toList()); List duplicates = null; try { duplicates = saveAndRestoreService.checkCompositeSnapshotConsistency(allSnapshotIds); 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 17d563fcfa..beb39383e6 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 @@ -28,9 +28,7 @@ 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.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; -import org.phoebus.applications.saveandrestore.ui.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -52,10 +50,8 @@ */ public class CompositeSnapshotTab extends SaveAndRestoreTab implements WebSocketMessageHandler { - private final SaveAndRestoreController saveAndRestoreController; public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { - this.saveAndRestoreController = saveAndRestoreController; ResourceBundle resourceBundle = NLS.getMessages(Messages.class); FXMLLoader loader = new FXMLLoader(); @@ -88,16 +84,6 @@ public CompositeSnapshotTab(SaveAndRestoreController saveAndRestoreController) { setContent(rootNode); setGraphic(new ImageView(ImageRepository.COMPOSITE_SNAPSHOT)); - - setOnCloseRequest(event -> { - if (!((CompositeSnapshotController) controller).handleCompositeSnapshotTabClosed()) { - event.consume(); - } else { - WebSocketClientService.getInstance().removeWebSocketMessageHandler(this); - } - }); - - WebSocketClientService.getInstance().addWebSocketMessageHandler(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 dfaa9082b2..93283f9dd1 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 @@ -41,7 +41,6 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.jobs.JobManager; import org.phoebus.saveandrestore.util.VNoData; @@ -85,9 +84,6 @@ public class SnapshotController extends SaveAndRestoreBaseController implements private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); - private final SaveAndRestoreService saveAndRestoreService; - private final WebSocketClientService webSocketClientService; - @FXML protected VBox progressIndicator; @@ -100,9 +96,6 @@ public SnapshotController(SnapshotTab snapshotTab) { ImageView imageView = new ImageView(); imageView.imageProperty().bind(tabGraphicImageProperty); snapshotTab.setGraphic(imageView); - - saveAndRestoreService = SaveAndRestoreService.getInstance(); - webSocketClientService = WebSocketClientService.getInstance(); } /** @@ -378,7 +371,7 @@ private void loadSnapshotInternal(Node snapshotNode) { setTabImage(snapshotNode); }); } finally { - disabledUi.set(false); + disabledUi.set(false); } }); } 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 57b1b789f2..c78c820394 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 @@ -32,7 +32,6 @@ 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.WebSocketClientService; import org.phoebus.applications.saveandrestore.ui.WebSocketMessageHandler; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -97,14 +96,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save return; } - setOnCloseRequest(event -> { - if (controller != null && !((SnapshotController) controller).handleSnapshotTabClosed()) { - event.consume(); - } else { - WebSocketClientService.getInstance().removeWebSocketMessageHandler(this); - } - }); - MenuItem compareSnapshotToArchiverDataMenuItem = new MenuItem(Messages.contextMenuCompareSnapshotWithArchiverData, new ImageView(compareSnapshotIcon)); compareSnapshotToArchiverDataMenuItem.setOnAction(ae -> addSnapshotFromArchive()); @@ -117,7 +108,7 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save }); getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem); - WebSocketClientService.getInstance().addWebSocketMessageHandler(this); + //WebSocketClientService.getInstance().addWebSocketMessageHandler(this); } /** 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 index 86f122358a..3098e98762 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -4,15 +4,11 @@ package org.phoebus.core.websocket; -import java.io.IOException; -import java.net.ConnectException; 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; @@ -27,9 +23,9 @@ public class WebSocketClient implements WebSocket.Listener { private final Logger logger = Logger.getLogger(WebSocketClient.class.getName()); private Runnable connectCallback; private Runnable disconnectCallback; - private URI uri; - private Consumer onTextCallback; - private AtomicBoolean attemptConnect = new AtomicBoolean(true); + private final URI uri; + private final Consumer onTextCallback; + private final AtomicBoolean attemptConnect = new AtomicBoolean(true); /** * @param uri The URI of the web socket peer. @@ -40,8 +36,13 @@ public WebSocketClient(URI uri, Consumer onTextCallback) { } public void connect() { + attemptConnect.set(true); + reconnect(); + } + + private void reconnect() { new Thread(() -> { - while(attemptConnect.get()){ + while (attemptConnect.get()) { logger.log(Level.INFO, "Attempting web socket connection to " + uri); try { webSocket = HttpClient.newBuilder() @@ -50,9 +51,8 @@ public void connect() { .buildAsync(uri, this) .join(); break; - } - catch (Exception e) { - logger.log(Level.INFO, "Failed to connect to " + uri + " " + (e != null ? e.getMessage() : "")); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to connect to " + uri + " " + (e.getMessage() != null ? e.getMessage() : "")); } try { Thread.sleep(10000); @@ -95,7 +95,7 @@ public CompletionStage onClose(WebSocket webSocket, if (disconnectCallback != null) { disconnectCallback.run(); } - connect(); + reconnect(); return null; } @@ -116,8 +116,7 @@ public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { @Override public void onError(WebSocket webSocket, Throwable error) { - logger.log(Level.WARNING, "Got web socket error"); - error.printStackTrace(); + logger.log(Level.WARNING, "Got web socket error", error); WebSocket.Listener.super.onError(webSocket, error); } @@ -126,7 +125,7 @@ public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { webSocket.request(1); - if(onTextCallback != null) { + if (onTextCallback != null) { onTextCallback.accept(data); } return WebSocket.Listener.super.onText(webSocket, data, last); @@ -137,11 +136,11 @@ public void close(String reason) { webSocket.sendClose(1000, reason); } - public void setConnectCallback(Runnable connectCallback){ + public void setConnectCallback(Runnable connectCallback) { this.connectCallback = connectCallback; } - public void setDisconnectCallback(Runnable disconnectCallback){ + public void setDisconnectCallback(Runnable disconnectCallback) { this.disconnectCallback = disconnectCallback; } } From ea1d31675cd689bf1bf3d6ab4463cbd6b59895a7 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 7 May 2025 09:58:48 +0200 Subject: [PATCH 36/43] Web socket messages when nodes are copied --- .../ui/SaveAndRestoreController.java | 70 ++--------- .../persistence/dao/NodeDAO.java | 32 +++-- .../impl/elasticsearch/ElasticsearchDAO.java | 30 +++-- .../web/controllers/StructureController.java | 11 +- .../persistence/dao/impl/DAOTestIT.java | 115 ++---------------- .../controllers/StructureControllerTest.java | 68 +++++++++-- 6 files changed, 124 insertions(+), 202 deletions(-) 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 2745b3f427..545628b99c 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 @@ -186,6 +186,7 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @FXML private VBox errorPane; + @SuppressWarnings("unused") @FXML private Label webSocketTrackerLabel; @@ -207,14 +208,10 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController List menuItems = Arrays.asList( new LoginMenuItem(this, selectedItemsProperty, () -> ApplicationService.createInstance("credentials_management")), - new NewFolderMenuItem(this, selectedItemsProperty, - () -> createNewFolder()), - new NewConfigurationMenuItem(this, selectedItemsProperty, - () -> createNewConfiguration()), - new CreateSnapshotMenuItem(this, selectedItemsProperty, - () -> openConfigurationForSnapshot()), - new NewCompositeSnapshotMenuItem(this, selectedItemsProperty, - () -> createNewCompositeSnapshot()), + new NewFolderMenuItem(this, selectedItemsProperty, this::createNewFolder), + new NewConfigurationMenuItem(this, selectedItemsProperty, this::createNewConfiguration), + new CreateSnapshotMenuItem(this, selectedItemsProperty, this::openConfigurationForSnapshot), + new NewCompositeSnapshotMenuItem(this, selectedItemsProperty, this::createNewCompositeSnapshot), new RestoreFromClientMenuItem(this, selectedItemsProperty, () -> { disabledUi.set(true); @@ -226,8 +223,8 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController RestoreUtil.restore(RestoreMode.SERVICE_RESTORE, saveAndRestoreService, selectedItemsProperty.get(0), () -> disabledUi.set(false)); }), new SeparatorMenuItem(), - new EditCompositeMenuItem(this, selectedItemsProperty, () -> editCompositeSnapshot()), - new RenameFolderMenuItem(this, selectedItemsProperty, () -> renameNode()), + new EditCompositeMenuItem(this, selectedItemsProperty, this::editCompositeSnapshot), + new RenameFolderMenuItem(this, selectedItemsProperty, this::renameNode), copyMenuItem, pasteMenuItem, deleteNodeMenuItem, @@ -236,11 +233,10 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController new TagGoldenMenuItem(this, selectedItemsProperty), tagWithComment, new SeparatorMenuItem(), - new CopyUniqueIdToClipboardMenuItem(this, selectedItemsProperty, - () -> copyUniqueNodeIdToClipboard()), + new CopyUniqueIdToClipboardMenuItem(this, selectedItemsProperty, this::copyUniqueNodeIdToClipboard), new SeparatorMenuItem(), - new ImportFromCSVMenuItem(this, selectedItemsProperty, () -> importFromCSV()), - new ExportToCSVMenuItem(this, selectedItemsProperty, () -> exportToCSV()) + new ImportFromCSVMenuItem(this, selectedItemsProperty, this::importFromCSV), + new ExportToCSVMenuItem(this, selectedItemsProperty, this::exportToCSV) ); private final SimpleStringProperty webSocketTrackerText = new SimpleStringProperty(); @@ -1041,45 +1037,6 @@ protected void moveNodes(List sourceNodes, Node targetNode, TransferMode t }); } - /** - * Updates the tree view such that moved items are shown in the drop target. - * - * @param parentTreeItem The drop target - * @param nodes List of {@link Node}s that were moved. - */ - private void addMovedNodes(TreeItem parentTreeItem, List nodes) { - parentTreeItem.getChildren().addAll(nodes.stream().map(this::createTreeItem).toList()); - parentTreeItem.getChildren().sort(treeNodeComparator); - TreeItem nextItemToExpand = parentTreeItem; - while (nextItemToExpand != null) { - nextItemToExpand.setExpanded(true); - nextItemToExpand = nextItemToExpand.getParent(); - } - - } - - /** - * Updates the tree view such that moved items are removed from source nodes' parent. - * - * @param parentTreeItem The parent of the {@link Node}s before the move. - * @param nodes List of {@link Node}s that were moved. - */ - private void removeMovedNodes(TreeItem parentTreeItem, List nodes) { - List> childItems = parentTreeItem.getChildren(); - List> treeItemsToRemove = new ArrayList<>(); - childItems.forEach(childItem -> { - if (nodes.contains(childItem.getValue())) { - treeItemsToRemove.add(childItem); - } - }); - parentTreeItem.getChildren().removeAll(treeItemsToRemove); - TreeItem nextItemToExpand = parentTreeItem; - while (nextItemToExpand != null) { - nextItemToExpand.setExpanded(true); - nextItemToExpand = nextItemToExpand.getParent(); - } - } - /** * Parses the {@link URI} to determine what to do. Supported actions/behavior: *
    @@ -1316,7 +1273,7 @@ private void pasteFromClipboard() { } List selectedNodeIds = ((List) selectedNodes).stream().map(Node::getUniqueId).collect(Collectors.toList()); - JobManager.schedule("copy nodes", monitor -> { + JobManager.schedule("Copy odes", monitor -> { try { saveAndRestoreService.copyNodes(selectedNodeIds, browserSelectionModel.getSelectedItem().getValue().getUniqueId()); disabledUi.set(false); @@ -1324,12 +1281,7 @@ private void pasteFromClipboard() { disabledUi.set(false); ExceptionDetailsErrorDialog.openError(Messages.errorGeneric, Messages.failedToPasteObjects, e); LOG.log(Level.WARNING, "Failed to paste nodes into target " + browserSelectionModel.getSelectedItem().getValue().getName()); - return; } - Platform.runLater(() -> { - expandTreeNode(browserSelectionModel.getSelectedItem()); - treeView.refresh(); - }); }); } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java index 01f42d29d7..a8dc71c7f6 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java @@ -32,10 +32,7 @@ import org.phoebus.applications.saveandrestore.model.search.SearchResult; import org.springframework.util.MultiValueMap; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * @author georgweiss Created 11 Mar 2019 @@ -60,6 +57,7 @@ public interface NodeDAO { /** * Retrieve the nodes identified by the list of unique node ids + * * @param uniqueNodeIds List of unique node ids * @return List of matching nodes */ @@ -75,8 +73,8 @@ public interface NodeDAO { /** * Checks that each of the node ids passed to this method exist, and that none of them * is the root node. If check passes all nodes are deleted. + * * @param nodeIds List of (existing) node ids. - * @return The collection of unique node id representing the deleted * {@link Node}s. Client may use this to trigger a refresh of the UI. */ void deleteNodes(List nodeIds); @@ -92,7 +90,6 @@ public interface NodeDAO { /** - * * @param uniqueNodeId Valid {@link Node} id. * @return The parent {@link Node} */ @@ -114,9 +111,9 @@ public interface NodeDAO { * @param nodeIds Non-null and non-empty list of unique node ids subject to move * @param targetId Non-null and non-empty unique id of target node * @param userName The (account) name of the user performing the operation. - * @return The target {@link Node} object that is the new parent of the moved source {@link Node} + * @return A list of new {@link Node}s */ - Node copyNodes(List nodeIds, String targetId, String userName); + List copyNodes(List nodeIds, String targetId, String userName); /** @@ -139,8 +136,9 @@ public interface NodeDAO { /** * Saves the {@link org.phoebus.applications.saveandrestore.model.Snapshot} to the persistence layer. + * * @param parentNodeId The unique id of the parent {@link Node} for the new {@link Snapshot}. - * @param snapshot The {@link Snapshot} data. + * @param snapshot The {@link Snapshot} data. * @return The persisted {@link Snapshot} data. */ Snapshot createSnapshot(String parentNodeId, Snapshot snapshot); @@ -201,7 +199,8 @@ public interface NodeDAO { /** * Saves the {@link org.phoebus.applications.saveandrestore.model.Configuration} to the persistence layer. - * @param parentNodeId The unique id of the parent {@link Node} for the new {@link Configuration}. + * + * @param parentNodeId The unique id of the parent {@link Node} for the new {@link Configuration}. * @param configuration The {@link Configuration} data. * @return The persisted {@link Configuration} data. */ @@ -209,6 +208,7 @@ public interface NodeDAO { /** * Retrieves the {@link ConfigurationData} for the specified (unique) id. + * * @param uniqueId Id of the configuration {@link Node} * @return A {@link ConfigurationData} object. */ @@ -226,6 +226,7 @@ public interface NodeDAO { /** * Retrieves the {@link SnapshotData} for the specified (unique) id. + * * @param uniqueId Id of the snapshot {@link Node} * @return A {@link SnapshotData} object. */ @@ -249,7 +250,8 @@ public interface NodeDAO { /** * Saves the {@link org.phoebus.applications.saveandrestore.model.CompositeSnapshot} to the persistence layer. - * @param parentNodeId The unique id of the parent {@link Node} for the new {@link CompositeSnapshot}. + * + * @param parentNodeId The unique id of the parent {@link Node} for the new {@link CompositeSnapshot}. * @param compositeSnapshot The {@link CompositeSnapshot} data. * @return The persisted {@link CompositeSnapshot} data. */ @@ -268,6 +270,7 @@ public interface NodeDAO { /** * Checks for duplicate PV names in the specified list of snapshot or composite snapshot {@link Node}s. + * * @param snapshotIds list of snapshot or composite snapshot {@link Node}s * @return A list if PV names that occur multiple times in the specified snapshot nodes or snapshot nodes * referenced in composite snapshots. If no duplicates are found, an empty list is returned. @@ -288,6 +291,7 @@ public interface NodeDAO { * Aggregates a list of {@link SnapshotItem}s from a composite snapshot node. Note that since a * composite snapshot may reference other composite snapshots, the implementation may need to recursively * locate all referenced single snapshots. + * * @param compositeSnapshotNodeId The if of an existing composite snapshot {@link Node} * @return A list of {@link SnapshotItem}s. */ @@ -297,14 +301,16 @@ public interface NodeDAO { * Checks if all the referenced snapshot {@link Node}s in a {@link CompositeSnapshot} are * of the supported type, i.e. {@link org.phoebus.applications.saveandrestore.model.NodeType#COMPOSITE_SNAPSHOT} * or {@link org.phoebus.applications.saveandrestore.model.NodeType#SNAPSHOT} + * * @param compositeSnapshot An existing {@link CompositeSnapshot}. * @return true if all referenced snapshot {@link Node}s in a {@link CompositeSnapshot} are all - * * of the supported type. + * * of the supported type. */ boolean checkCompositeSnapshotReferencedNodeTypes(CompositeSnapshot compositeSnapshot); /** * Performs a search based on the provided search parameters. + * * @param searchParameters Map of keyword/value pairs defining the search criteria. * @return A {@link SearchResult} object with a potentially empty list of {@link Node}s. */ @@ -312,6 +318,7 @@ public interface NodeDAO { /** * Save a new or updated {@link Filter} + * * @param filter The {@link Filter} to save * @return The saved {@link Filter} */ @@ -324,6 +331,7 @@ public interface NodeDAO { /** * Deletes a {@link Filter} based on its name. + * * @param name Unique name of the {@link Filter} */ void deleteFilter(String name); @@ -335,6 +343,7 @@ public interface NodeDAO { /** * Adds a {@link Tag} to specified list of target {@link Node}s + * * @param tagData See {@link TagData} * @return The list of updated {@link Node}s */ @@ -342,6 +351,7 @@ public interface NodeDAO { /** * Removes a {@link Tag} from specified list of target {@link Node}s + * * @param tagData See {@link TagData} * @return The list of updated {@link Node}s */ diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java index 43a5cbc5ec..083c397d9b 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java @@ -47,11 +47,9 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -142,7 +140,7 @@ public void deleteNode(String nodeId) { * {@inheritDoc} */ @Override - public void deleteNodes(List nodeIds){ + public void deleteNodes(List nodeIds) { List nodesToDelete = new ArrayList<>(); for (String nodeId : nodeIds) { Node nodeToDelete = getNode(nodeId); @@ -153,7 +151,7 @@ public void deleteNodes(List nodeIds){ } nodesToDelete.add(nodeToDelete); } - for(Node node : nodesToDelete){ + for (Node node : nodesToDelete) { deleteNode(node); } } @@ -340,7 +338,7 @@ public Node moveNodes(List nodeIds, String targetId, String userName) { } // Remove source nodes from the list of child nodes in parent node. - parentNode.getChildNodes().removeAll(sourceNodes.stream().map(Node::getUniqueId).collect(Collectors.toList())); + parentNode.getChildNodes().removeAll(sourceNodes.stream().map(Node::getUniqueId).toList()); elasticsearchTreeRepository.save(parentNode); // Update the target node to include the source nodes in its list of child nodes @@ -348,14 +346,14 @@ public Node moveNodes(List nodeIds, String targetId, String userName) { targetNode.setChildNodes(new ArrayList<>()); } - targetNode.getChildNodes().addAll(sourceNodes.stream().map(Node::getUniqueId).collect(Collectors.toList())); + targetNode.getChildNodes().addAll(sourceNodes.stream().map(Node::getUniqueId).toList()); ESTreeNode updatedTargetNode = elasticsearchTreeRepository.save(targetNode); return updatedTargetNode.getNode(); } @Override - public Node copyNodes(List nodeIds, String targetId, String userName) { + public List copyNodes(List nodeIds, String targetId, String userName) { // Copy to root node not allowed, neither is copying of root folder itself if (targetId.equals(ROOT_FOLDER_UNIQUE_ID) || nodeIds.contains(ROOT_FOLDER_UNIQUE_ID)) { throw new IllegalArgumentException("Copy to root node or copy root node not supported"); @@ -428,9 +426,11 @@ public Node copyNodes(List nodeIds, String targetId, String userName) { } } - sourceNodes.forEach(sourceNode -> copyNode(sourceNode, targetNode, userName)); + List newNodes = new ArrayList<>(); - return targetNode; + sourceNodes.forEach(sourceNode -> newNodes.add(copyNode(sourceNode, targetNode, userName))); + + return newNodes; } /** @@ -444,8 +444,9 @@ public Node copyNodes(List nodeIds, String targetId, String userName) { * @param sourceNode The source {@link Node} to be copied (cloned). * @param targetParentNode The parent {@link Node} of the copy. * @param userName Username of the individual performing the action. + * @return The new {@link Node} */ - private void copyNode(Node sourceNode, Node targetParentNode, String userName) { + private Node copyNode(Node sourceNode, Node targetParentNode, String userName) { List targetsChildNodes = getChildNodes(targetParentNode.getUniqueId()); String newNodeName = determineNewNodeName(sourceNode, targetsChildNodes); // First create a clone of the source Node object @@ -470,6 +471,7 @@ private void copyNode(Node sourceNode, Node targetParentNode, String userName) { getCompositeSnapshotData(sourceNode.getUniqueId()); copyCompositeSnapshotData(newSourceNode, compositeSnapshotData); } + return newSourceNode; } protected boolean mayMoveOrCopySnapshot(Node sourceNode, Node targetParentNode) { @@ -639,10 +641,6 @@ private void resolvePath(String nodeId, List pathElements) { resolvePath(parent.getUniqueId(), pathElements); } - /** - * - * @param nodeToDelete - */ private void deleteNode(Node nodeToDelete) { for (Node node : getChildNodes(nodeToDelete.getUniqueId())) { deleteNode(node); @@ -1237,7 +1235,7 @@ public List deleteTag(TagData tagData) { protected String determineNewNodeName(Node sourceNode, List targetParentChildNodes) { // Filter to make sure only nodes of same type are considered. targetParentChildNodes = targetParentChildNodes.stream().filter(n -> n.getNodeType().equals(sourceNode.getNodeType())).collect(Collectors.toList()); - List targetParentChildNodeNames = targetParentChildNodes.stream().map(Node::getName).collect(Collectors.toList()); + List targetParentChildNodeNames = targetParentChildNodes.stream().map(Node::getName).toList(); if (!targetParentChildNodeNames.contains(sourceNode.getName())) { return sourceNode.getName(); } @@ -1258,7 +1256,7 @@ protected String determineNewNodeName(Node sourceNode, List targetParentCh if (nodeNameCopies.isEmpty()) { return newNodeBaseName + " copy"; } else { - Collections.sort(nodeNameCopies, new NodeNameComparator()); + nodeNameCopies.sort(new NodeNameComparator()); try { String lastCopyName = nodeNameCopies.get(nodeNameCopies.size() - 1); if (lastCopyName.equals(newNodeBaseName + " copy")) { 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 7a05b37f78..cf90c4714a 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 @@ -71,9 +71,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, + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_UPDATED, targetNode)); - webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, + webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_UPDATED, sourceParentNode)); return targetNode; } @@ -98,8 +98,11 @@ public Node copyNodes(@RequestParam(value = "to") String to, if (to.isEmpty() || nodes.isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target node and list of source nodes must all be non-empty."); } - Logger.getLogger(StructureController.class.getName()).info(Thread.currentThread().getName() + " " + (new Date()) + " move"); - return nodeDAO.copyNodes(nodes, to, principal.getName()); + 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()))); + return targetNode; } /** diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java index c36dcee8c6..63a21f75d9 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java @@ -30,6 +30,7 @@ import org.epics.vtype.VDouble; import org.epics.vtype.VInt; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -113,6 +114,13 @@ public void init() { time = Time.of(Instant.now()); alarm = Alarm.of(AlarmSeverity.NONE, AlarmStatus.NONE, "name"); display = Display.none(); + clearAllData(); + System.out.println(); + } + + @AfterEach + public void cleanUp(){ + clearAllData(); } @Test @@ -126,8 +134,6 @@ public void testNewNode() { assertEquals(NodeType.FOLDER, newNode.getNodeType()); // Check that the parent folder's last modified date is updated assertTrue(root.getLastModified().getTime() > lastModified.getTime()); - - clearAllData(); } @Test @@ -140,8 +146,6 @@ public void testNewFolderNoDuplicateName() { assertNotNull(nodeDAO.createNode(rootNode.getUniqueId(), folder1)); // Try to create a new folder with a different name in the same parent directory assertNotNull(nodeDAO.createNode(rootNode.getUniqueId(), folder2)); - - clearAllData(); } @Test @@ -190,8 +194,6 @@ public void testDeleteConfigurationAndPvs() { // This should not throw exception nodeDAO.createNode(topLevelFolderNode.getUniqueId(), config); - - clearAllData(); } @@ -239,7 +241,6 @@ public void testGetNodeAsConfig() { Node configFromDB = nodeDAO.getNode(newConfig.getUniqueId()); assertEquals(newConfig.getUniqueId(), configFromDB.getUniqueId()); - clearAllData(); } @Test @@ -276,7 +277,6 @@ public void testGetConfigForSnapshot() { assertNotNull(config); - clearAllData(); } @Test @@ -321,8 +321,6 @@ public void testDeleteSnapshotReferencedInCompositeSnapshot() { assertThrows(RuntimeException.class, () -> nodeDAO.deleteNode(snapshotNode.getUniqueId())); nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); - - clearAllData(); } @Test @@ -404,7 +402,6 @@ public void testUpdateCompositeSnapshot() { nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); - clearAllData(); } @Test @@ -458,8 +455,6 @@ public void testGetAllCompositeSnapshotData() { compositeSnapshotNodeIds.forEach(id -> nodeDAO.deleteNode(id)); - clearAllData(); - } @Test @@ -510,7 +505,6 @@ public void testTakeSnapshot() { snapshots = nodeDAO.getSnapshots(config.getUniqueId()); assertTrue(snapshots.isEmpty()); - clearAllData(); } @Test @@ -568,7 +562,6 @@ public void testUpdateSnapshot() { assertEquals("other snapshot name", snapshotNode.getName()); assertEquals("other comment", snapshotNode.getDescription()); - clearAllData(); } @Test @@ -627,7 +620,6 @@ public void testGetSnapshotItemsWithNullPvValues() { assertNull(snapshot1.getSnapshotData().getSnapshotItems()); - clearAllData(); } @Test @@ -703,7 +695,6 @@ public void testSnapshotTag() { tagList2 = n2.getTags(); assertFalse(tagList2.stream().anyMatch(t -> t.getName().equals(newTag.getName()))); - clearAllData(); } @Test @@ -725,7 +716,6 @@ public void testGetChildNodes() { List childNodes = nodeDAO.getChildNodes(rootNode.getUniqueId()); assertTrue(nodeDAO.getChildNodes(folder1.getUniqueId()).isEmpty()); - clearAllData(); } @Test @@ -746,7 +736,6 @@ public void testUpdateNode() { assertEquals("folderB", folderA.getName()); assertEquals(uniqueId, folderA.getUniqueId()); - clearAllData(); } @@ -848,8 +837,6 @@ public void testUpdateNodeType() { Node node = folderNode; assertThrows(IllegalArgumentException.class, () -> nodeDAO.updateNode(node, false)); - - clearAllData(); } @@ -866,7 +853,6 @@ public void testNoNameClash1() { Node n2 = Node.builder().nodeType(NodeType.CONFIGURATION).uniqueId("2").name("n1").build(); nodeDAO.createNode(topLevelFolderNode.getUniqueId(), n2); - clearAllData(); } @Test @@ -878,8 +864,6 @@ public void testNoNameClash() { nodeDAO.createNode(rootNode.getUniqueId(), n1); Node n2 = Node.builder().uniqueId("2").name("n2").build(); nodeDAO.createNode(rootNode.getUniqueId(), n2); - - clearAllData(); } @Test @@ -901,7 +885,6 @@ public void testUpdateNodeNewNameInvalid() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.updateNode(node, false)); - clearAllData(); } @Test @@ -910,7 +893,6 @@ public void testCreateNodeWithNonNullUniqueId() { Node folder = Node.builder().name("Folder").nodeType(NodeType.FOLDER).uniqueId("uniqueid").build(); folder = nodeDAO.createNode(rootNode.getUniqueId(), folder); assertEquals("uniqueid", folder.getUniqueId()); - clearAllData(); } @Test @@ -938,8 +920,6 @@ public void testCreateFolderInConfigNode() { Node node = configNode; assertThrows(IllegalArgumentException.class, () -> nodeDAO.createNode(node.getUniqueId(), folderNode)); - - clearAllData(); } @Test @@ -960,8 +940,6 @@ public void testCreateConfigInConfigNode() { Node node = configNode; assertThrows(IllegalArgumentException.class, () -> nodeDAO.createNode(node.getUniqueId(), node)); - - clearAllData(); } @Test @@ -995,8 +973,6 @@ public void testCreateNameClash() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.createNode(rootNode.getUniqueId(), anotherConfig)); - - clearAllData(); } @Test @@ -1032,8 +1008,6 @@ public void testFindParentFromPathElements() { found = nodeDAO.findParentFromPathElements(rootNode, "/a/d/c".split("/"), 1); assertNull(found); - - clearAllData(); } @Test @@ -1046,8 +1020,6 @@ public void testGetFromPathTwoNodes() { List nodes = nodeDAO.getFromPath("/a/b/c"); assertEquals(2, nodes.size()); - - clearAllData(); } @Test @@ -1068,8 +1040,6 @@ public void testGetFromPathOneNode() { nodes = nodeDAO.getFromPath("/a"); assertEquals(1, nodes.size()); assertEquals(a.getUniqueId(), nodes.get(0).getUniqueId()); - - clearAllData(); } @Test @@ -1084,8 +1054,6 @@ public void testGetFromPathZeroNodes() { nodes = nodeDAO.getFromPath("/a/x/c"); assertNull(nodes); - - clearAllData(); } @Test @@ -1115,8 +1083,6 @@ public void testGetFullPathNonExistingNode() { // This will throw NodeNotFoundException assertThrows(NodeNotFoundException.class, () -> nodeDAO.getFullPath("nonExisting")); - - clearAllData(); } @Test @@ -1127,8 +1093,6 @@ public void testGetFullPathRootNode() { nodeDAO.createNode(b.getUniqueId(), Node.builder().nodeType(NodeType.FOLDER).name("c").build()); assertEquals("/", nodeDAO.getFullPath(rootNode.getUniqueId())); - - clearAllData(); } @Test @@ -1139,8 +1103,6 @@ public void testGetFullPath() { Node c = nodeDAO.createNode(b.getUniqueId(), Node.builder().nodeType(NodeType.FOLDER).name("c").build()); assertEquals("/a/b/c", nodeDAO.getFullPath(c.getUniqueId())); - - clearAllData(); } @Test @@ -1161,8 +1123,6 @@ public void testMoveNodesInvalidId() { assertThrows(NodeNotFoundException.class, () -> nodeDAO.moveNodes(nodeIds, rootNode.getUniqueId(), "userName")); - - clearAllData(); } @Test @@ -1191,8 +1151,6 @@ public void testMoveConfiguration() { assertEquals(1, nodeDAO.getChildNodes(topLevelFolderNode2.getUniqueId()).size()); assertEquals(0, nodeDAO.getChildNodes(topLevelFolderNode.getUniqueId()).size()); - - clearAllData(); } @Test @@ -1221,8 +1179,6 @@ public void testMoveConfigurationNameClash() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.moveNodes(List.of(_configNode.getUniqueId()), topLevelFolderNode2.getUniqueId(), "user")); - - clearAllData(); } @Test @@ -1246,8 +1202,6 @@ public void testMoveSnasphotToRoot() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.moveNodes(List.of(uniqueId), rootNode.getUniqueId(), "user")); - - clearAllData(); } @Test @@ -1271,8 +1225,6 @@ public void testMoveSnasphotToConfiguration() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.moveNodes(List.of(uniqueId), rootNode.getUniqueId(), "user")); - - clearAllData(); } @Test @@ -1309,8 +1261,6 @@ public void testMoveNodes() { // After move parent of source nodes should now have only one element assertEquals(1, nodeDAO.getChildNodes(folderNode.getUniqueId()).size()); - - clearAllData(); } @Test @@ -1325,8 +1275,6 @@ public void testCopyFolderToSameParent() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(List.of(uniqueId), rootNode.getUniqueId(), "username")); - - clearAllData(); } @Test @@ -1344,8 +1292,6 @@ public void testCopyConfigToSameParent() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(List.of(uniqueId), rootNode.getUniqueId(), "username")); - - clearAllData(); } @Test @@ -1366,8 +1312,6 @@ public void testCopyFolderNotSupported() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(List.of(_childFolderNode.getUniqueId()), rootNode.getUniqueId(), "username")); - - clearAllData(); } @Test @@ -1399,8 +1343,6 @@ public void testCopyConfigToOtherParent() { List childNodes = nodeDAO.getChildNodes(rootNode.getUniqueId()); assertEquals(2, childNodes.size()); - - clearAllData(); } @Test @@ -1427,8 +1369,6 @@ public void testCopyMultipleFolders() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(Arrays.asList(f1, f2), rootNode.getUniqueId(), "username")); - - clearAllData(); } @Test @@ -1455,8 +1395,6 @@ public void testCopyFolderAndConfig() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(Arrays.asList(folderUniqueId, configUniqueId), rootNode.getUniqueId(), "username")); - - clearAllData(); } @Test @@ -1498,8 +1436,6 @@ public void testCopySnapshotToFolderNotSupported() { String uniqueId = folderNode1.getUniqueId(); assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(List.of(snapshotId), uniqueId, "username")); - - clearAllData(); } @Test @@ -1540,12 +1476,10 @@ public void testCopySnapshotToConfiguration() { String snapshotId = snapshot.getSnapshotNode().getUniqueId(); - Node updatedConfigNode = + List newNodes = nodeDAO.copyNodes(List.of(snapshotId), configuration2.getConfigurationNode().getUniqueId(), "useername"); - assertEquals(1, nodeDAO.getChildNodes(updatedConfigNode.getUniqueId()).size()); - - clearAllData(); + assertEquals(1, newNodes.size()); } @Test @@ -1591,8 +1525,6 @@ public void testCopySnapshotToConfigurationPvListMismatch() { assertThrows(IllegalArgumentException.class, () -> nodeDAO.copyNodes(List.of(snapshotId), config2Id, "userName")); - - clearAllData(); } @Test @@ -1641,18 +1573,17 @@ public void testCopyCompositeSnapshot() { String compositeSnapshotId = compositeSnapshot.getCompositeSnapshotNode().getUniqueId(); - folderNode1 = nodeDAO.copyNodes(List.of(compositeSnapshotId), folderNode1.getUniqueId(), "user"); + List newNodes = nodeDAO.copyNodes(List.of(compositeSnapshotId), folderNode1.getUniqueId(), "user"); List childNodes = nodeDAO.getChildNodes(folderNode1.getUniqueId()); + assertEquals(1, newNodes.size()); assertEquals(1, childNodes.size()); // Make sure referenced nodes have been copied to copied composite snapshot assertEquals(1, nodeDAO.getCompositeSnapshotData(childNodes.get(0).getUniqueId()).getReferencedSnapshotNodes().size()); nodeDAO.deleteNode(childNodes.get(0).getUniqueId()); nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); - - clearAllData(); } @Test @@ -1716,8 +1647,6 @@ public void testCopyCompositeSnapshotToConfiguration() { () -> nodeDAO.copyNodes(List.of(compositeSnapshotId), config2Id, "user")); nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); - - clearAllData(); } @Test @@ -1793,8 +1722,6 @@ public void testIsContainedInSubTree() { assertFalse(nodeDAO.isContainedInSubtree(L2F2.getUniqueId(), rootNode.getUniqueId())); assertFalse(nodeDAO.isContainedInSubtree(L1F1.getUniqueId(), L1F1.getUniqueId())); assertFalse(nodeDAO.isContainedInSubtree(L2F1.getUniqueId(), L1F1.getUniqueId())); - - clearAllData(); } @Test @@ -1836,8 +1763,6 @@ public void testGetAllTags() { List tags = nodeDAO.getAllTags(); assertEquals(4, tags.size()); - - clearAllData(); } @Test @@ -1877,8 +1802,6 @@ public void testGetAllSnapshots() { List snapshotNodes = nodeDAO.getAllSnapshots(); assertEquals(1, snapshotNodes.size()); - - clearAllData(); } @Test @@ -1896,8 +1819,6 @@ public void testGetAllNodes() { List nodes = nodeDAO.getNodes(Arrays.asList(folderNode1.getUniqueId(), folderNode2.getUniqueId())); assertEquals(2, nodes.size()); - - clearAllData(); } /** @@ -1907,7 +1828,6 @@ private void clearAllData() { List childNodes = nodeDAO.getChildNodes(Node.ROOT_FOLDER_UNIQUE_ID); childNodes.forEach(node -> nodeDAO.deleteNode(node.getUniqueId())); nodeDAO.deleteAllFilters(); - } @@ -2054,8 +1974,6 @@ public void testCheckForPVNameDuplicates() { assertEquals("pv1", duplicates.get(0)); nodeDAO.deleteNode(compositeSnapshotNode.getUniqueId()); - - clearAllData(); } @Test @@ -2140,8 +2058,6 @@ public void testCheckForRejectedReferencedNodesInCompositeSnapshot() { compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); assertFalse(nodeDAO.checkCompositeSnapshotReferencedNodeTypes(compositeSnapshot)); - - clearAllData(); } @Test @@ -2224,8 +2140,6 @@ public void testGetSnapshotItemsFromCompositeSnapshot() { assertEquals(4, snapshotItems.size()); nodeDAO.deleteNode(compositeSnapshotNode.getUniqueId()); - - clearAllData(); } @Test @@ -2278,8 +2192,6 @@ public void testFilters() { unformattedQueryStringFilter = nodeDAO.saveFilter(unformattedQueryStringFilter); assertTrue(unformattedQueryStringFilter.getQueryString().contains("type=Folder,Configuration")); assertFalse(unformattedQueryStringFilter.getQueryString().contains("unsupoorted")); - - clearAllData(); } @Test @@ -2340,8 +2252,5 @@ public void testSearchForPvs() { searchResult = nodeDAO.search(searchParameters); // No pvs specified -> find all nodes. assertEquals(4, searchResult.getHitCount()); - - clearAllData(); - } } 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 df736f8acc..67eeb88386 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 @@ -20,11 +20,15 @@ package org.phoebus.service.saveandrestore.web.controllers; import com.fasterxml.jackson.databind.ObjectMapper; +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.Node; +import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; @@ -38,6 +42,9 @@ import java.util.Collections; import java.util.List; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +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.post; @@ -73,9 +80,16 @@ public class StructureControllerTest { @Autowired private String readOnlyAuthorization; + @Autowired + private WebSocketHandler webSocketHandler; + + @AfterEach + public void resetMocks(){ + reset(webSocketHandler, nodeDAO); + } @Test - public void testMoveNode() throws Exception { + public void testMoveNode1() throws Exception { when(nodeDAO.moveNodes(List.of("a"), "b", demoAdmin)) .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoAdmin).build()); @@ -92,10 +106,16 @@ public void testMoveNode() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); + verify(webSocketHandler, times(2)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testMoveNode2() throws Exception{ + when(nodeDAO.moveNodes(List.of("a"), "b", demoUser)) .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoUser).build()); - request = post("/move") + MockHttpServletRequestBuilder request = post("/move") .header(HttpHeaders.AUTHORIZATION, userAuthorization) .contentType(JSON) .content(objectMapper.writeValueAsString(List.of("a"))) @@ -104,8 +124,13 @@ public void testMoveNode() throws Exception { mockMvc.perform(request).andExpect(status().isForbidden()); + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } - request = post("/move") + @Test + public void testMoveNode3() throws Exception{ + + MockHttpServletRequestBuilder request = post("/move") .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) .contentType(JSON) .content(objectMapper.writeValueAsString(List.of("a"))) @@ -121,6 +146,8 @@ public void testMoveNode() throws Exception { .param("username", "username"); mockMvc.perform(request).andExpect(status().isUnauthorized()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test @@ -133,6 +160,8 @@ public void testMoveNodeSourceNodeListEmpty() throws Exception { .param("username", "user"); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test @@ -145,10 +174,12 @@ public void testMoveNodeTargetIdEmpty() throws Exception { .param("username", "user"); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } @Test - public void testCopyNodes() throws Exception { + public void testCopyNodes1() throws Exception { MockHttpServletRequestBuilder request = post("/copy") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) @@ -163,7 +194,13 @@ public void testCopyNodes() throws Exception { .param("to", "target"); mockMvc.perform(request).andExpect(status().isForbidden()); - request = post("/copy") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCopyNodes2() throws Exception{ + + MockHttpServletRequestBuilder request = post("/copy") .contentType(JSON) .content(objectMapper.writeValueAsString(List.of("a"))) .param("to", "target"); @@ -171,8 +208,8 @@ public void testCopyNodes() throws Exception { } @Test - public void testCopyNodesBadRequest() throws Exception { - Node node = Node.builder().uniqueId("uniqueId").userName(demoUser).build(); + public void testCopyNodesBadRequest1() throws Exception { + MockHttpServletRequestBuilder request = post("/copy") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) @@ -180,17 +217,30 @@ public void testCopyNodesBadRequest() throws Exception { .param("to", ""); mockMvc.perform(request).andExpect(status().isBadRequest()); - request = post("/copy") + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); + } + + @Test + public void testCopyNodesBadRequest2() throws Exception { + + MockHttpServletRequestBuilder request = post("/copy") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(objectMapper.writeValueAsString(List.of("a"))); mockMvc.perform(request).andExpect(status().isBadRequest()); - request = post("/copy") + } + + @Test + public void testCopyNodesBadRequest3() throws Exception { + + MockHttpServletRequestBuilder request = post("/copy") .header(HttpHeaders.AUTHORIZATION, adminAuthorization) .contentType(JSON) .content(objectMapper.writeValueAsString(Collections.emptyList())) .param("to", "target"); mockMvc.perform(request).andExpect(status().isBadRequest()); + + verify(webSocketHandler, times(0)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class)); } } From 7a9d9408a4b8f49990a3a4d8128f0dcd9b50a981 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 7 May 2025 13:11:46 +0200 Subject: [PATCH 37/43] Code cleanup, javadoc and doc --- app/save-and-restore/app/doc/index.rst | 10 ++- .../ui/SaveAndRestoreController.java | 3 +- .../model/websocket/MessageType.java | 5 +- .../SaveAndRestoreWebSocketMessage.java | 6 +- .../websocket/WebMessageDeserializer.java | 27 +++--- .../core/websocket/WebSocketClient.java | 49 ++++++++++- .../saveandrestore/websocket/WebSocket.java | 88 ++----------------- .../websocket/WebSocketHandler.java | 2 - 8 files changed, 86 insertions(+), 104 deletions(-) diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst index cc08a6da49..b7c4816767 100644 --- a/app/save-and-restore/app/doc/index.rst +++ b/app/save-and-restore/app/doc/index.rst @@ -58,9 +58,13 @@ A word of caution ----------------- Save-and-restore data is persisted in a central service and is therefore accessible by multiple -clients. Users should keep in mind that changes (e.g. new or deleted nodes) are not pushed to all clients. -Caution is therefore advocated when working on the nodes in the tree, in particular when changing the structure by -copying, deleting or moving nodes. +clients. Users should keep in mind that changes (e.g. new or deleted nodes) are pushed by the service to +all connected clients. If any other user is working in the save-and-restore app, saved changes may update +the current view. For instance, if a folder node is expanded and another user adds an object (folder +or configuration) to that folder, the new object will automatically be added to the expanded folder. + +In other words, changes in the current view are triggered not only by the current user, but may be triggered as a result of +changes done by others. Tree View Context Menu ---------------------- 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 545628b99c..f0de9c6996 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 @@ -977,8 +977,7 @@ private void exportToCSV() { protected void addTagToSnapshots() { ObservableList> selectedItems = browserSelectionModel.getSelectedItems(); List selectedNodes = selectedItems.stream().map(TreeItem::getValue).collect(Collectors.toList()); - List updatedNodes = TagUtil.addTag(selectedNodes); - //updatedNodes.forEach(this::nodeChanged); + TagUtil.addTag(selectedNodes); } /** 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/MessageType.java index 7860ad9ad2..9cc99875bd 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/MessageType.java @@ -4,10 +4,13 @@ package org.phoebus.applications.saveandrestore.model.websocket; +/** + * Enum to indicate what type of web socket message the service is sending to clients. + */ public enum MessageType { NODE_ADDED, NODE_UPDATED, NODE_REMOVED, FILTER_ADDED_OR_UPDATED, - FILTER_REMOVED; + FILTER_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 index 0fe9d247bf..30e6a7d6d7 100644 --- 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 @@ -4,6 +4,10 @@ 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/WebMessageDeserializer.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java index 49bd41ef1c..7291360de5 100644 --- 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 @@ -10,17 +10,26 @@ 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 ObjectMapper objectMapper = new ObjectMapper(); + 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) { @@ -28,20 +37,16 @@ public SaveAndRestoreWebSocketMessage deserialize(JsonParser j 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_ADDED, NODE_REMOVED, FILTER_REMOVED -> { + return objectMapper.readValue(rootNode.toString(), SaveAndRestoreWebSocketMessage.class); } case NODE_UPDATED -> { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { + return objectMapper.readValue(rootNode.toString(), new TypeReference<>() { }); - return saveAndRestoreWebSocketMessage; } case FILTER_ADDED_OR_UPDATED -> { - SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { + return objectMapper.readValue(rootNode.toString(), new TypeReference<>() { }); - return saveAndRestoreWebSocketMessage; } } } catch (Exception e) { 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 index 3098e98762..fcfedaa3f2 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -29,18 +29,26 @@ public class WebSocketClient implements WebSocket.Listener { /** * @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() { attemptConnect.set(true); - reconnect(); + doConnect(); } - private void reconnect() { + /** + * Internal connect implementation. This is done in a loop with 10 s intervals until + * connection is established. + */ + private void doConnect() { new Thread(() -> { while (attemptConnect.get()) { logger.log(Level.INFO, "Attempting web socket connection to " + uri); @@ -63,6 +71,12 @@ private void reconnect() { }).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); @@ -87,6 +101,16 @@ public void sendText(String message) { } + /** + * 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, @@ -95,7 +119,7 @@ public CompletionStage onClose(WebSocket webSocket, if (disconnectCallback != null) { disconnectCallback.run(); } - reconnect(); + doConnect(); return null; } @@ -131,15 +155,34 @@ public CompletionStage onText(WebSocket webSocket, 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) { attemptConnect.set(false); 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; } 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 index 7e20ede0ff..4d1458aaf0 100644 --- 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 @@ -8,35 +8,20 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.JsonNodeType; -import org.epics.vtype.VType; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; 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. In the context of the save-and-restore service, + * only messages from server are expected. Client messages are logged, but do not invoke any behavior. + */ @SuppressWarnings("nls") public class WebSocket { - /** - * Time when web socket was created - */ - private final long created = System.currentTimeMillis(); - - /** - * Track when the last message was received by web client - */ - private volatile long lastClientMessage = 0; - - /** - * Track when the last message was sent to web client - */ - private volatile long lastMessageSent = 0; /** * Is the queue full? @@ -54,9 +39,8 @@ public class WebSocket { private static final String EXIT_MESSAGE = "EXIT"; - private volatile WebSocketSession session; - private volatile String id; - + private final WebSocketSession session; + private final String id; private final Logger logger = Logger.getLogger(WebSocket.class.getName()); @@ -74,7 +58,6 @@ public WebSocket(ObjectMapper objectMapper, WebSocketSession webSocketSession) { writeThread.setName("Web Socket Write Thread " + this.id); writeThread.setDaemon(true); writeThread.start(); - trackClientUpdate(); } /** @@ -87,34 +70,6 @@ public String getId() { return id; } - /** - * @return Timestamp (ms since epoch) when socket was created - */ - public long getCreateTime() { - return created; - } - - /** - * @return Timestamp (ms since epoch) of last client message - */ - public long getLastClientMessage() { - return lastClientMessage; - } - - /** - * @return Timestamp (ms since epoch) of last message sent to client - */ - public long getLastMessageSent() { - return lastMessageSent; - } - - /** - * @return Number of queued messages - */ - public int getQueuedMessageCount() { - return writeQueue.size(); - } - /** * @param message Potentially long message * @return Message shorted to 200 chars @@ -162,7 +117,6 @@ private void writeQueuedMessages() { if (!safeSession.isOpen()) throw new Exception("Session closed"); safeSession.sendMessage(new TextMessage(message)); - lastMessageSent = System.currentTimeMillis(); } catch (final Exception ex) { logger.log(Level.WARNING, ex, () -> "Cannot write '" + shorten(message) + "' for " + id); @@ -182,10 +136,6 @@ private void writeQueuedMessages() { } } - public void trackClientUpdate() { - lastClientMessage = System.currentTimeMillis(); - } - /** * Called when client sends a general message * @@ -197,12 +147,7 @@ public void handleTextMessage(TextMessage message) throws Exception { if (node.isMissingNode()) throw new Exception("Missing 'type' in " + shorten(message.getPayload())); final String type = node.asText(); - - switch (type) { - case "monitor": - default: - throw new Exception("Unknown message type: " + shorten(message.getPayload())); - } + logger.log(Level.INFO, "Client message type: " + type); } /** @@ -224,24 +169,5 @@ public void dispose() { logger.log(Level.WARNING, "Error disposing " + getId(), ex); } logger.log(Level.INFO, () -> "Web socket " + session.getId() + " closed"); - lastClientMessage = 0; - } - - - private void write(String message, JsonNode json) throws Exception { - JsonNode n = json.path("pv"); - if (n.isMissingNode()) - throw new Exception("Missing 'pv' in " + shorten(message)); - final String pv_name = n.asText(); - - n = json.path("value"); - if (n.isMissingNode()) - throw new Exception("Missing 'value' in " + shorten(message)); - final Object value; - if (n.getNodeType() == JsonNodeType.NUMBER) - value = n.asDouble(); - else - value = n.asText(); - } } 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 index ae99b57d1e..82eee32225 100644 --- 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 @@ -145,8 +145,6 @@ protected void handlePongMessage(@NonNull WebSocketSession session, @NonNull Pon if (webSocketOptional.isEmpty()) { return; // Should only happen in case of timing issues? } - webSocketOptional.get().trackClientUpdate(); - } /** From 3616a975dadc410f6075236fd3787673642cf060 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 7 May 2025 13:46:22 +0200 Subject: [PATCH 38/43] Remove printout --- .../service/saveandrestore/websocket/WebSocketHandler.java | 4 ---- 1 file changed, 4 deletions(-) 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 index 82eee32225..2bcb98a0da 100644 --- 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 @@ -59,10 +59,6 @@ public class WebSocketHandler extends TextWebSocketHandler { private final Logger logger = Logger.getLogger(WebSocketHandler.class.getName()); - public WebSocketHandler(){ - System.out.println(); - } - /** * Handles text message from web socket client * From 0ce1b0085b6b4016ab5e3cb9e8488042f7c2e43e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 7 May 2025 15:29:32 +0200 Subject: [PATCH 39/43] Update logging of connection failure --- .../main/java/org/phoebus/core/websocket/WebSocketClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index fcfedaa3f2..dd4faf1941 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -60,7 +60,7 @@ private void doConnect() { .join(); break; } catch (Exception e) { - logger.log(Level.INFO, "Failed to connect to " + uri + " " + (e.getMessage() != null ? e.getMessage() : "")); + logger.log(Level.INFO, "Failed to connect to web socket on " + uri, e); } try { Thread.sleep(10000); From 90b5dfa780313042bac182c1b028e1eab1b58c6b Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 9 May 2025 13:24:06 +0200 Subject: [PATCH 40/43] Updates due to review feed-back --- .../ui/SaveAndRestoreController.java | 2 +- .../ui/configuration/ConfigurationController.java | 2 +- .../ui/snapshot/SnapshotController.java | 2 +- .../saveandrestore/ui/snapshot/SnapshotTab.java | 2 -- .../model/websocket/WebMessageDeserializer.java | 14 ++++++++++---- 5 files changed, 13 insertions(+), 9 deletions(-) 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 f0de9c6996..376d1102e1 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 @@ -1272,7 +1272,7 @@ private void pasteFromClipboard() { } List selectedNodeIds = ((List) selectedNodes).stream().map(Node::getUniqueId).collect(Collectors.toList()); - JobManager.schedule("Copy odes", monitor -> { + JobManager.schedule("Copy nodes", monitor -> { try { saveAndRestoreService.copyNodes(selectedNodeIds, browserSelectionModel.getSelectedItem().getValue().getUniqueId()); disabledUi.set(false); 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 4197c829d5..6ba19f78d5 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 @@ -477,7 +477,7 @@ public void newConfiguration(Node parentNode) { * * @param node An existing {@link Node} of type {@link NodeType#CONFIGURATION}. */ - public synchronized void loadConfiguration(final Node node) { + public void loadConfiguration(final Node node) { removeListeners(); JobManager.schedule("Load save&restore configuration", monitor -> { final ConfigurationData configurationData; 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 93283f9dd1..ef725bda58 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 @@ -381,7 +381,7 @@ private void loadSnapshotInternal(Node snapshotNode) { * * @param snapshotNode An existing {@link Node} of type {@link NodeType#SNAPSHOT} */ - public synchronized void loadSnapshot(Node snapshotNode) { + public void loadSnapshot(Node snapshotNode) { snapshotControlsViewController.setSnapshotNode(snapshotNode); snapshotControlsViewController.setSnapshotRestorableProperty(true); snapshotTableViewController.setSelectionColumnVisible(true); 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 c78c820394..948a7c2603 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 @@ -107,8 +107,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save compareSnapshotToArchiverDataMenuItem.disableProperty().set(snapshot.getSnapshotNode().getUniqueId() == null); }); getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem); - - //WebSocketClientService.getInstance().addWebSocketMessageHandler(this); } /** 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 index 7291360de5..3e636cbf5a 100644 --- 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 @@ -10,6 +10,8 @@ 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. @@ -37,16 +39,20 @@ public SaveAndRestoreWebSocketMessage deserialize(JsonParser j JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); String messageType = rootNode.get("messageType").asText(); switch (MessageType.valueOf(messageType)) { - case NODE_ADDED, NODE_REMOVED, FILTER_REMOVED -> { - return objectMapper.readValue(rootNode.toString(), SaveAndRestoreWebSocketMessage.class); + case NODE_ADDED, NODE_REMOVED, FILTER_REMOVED-> { + SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = + objectMapper.readValue(rootNode.toString(), SaveAndRestoreWebSocketMessage.class); + return saveAndRestoreWebSocketMessage; } case NODE_UPDATED -> { - return objectMapper.readValue(rootNode.toString(), new TypeReference<>() { + SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { }); + return saveAndRestoreWebSocketMessage; } case FILTER_ADDED_OR_UPDATED -> { - return objectMapper.readValue(rootNode.toString(), new TypeReference<>() { + SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() { }); + return saveAndRestoreWebSocketMessage; } } } catch (Exception e) { From 7ea9f1785bcedcfa6e327401a1739ca477074e53 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 12 May 2025 14:24:35 +0200 Subject: [PATCH 41/43] Refresh UI when web socket client reconnects --- .../ui/SaveAndRestoreController.java | 60 ++++++++++--------- .../saveandrestore/ui/SaveAndRestoreUI.fxml | 4 -- 2 files changed, 31 insertions(+), 33 deletions(-) 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 376d1102e1..a19fbc78ec 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 @@ -25,7 +25,6 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; @@ -38,7 +37,6 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ContextMenu; -import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Menu; @@ -186,16 +184,12 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @FXML private VBox errorPane; - @SuppressWarnings("unused") - @FXML - private Label webSocketTrackerLabel; - private final ObservableList searchResultNodes = FXCollections.observableArrayList(); - private final ObservableList filtersList = FXCollections.observableArrayList(); private final CountDownLatch treeInitializationCountDownLatch = new CountDownLatch(1); private final ObservableList selectedItemsProperty = FXCollections.observableArrayList(); + private final SimpleBooleanProperty serviceConnected = new SimpleBooleanProperty(); private final ContextMenu contextMenu = new ContextMenu(); private final Menu tagWithComment = new Menu(Messages.contextMenuTags, new ImageView(ImageCache.getImage(SaveAndRestoreController.class, "/icons/save-and-restore/snapshot-add_tag.png"))); @@ -239,8 +233,6 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController new ExportToCSVMenuItem(this, selectedItemsProperty, this::exportToCSV) ); - private final SimpleStringProperty webSocketTrackerText = new SimpleStringProperty(); - @Override public void initialize(URL url, ResourceBundle resourceBundle) { @@ -347,13 +339,13 @@ public Filter fromString(String s) { contextMenu.getItems().addAll(menuItems); treeView.setContextMenu(contextMenu); - loadTreeData(); - - webSocketTrackerLabel.textProperty().bind(webSocketTrackerText); + splitPane.disableProperty().bind(serviceConnected.not()); + treeView.visibleProperty().bind(serviceConnected); + errorPane.visibleProperty().bind(serviceConnected.not()); webSocketClientService.addWebSocketMessageHandler(this); - webSocketClientService.setConnectCallback(() -> Platform.runLater(() -> webSocketTrackerText.setValue("Web Socket Connected"))); - webSocketClientService.setDisconnectCallback(() -> Platform.runLater(() -> webSocketTrackerText.setValue("Web Socket Disconnected"))); + webSocketClientService.setConnectCallback(this::handleWebSocketConnected); + webSocketClientService.setDisconnectCallback(this::handleWebSocketDisconnected); webSocketClientService.connect(); } @@ -365,11 +357,7 @@ public void loadTreeData() { JobManager.schedule("Load save-and-restore tree data", monitor -> { Node rootNode = saveAndRestoreService.getRootNode(); - if (rootNode == null) { // Service off-line or not reachable - treeInitializationCountDownLatch.countDown(); - errorPane.visibleProperty().set(true); - return; - } + treeInitializationCountDownLatch.countDown(); TreeItem rootItem = createTreeItem(rootNode); List savedTreeViewStructure = getSavedTreeStructure(); // Check if there is a save tree structure. Also check that the first node id (=tree root) @@ -385,12 +373,12 @@ public void loadTreeData() { } }); setChildItems(childNodesMap, rootItem); + } else { List childNodes = saveAndRestoreService.getChildNodes(rootItem.getValue()); List> childItems = childNodes.stream().map(this::createTreeItem).sorted(treeNodeComparator).toList(); rootItem.getChildren().addAll(childItems); } - Platform.runLater(() -> { treeView.setRoot(rootItem); expandNodes(treeView.getRoot()); @@ -398,6 +386,7 @@ public void loadTreeData() { treeView.getRoot().addEventHandler(TreeItem.branchExpandedEvent(), e -> expandTreeNode(e.getTreeItem())); treeInitializationCountDownLatch.countDown(); }); + loadFilters(); }); } @@ -920,7 +909,7 @@ public void saveLocalState() { @Override public boolean handleTabClosed() { - saveLocalState(); + //saveLocalState(); webSocketClientService.closeWebSocket(); return true; } @@ -1170,14 +1159,12 @@ private void filterAddedOrUpdated(Filter filter) { filtersList.add(filter); } else { final int index = filtersList.indexOf(filter); - Platform.runLater(() -> { - filtersList.set(index, filter); - filtersComboBox.valueProperty().set(filter); - // If this is the active filter, update the tree view - if (filter.equals(filtersComboBox.getSelectionModel().getSelectedItem())) { - applyFilter(filter); - } - }); + filtersList.set(index, filter); + filtersComboBox.valueProperty().set(filter); + // If this is the active filter, update the tree view + if (filter.equals(filtersComboBox.getSelectionModel().getSelectedItem())) { + applyFilter(filter); + } } } @@ -1444,4 +1431,19 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore case FILTER_REMOVED -> filterRemoved((String) saveAndRestoreWebSocketMessage.payload()); } } + + private void handleWebSocketConnected() { + serviceConnected.setValue(true); + Platform.runLater(() -> { + }); + loadTreeData(); + } + + private void handleWebSocketDisconnected() { + serviceConnected.setValue(false); + Platform.runLater(() -> { + + }); + saveLocalState(); + } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml index 585ab1630c..3608b13d1f 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml @@ -42,10 +42,6 @@ - From 939154fc0c97da877e92eadec849b4feaec721f1 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 12 May 2025 14:53:05 +0200 Subject: [PATCH 42/43] Code cleanup --- .../saveandrestore/ui/SaveAndRestoreController.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 a19fbc78ec..b9f9bda58d 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 @@ -909,7 +909,7 @@ public void saveLocalState() { @Override public boolean handleTabClosed() { - //saveLocalState(); + saveLocalState(); webSocketClientService.closeWebSocket(); return true; } @@ -1434,16 +1434,11 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore private void handleWebSocketConnected() { serviceConnected.setValue(true); - Platform.runLater(() -> { - }); loadTreeData(); } private void handleWebSocketDisconnected() { serviceConnected.setValue(false); - Platform.runLater(() -> { - - }); saveLocalState(); } } From 503dd4c609e8f730bf488d7c24c0efa0d44b5e1a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 13 May 2025 13:29:41 +0200 Subject: [PATCH 43/43] Ping/pong mechanism to handle connection issues in web socket --- .../core/websocket/WebSocketClient.java | 88 +++++++++++++------ 1 file changed, 60 insertions(+), 28 deletions(-) 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 index dd4faf1941..d196e1a503 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java @@ -9,6 +9,8 @@ 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; @@ -16,6 +18,12 @@ /** * 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 { @@ -25,10 +33,12 @@ public class WebSocketClient implements WebSocket.Listener { private Runnable disconnectCallback; private final URI uri; private final Consumer onTextCallback; - private final AtomicBoolean attemptConnect = new AtomicBoolean(true); + + private final AtomicBoolean attemptReconnect = new AtomicBoolean(); + private CountDownLatch pingCountdownLatch; /** - * @param uri The URI of the web socket peer. + * @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) { @@ -40,7 +50,6 @@ public WebSocketClient(URI uri, Consumer onTextCallback) { * Attempts to connect to the remote web socket. */ public void connect() { - attemptConnect.set(true); doConnect(); } @@ -49,23 +58,18 @@ public void connect() { * connection is established. */ private void doConnect() { + attemptReconnect.set(true); new Thread(() -> { - while (attemptConnect.get()) { + while (attemptReconnect.get()) { logger.log(Level.INFO, "Attempting web socket connection to " + uri); - try { - webSocket = HttpClient.newBuilder() - .build() - .newWebSocketBuilder() - .buildAsync(uri, this) - .join(); - break; - } catch (Exception e) { - logger.log(Level.INFO, "Failed to connect to web socket on " + uri, e); - } + HttpClient.newBuilder() + .build() + .newWebSocketBuilder() + .buildAsync(uri, this); try { Thread.sleep(10000); } catch (InterruptedException e) { - logger.log(Level.WARNING, "Interrupted while sleeping"); + logger.log(Level.WARNING, "Got interrupted exception"); } } }).start(); @@ -74,16 +78,19 @@ private void doConnect() { /** * 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 + * + * @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); + new Thread(new PingRunnable()).start(); } /** @@ -106,10 +113,10 @@ public void sendText(String message) { * {@link #disconnectCallback} which is called when connection is opened. * *

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

    - * @param webSocket - * the WebSocket that has been connected + * + * @param webSocket the WebSocket that has been connected */ @Override public CompletionStage onClose(WebSocket webSocket, @@ -119,7 +126,6 @@ public CompletionStage onClose(WebSocket webSocket, if (disconnectCallback != null) { disconnectCallback.run(); } - doConnect(); return null; } @@ -128,13 +134,14 @@ public CompletionStage onClose(WebSocket webSocket, * is called. */ public void sendPing() { - logger.log(Level.INFO, "Sending ping"); + logger.log(Level.FINE, "Sending ping"); webSocket.sendPing(ByteBuffer.allocate(0)); } @Override public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - logger.log(Level.INFO, "Got pong"); + pingCountdownLatch.countDown(); + logger.log(Level.FINE, "Got pong"); return WebSocket.Listener.super.onPong(webSocket, message); } @@ -156,19 +163,18 @@ public CompletionStage onText(WebSocket webSocket, } /** - * * 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. + * 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) { - attemptConnect.set(false); webSocket.sendClose(1000, reason); } @@ -186,4 +192,30 @@ public void setConnectCallback(Runnable connectCallback) { public void setDisconnectCallback(Runnable disconnectCallback) { this.disconnectCallback = disconnectCallback; } + + private class PingRunnable implements Runnable { + + @Override + public void run() { + while (true) { + 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; + } + } + } + } }