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 @@
-