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/pom.xml b/app/save-and-restore/app/pom.xml
index 18d95dcdf7..506cbed138 100644
--- a/app/save-and-restore/app/pom.xml
+++ b/app/save-and-restore/app/pom.xml
@@ -25,6 +25,16 @@
core-logbook
5.0.1-SNAPSHOT
+
+ 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/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/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java
index 660debeb9a..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
@@ -178,6 +178,8 @@ public class Messages {
public static String toolTipConfigurationExists;
public static String toolTipConfigurationExistsOption;
public static String toolTipMultiplierSpinner;
+ public static String unnamedConfiguration;
+ public static String unnamedCompositeSnapshot;
public static String unnamedSnapshot;
public static String updateCompositeSnapshotFailed;
@@ -185,6 +187,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..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
@@ -55,7 +55,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();
@@ -82,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/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/NodeAddedListener.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java
deleted file mode 100644
index 6f7305e2d1..0000000000
--- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/NodeAddedListener.java
+++ /dev/null
@@ -1,34 +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;
-
-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}.
- */
- void nodesAdded(Node parentNode, List newNodes);
-}
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/SaveAndRestoreBaseController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java
index 428ee52127..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
@@ -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;
@@ -32,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 =
@@ -63,4 +68,16 @@ 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}
+ */
+ 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 952fe132e5..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
@@ -76,6 +76,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;
@@ -134,7 +135,7 @@
* Main controller for the save and restore UI.
*/
public class SaveAndRestoreController extends SaveAndRestoreBaseController
- implements Initializable, NodeChangedListener, NodeAddedListener, FilterChangeListener {
+ implements Initializable, WebSocketMessageHandler {
@FXML
protected TreeView treeView;
@@ -165,24 +166,14 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController
@FXML
private VBox treeViewPane;
- protected SaveAndRestoreService saveAndRestoreService;
-
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")
@@ -194,11 +185,11 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController
private VBox errorPane;
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")));
@@ -211,14 +202,10 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController
List
*/
-public class SnapshotTab extends SaveAndRestoreTab {
+public class SnapshotTab extends SaveAndRestoreTab implements WebSocketMessageHandler {
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) {
@@ -98,22 +96,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save
return;
}
- ImageView imageView = new ImageView();
- imageView.imageProperty().bind(tabGraphicImageProperty);
-
- setGraphic(imageView);
-
- textProperty().set(node.getNodeType().equals(NodeType.SNAPSHOT) ? node.getName() : Messages.unnamedSnapshot);
- setTabImage(node);
-
- setOnCloseRequest(event -> {
- if (controller != null && !((SnapshotController) controller).handleSnapshotTabClosed()) {
- event.consume();
- } else {
- SaveAndRestoreService.getInstance().removeNodeChangeListener(this);
- }
- });
-
MenuItem compareSnapshotToArchiverDataMenuItem = new MenuItem(Messages.contextMenuCompareSnapshotWithArchiverData, new ImageView(compareSnapshotIcon));
compareSnapshotToArchiverDataMenuItem.setOnAction(ae -> addSnapshotFromArchive());
@@ -125,30 +107,6 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save
compareSnapshotToArchiverDataMenuItem.disableProperty().set(snapshot.getSnapshotNode().getUniqueId() == null);
});
getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem);
-
- SaveAndRestoreService.getInstance().addNodeChangeListener(this);
- }
-
- public void updateTabTitle(String name) {
- Platform.runLater(() -> textProperty().set(name));
- }
-
- /**
- * 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);
- }
- }
}
/**
@@ -158,7 +116,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);
}
@@ -168,8 +125,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);
}
@@ -181,16 +136,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();
}
@@ -199,4 +144,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/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 4936a88f4c..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
@@ -262,10 +262,14 @@ 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=
+unnamedCompositeSnapshot=
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/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..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
@@ -39,14 +39,9 @@
-
-
-
-
-
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..9cc99875bd
--- /dev/null
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/MessageType.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2024 European Spallation Source ERIC.
+ */
+
+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
+}
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
new file mode 100644
index 0000000000..30e6a7d6d7
--- /dev/null
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessage.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (C) 2024 European Spallation Source ERIC.
+ */
+
+package org.phoebus.applications.saveandrestore.model.websocket;
+
+/**
+ * Record encapsulating a {@link MessageType} and a payload.
+ * @param messageType The {@link MessageType} of a web socket message
+ * @param payload The payload, e.g. {@link String} or {@link org.phoebus.applications.saveandrestore.model.Node}
+ */
+public record SaveAndRestoreWebSocketMessage(MessageType messageType, T payload) {
+}
diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java
new file mode 100644
index 0000000000..3e636cbf5a
--- /dev/null
+++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 European Spallation Source ERIC.
+ */
+
+package org.phoebus.applications.saveandrestore.model.websocket;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import org.phoebus.applications.saveandrestore.model.Node;
+import org.phoebus.applications.saveandrestore.model.search.Filter;
+
+/**
+ * Custom JSON deserializer of {@link SaveAndRestoreWebSocketMessage}s.
+ */
+public class WebMessageDeserializer extends StdDeserializer {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public WebMessageDeserializer(Class> clazz) {
+ super(clazz);
+ }
+
+ /**
+ * Deserializes a {@link SaveAndRestoreWebSocketMessage}-
+ *
+ * @param jsonParser Parsed used for reading JSON content
+ * @param context Context that can be used to access information about
+ * this deserialization activity.
+ * @return A {@link SaveAndRestoreWebSocketMessage} object, or null if deserialization fails.
+ */
+ @Override
+ public SaveAndRestoreWebSocketMessage extends Object> deserialize(JsonParser jsonParser,
+ DeserializationContext context) {
+ try {
+ JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser);
+ String messageType = rootNode.get("messageType").asText();
+ switch (MessageType.valueOf(messageType)) {
+ case NODE_ADDED, NODE_REMOVED, FILTER_REMOVED-> {
+ SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage =
+ objectMapper.readValue(rootNode.toString(), SaveAndRestoreWebSocketMessage.class);
+ return saveAndRestoreWebSocketMessage;
+ }
+ case NODE_UPDATED -> {
+ SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() {
+ });
+ return saveAndRestoreWebSocketMessage;
+ }
+ case FILTER_ADDED_OR_UPDATED -> {
+ SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage = objectMapper.readValue(rootNode.toString(), new TypeReference<>() {
+ });
+ return saveAndRestoreWebSocketMessage;
+ }
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ return null;
+ }
+
+}
+
diff --git a/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java
new file mode 100644
index 0000000000..0e60a5eb07
--- /dev/null
+++ b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/websocket/SaveAndRestoreWebSocketMessageDeserializerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2025 European Spallation Source ERIC.
+ */
+
+package org.phoebus.applications.saveandrestore.model.websocket;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.junit.jupiter.api.Test;
+import org.phoebus.applications.saveandrestore.model.Node;
+import org.phoebus.applications.saveandrestore.model.search.Filter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class SaveAndRestoreWebSocketMessageDeserializerTest {
+
+ private ObjectMapper mapper = new ObjectMapper();
+
+ public SaveAndRestoreWebSocketMessageDeserializerTest(){
+ SimpleModule module = new SimpleModule();
+ module.addDeserializer(SaveAndRestoreWebSocketMessage.class,
+ new WebMessageDeserializer(SaveAndRestoreWebSocketMessage.class));
+ mapper.registerModule(module);
+ }
+
+ @Test
+ public void test1() {
+ try {
+ SaveAndRestoreWebSocketMessage webSocketMessage =
+ mapper.readValue(getClass().getResourceAsStream("/websocketexample2.json"), new TypeReference<>() {
+ });
+ assertEquals(MessageType.NODE_UPDATED, webSocketMessage.messageType());
+ assertEquals("a", webSocketMessage.payload().getUniqueId());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @Test
+ public void test2() {
+ try {
+ SaveAndRestoreWebSocketMessage webSocketMessage =
+ mapper.readValue(getClass().getResourceAsStream("/websocketexample1.json"), new TypeReference<>() {
+ });
+ assertEquals(MessageType.NODE_ADDED, webSocketMessage.messageType());
+ assertEquals("parentNodeId", webSocketMessage.payload());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void test3() {
+ try {
+ SaveAndRestoreWebSocketMessage webSocketMessage =
+ mapper.readValue(getClass().getResourceAsStream("/websocketexample3.json"), new TypeReference<>() {
+ });
+ assertEquals(MessageType.FILTER_ADDED_OR_UPDATED, webSocketMessage.messageType());
+ assertEquals("myFilter", webSocketMessage.payload().getName());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void test4() {
+ try {
+ SaveAndRestoreWebSocketMessage webSocketMessage =
+ mapper.readValue(getClass().getResourceAsStream("/websocketexample4.json"), new TypeReference<>() {
+ });
+ assertNull(webSocketMessage);
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void test5() {
+ try {
+ SaveAndRestoreWebSocketMessage webSocketMessage =
+ mapper.readValue(getClass().getResourceAsStream("/websocketexample5.json"), new TypeReference<>() {
+ });
+ assertNull(webSocketMessage.payload());
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/app/save-and-restore/model/src/test/resources/websocketexample1.json b/app/save-and-restore/model/src/test/resources/websocketexample1.json
new file mode 100644
index 0000000000..ed9ef5460b
--- /dev/null
+++ b/app/save-and-restore/model/src/test/resources/websocketexample1.json
@@ -0,0 +1,4 @@
+{
+ "messageType": "NODE_ADDED",
+ "payload": "parentNodeId"
+}
\ No newline at end of file
diff --git a/app/save-and-restore/model/src/test/resources/websocketexample2.json b/app/save-and-restore/model/src/test/resources/websocketexample2.json
new file mode 100644
index 0000000000..edee801d5d
--- /dev/null
+++ b/app/save-and-restore/model/src/test/resources/websocketexample2.json
@@ -0,0 +1,9 @@
+{
+ "messageType": "NODE_UPDATED",
+ "payload": {
+ "uniqueId": "a",
+ "name" : "myNode",
+ "nodeType": "CONFIGURATION",
+ "created": 1654690762473
+ }
+}
\ No newline at end of file
diff --git a/app/save-and-restore/model/src/test/resources/websocketexample3.json b/app/save-and-restore/model/src/test/resources/websocketexample3.json
new file mode 100644
index 0000000000..698641f01e
--- /dev/null
+++ b/app/save-and-restore/model/src/test/resources/websocketexample3.json
@@ -0,0 +1,8 @@
+{
+ "messageType": "FILTER_ADDED_OR_UPDATED",
+ "payload": {
+ "name": "myFilter",
+ "queryString": "a=b&c=d",
+ "user": "John Doe"
+ }
+}
\ No newline at end of file
diff --git a/app/save-and-restore/model/src/test/resources/websocketexample4.json b/app/save-and-restore/model/src/test/resources/websocketexample4.json
new file mode 100644
index 0000000000..b0ec835bdc
--- /dev/null
+++ b/app/save-and-restore/model/src/test/resources/websocketexample4.json
@@ -0,0 +1,4 @@
+{
+ "messageType": "INVALID",
+ "payload": "parentNodeId"
+}
\ No newline at end of file
diff --git a/app/save-and-restore/model/src/test/resources/websocketexample5.json b/app/save-and-restore/model/src/test/resources/websocketexample5.json
new file mode 100644
index 0000000000..0e09667bf6
--- /dev/null
+++ b/app/save-and-restore/model/src/test/resources/websocketexample5.json
@@ -0,0 +1,3 @@
+{
+ "messageType": "NODE_UPDATED"
+}
\ No newline at end of file
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/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java
new file mode 100644
index 0000000000..d196e1a503
--- /dev/null
+++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketClient.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2025 European Spallation Source ERIC.
+ */
+
+package org.phoebus.core.websocket;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.WebSocket;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A web socket client implementation supporting pong and text messages.
+ *
+ *
+ * Once connection is established, a ping/pong thread is set up to check peer availability. This should be
+ * able to handle both remote peer being shut down and network issues. Ping messages are dispatched once
+ * per minute. A reconnection loop is started if a pong message is not received from peer within three seconds.
+ *
+ */
+public class WebSocketClient implements WebSocket.Listener {
+
+ private WebSocket webSocket;
+ private final Logger logger = Logger.getLogger(WebSocketClient.class.getName());
+ private Runnable connectCallback;
+ private Runnable disconnectCallback;
+ private final URI uri;
+ private final Consumer onTextCallback;
+
+ private final AtomicBoolean attemptReconnect = new AtomicBoolean();
+ private CountDownLatch pingCountdownLatch;
+
+ /**
+ * @param uri The URI of the web socket peer.
+ * @param onTextCallback A callback method the API client will use to process web socket messages.
+ */
+ public WebSocketClient(URI uri, Consumer onTextCallback) {
+ this.uri = uri;
+ this.onTextCallback = onTextCallback;
+ }
+
+ /**
+ * Attempts to connect to the remote web socket.
+ */
+ public void connect() {
+ doConnect();
+ }
+
+ /**
+ * Internal connect implementation. This is done in a loop with 10 s intervals until
+ * connection is established.
+ */
+ private void doConnect() {
+ attemptReconnect.set(true);
+ new Thread(() -> {
+ while (attemptReconnect.get()) {
+ logger.log(Level.INFO, "Attempting web socket connection to " + uri);
+ HttpClient.newBuilder()
+ .build()
+ .newWebSocketBuilder()
+ .buildAsync(uri, this);
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ logger.log(Level.WARNING, "Got interrupted exception");
+ }
+ }
+ }).start();
+ }
+
+ /**
+ * Called when connection has been established. An API client may optionally register a
+ * {@link #connectCallback} which is called when connection is opened.
+ *
+ * @param webSocket the WebSocket that has been connected
+ */
+ @Override
+ public void onOpen(WebSocket webSocket) {
+ WebSocket.Listener.super.onOpen(webSocket);
+ attemptReconnect.set(false);
+ this.webSocket = webSocket;
+ if (connectCallback != null) {
+ connectCallback.run();
+ }
+ logger.log(Level.INFO, "Connected to " + uri);
+ new Thread(new PingRunnable()).start();
+ }
+
+ /**
+ * Send a text message to peer.
+ *
+ * @param message The actual message. In practice a JSON formatted string that peer can evaluate
+ * to take proper action.
+ */
+ public void sendText(String message) {
+ try {
+ webSocket.sendText(message, true).get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ /**
+ * Called when connection has been closed, e.g. by remote peer. An API client may optionally register a
+ * {@link #disconnectCallback} which is called when connection is opened.
+ *
+ *
+ * Note that reconnection will be attempted immediately.
+ *
+ *
+ * @param webSocket the WebSocket that has been connected
+ */
+ @Override
+ public CompletionStage> onClose(WebSocket webSocket,
+ int statusCode,
+ String reason) {
+ logger.log(Level.INFO, "Web socket closed, status code=" + statusCode + ", reason: " + reason);
+ if (disconnectCallback != null) {
+ disconnectCallback.run();
+ }
+ return null;
+ }
+
+ /**
+ * Utility method to check connectivity. Peer should respond such that {@link #onPong(WebSocket, ByteBuffer)}
+ * is called.
+ */
+ public void sendPing() {
+ logger.log(Level.FINE, "Sending ping");
+ webSocket.sendPing(ByteBuffer.allocate(0));
+ }
+
+ @Override
+ public CompletionStage> onPong(WebSocket webSocket, ByteBuffer message) {
+ pingCountdownLatch.countDown();
+ logger.log(Level.FINE, "Got pong");
+ return WebSocket.Listener.super.onPong(webSocket, message);
+ }
+
+ @Override
+ public void onError(WebSocket webSocket, Throwable error) {
+ logger.log(Level.WARNING, "Got web socket error", error);
+ WebSocket.Listener.super.onError(webSocket, error);
+ }
+
+ @Override
+ public CompletionStage> onText(WebSocket webSocket,
+ CharSequence data,
+ boolean last) {
+ webSocket.request(1);
+ if (onTextCallback != null) {
+ onTextCallback.accept(data);
+ }
+ return WebSocket.Listener.super.onText(webSocket, data, last);
+ }
+
+ /**
+ * NOTE: this must be called by the API client when web socket messages are no longer
+ * needed, otherwise reconnect attempts will continue as these run on a separate thread.
+ *
+ *
+ * The status code 1000 is used when calling the {@link WebSocket#sendClose(int, String)} method. See
+ * list of common web socket status codes
+ * here.
+ *
+ *
+ * @param reason Custom reason text.
+ */
+ public void close(String reason) {
+ webSocket.sendClose(1000, reason);
+ }
+
+ /**
+ * @param connectCallback A {@link Runnable} invoked when web socket connects successfully.
+ */
+ public void setConnectCallback(Runnable connectCallback) {
+ this.connectCallback = connectCallback;
+ }
+
+ /**
+ * @param disconnectCallback A {@link Runnable} invoked when web socket disconnects, either
+ * when closed explicitly, or if remote peer goes away.
+ */
+ public void setDisconnectCallback(Runnable disconnectCallback) {
+ this.disconnectCallback = disconnectCallback;
+ }
+
+ private class PingRunnable implements Runnable {
+
+ @Override
+ public void run() {
+ while (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;
+ }
+ }
+ }
+ }
+}
diff --git a/services/save-and-restore/pom.xml b/services/save-and-restore/pom.xml
index dc6fe66f07..e87b61d6fa 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
@@ -81,6 +86,24 @@
5.0.1-SNAPSHOT
+
+ org.phoebus
+ core-websocket
+ 5.0.1-SNAPSHOT
+ test
+
+
+
+ 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/persistence/dao/NodeDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java
index cf4601bcbd..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
@@ -57,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
*/
@@ -72,7 +73,9 @@ 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.
+ * {@link Node}s. Client may use this to trigger a refresh of the UI.
*/
void deleteNodes(List nodeIds);
@@ -87,7 +90,6 @@ public interface NodeDAO {
/**
- *
* @param uniqueNodeId Valid {@link Node} id.
* @return The parent {@link Node}
*/
@@ -109,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);
/**
@@ -134,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);
@@ -196,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.
*/
@@ -204,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.
*/
@@ -221,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.
*/
@@ -244,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.
*/
@@ -263,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.
@@ -283,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.
*/
@@ -292,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.
*/
@@ -307,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}
*/
@@ -319,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);
@@ -330,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
*/
@@ -337,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 df239e71cc..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
@@ -141,7 +141,7 @@ public void deleteNode(String nodeId) {
*/
@Override
public void deleteNodes(List nodeIds) {
- List nodes = new ArrayList<>();
+ List nodesToDelete = new ArrayList<>();
for (String nodeId : nodeIds) {
Node nodeToDelete = getNode(nodeId);
if (nodeToDelete == null) {
@@ -149,9 +149,11 @@ public void 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(this::deleteNode);
}
@Override
@@ -336,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
@@ -344,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");
@@ -424,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;
}
/**
@@ -440,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
@@ -466,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) {
@@ -635,7 +641,6 @@ private void resolvePath(String nodeId, List pathElements) {
resolvePath(parent.getUniqueId(), pathElements);
}
-
private void deleteNode(Node nodeToDelete) {
for (Node node : getChildNodes(nodeToDelete.getUniqueId())) {
deleteNode(node);
@@ -662,7 +667,6 @@ private void deleteNode(Node nodeToDelete) {
// Delete the node
elasticsearchTreeRepository.deleteById(nodeToDelete.getUniqueId());
-
}
@Override
@@ -699,12 +703,14 @@ 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(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());
@@ -755,12 +761,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) {
@@ -1223,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();
}
@@ -1244,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/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/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/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 c769e99fbd..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
@@ -21,7 +21,10 @@
import org.phoebus.applications.saveandrestore.model.Configuration;
import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
+import org.phoebus.applications.saveandrestore.model.websocket.MessageType;
+import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage;
import org.phoebus.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;
@@ -46,6 +49,10 @@ public class ConfigurationController extends BaseController {
@Autowired
private NodeDAO nodeDAO;
+ @SuppressWarnings("unused")
+ @Autowired
+ private WebSocketHandler webSocketHandler;
+
/**
* Creates new {@link Configuration} {@link Node}.
* @param parentNodeId Valid id of the {@link Node}s intended parent.
@@ -71,7 +78,9 @@ public Configuration createConfiguration(@RequestParam(value = "parentNodeId") S
}
}
configuration.getConfigurationNode().setUserName(principal.getName());
- return nodeDAO.createConfiguration(parentNodeId, configuration);
+ Configuration newConfiguration = nodeDAO.createConfiguration(parentNodeId, configuration);
+ webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newConfiguration.getConfigurationNode().getUniqueId()));
+ return newConfiguration;
}
/**
@@ -97,6 +106,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/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));
}
}
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 cf385aaae4..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
@@ -20,7 +20,10 @@
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
import org.phoebus.applications.saveandrestore.model.Tag;
+import org.phoebus.applications.saveandrestore.model.websocket.MessageType;
+import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage;
import org.phoebus.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;
@@ -47,6 +50,9 @@ public class NodeController extends BaseController {
@Autowired
private NodeDAO nodeDAO;
+ @Autowired
+ private WebSocketHandler webSocketHandler;
+
/**
* Create a new folder in the tree structure.
@@ -77,7 +83,10 @@ public Node createNode(@RequestParam(name = "parentNodeId") String parentsUnique
throw new IllegalArgumentException("Node may not contain golden tag");
}
node.setUserName(principal.getName());
- return nodeDAO.createNode(parentsUniqueId, node);
+ Node savedNode = nodeDAO.createNode(parentsUniqueId, node);
+ webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage<>(MessageType.NODE_ADDED,
+ savedNode.getUniqueId()));
+ return savedNode;
}
/**
@@ -149,6 +158,8 @@ public List getChildNodes(@PathVariable final String uniqueNodeId) {
@PreAuthorize("@authorizationHelper.mayDelete(#nodeIds, #root)")
public void deleteNodes(@RequestBody List nodeIds) {
nodeDAO.deleteNodes(nodeIds);
+ nodeIds.forEach(id ->
+ webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, id)));
}
/**
@@ -209,7 +220,9 @@ public Node updateNode(@RequestParam(value = "customTimeForMigration", required
throw new IllegalArgumentException("Node may not contain golden tag");
}
nodeToUpdate.setUserName(principal.getName());
- return nodeDAO.updateNode(nodeToUpdate, Boolean.parseBoolean(customTimeForMigration));
+ Node updatedNode = nodeDAO.updateNode(nodeToUpdate, Boolean.parseBoolean(customTimeForMigration));
+ webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedNode));
+ return updatedNode;
}
/**
diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java
index 68906377de..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
@@ -21,7 +21,10 @@
import org.phoebus.applications.saveandrestore.model.NodeType;
import org.phoebus.applications.saveandrestore.model.Snapshot;
import org.phoebus.applications.saveandrestore.model.SnapshotData;
+import org.phoebus.applications.saveandrestore.model.websocket.MessageType;
+import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage;
import org.phoebus.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,8 @@ public class SnapshotController extends BaseController {
@Autowired
private NodeDAO nodeDAO;
+ @Autowired
+ private WebSocketHandler webSocketHandler;
/**
*
* @param uniqueId Unique {@link Node} id of a snapshot.
@@ -74,7 +79,9 @@ public Snapshot createSnapshot(@RequestParam(value = "parentNodeId") String pare
throw new IllegalArgumentException("Snapshot node of wrong type");
}
snapshot.getSnapshotNode().setUserName(principal.getName());
- return nodeDAO.createSnapshot(parentNodeId, snapshot);
+ Snapshot newSnapshot = nodeDAO.createSnapshot(parentNodeId, snapshot);
+ webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_ADDED, newSnapshot.getSnapshotNode().getUniqueId()));
+ return newSnapshot;
}
/**
@@ -91,6 +98,8 @@ public Snapshot updateSnapshot(@RequestBody Snapshot snapshot,
throw new IllegalArgumentException("Snapshot node of wrong type");
}
snapshot.getSnapshotNode().setUserName(principal.getName());
- return nodeDAO.updateSnapshot(snapshot);
+ Snapshot updatedSnapshot = nodeDAO.updateSnapshot(snapshot);
+ webSocketHandler.sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_UPDATED, updatedSnapshot.getSnapshotNode()));
+ return updatedSnapshot;
}
}
diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java
index 4bfc3e1cc8..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
@@ -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;
}
/**
@@ -84,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/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;
}
}
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..4d1458aaf0
--- /dev/null
+++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocket.java
@@ -0,0 +1,173 @@
+/*******************************************************************************
+ * Copyright (c) 2019-2022 UT-Battelle, LLC.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the LICENSE
+ * which accompanies this distribution
+ ******************************************************************************/
+package org.phoebus.service.saveandrestore.websocket;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+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 {
+
+ /**
+ * Is the queue full?
+ */
+ private final AtomicBoolean stuffed = new AtomicBoolean();
+
+ /**
+ * Queue of messages for the client.
+ *
+ * Multiple threads concurrently writing to the socket results in
+ * IllegalStateException "remote endpoint was in state [TEXT_FULL_WRITING]"
+ * All writes are thus performed by just one thread off this queue.
+ */
+ private final ArrayBlockingQueue writeQueue = new ArrayBlockingQueue<>(2048);
+
+ private static final String EXIT_MESSAGE = "EXIT";
+
+ private final WebSocketSession session;
+ private final String id;
+
+ private final Logger logger = Logger.getLogger(WebSocket.class.getName());
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Constructor
+ */
+ public WebSocket(ObjectMapper objectMapper, WebSocketSession webSocketSession) {
+ this.session = webSocketSession;
+ logger.log(Level.INFO, () -> "Creating web socket " + session.getUri() + " ID " + session.getId());
+ this.objectMapper = objectMapper;
+ this.id = webSocketSession.getId();
+ Thread writeThread = new Thread(this::writeQueuedMessages, "Web Socket Write Thread");
+ writeThread.setName("Web Socket Write Thread " + this.id);
+ writeThread.setDaemon(true);
+ writeThread.start();
+ }
+
+ /**
+ * @return Session ID
+ */
+ public String getId() {
+ if (session == null)
+ return "(" + id + ")";
+ else
+ return id;
+ }
+
+ /**
+ * @param message Potentially long message
+ * @return Message shorted to 200 chars
+ */
+ private String shorten(final String message) {
+ if (message == null || message.length() < 200)
+ return message;
+ return message.substring(0, 200) + " ...";
+ }
+
+ public void queueMessage(final String message) {
+ // Ignore messages after 'dispose'
+ if (session == null)
+ return;
+
+ if (writeQueue.offer(message)) { // Queued OK. Is this a recovery from stuffed queue?
+ if (stuffed.getAndSet(false))
+ logger.log(Level.WARNING, () -> "Un-stuffed message queue for " + id);
+ } else { // Log, but only for the first message to prevent flooding the log
+ if (!stuffed.getAndSet(true))
+ logger.log(Level.WARNING, () -> "Cannot queue message '" + shorten(message) + "' for " + id);
+ }
+ }
+
+ private void writeQueuedMessages() {
+ try {
+ while (true) {
+ final String message;
+ try {
+ message = writeQueue.take();
+ } catch (final InterruptedException ex) {
+ return;
+ }
+
+ // Check if we should exit the thread
+ if (message.equals(EXIT_MESSAGE)) {
+ logger.log(Level.FINE, () -> "Exiting write thread " + id);
+ return;
+ }
+
+ final WebSocketSession safeSession = session;
+ try {
+ if (safeSession == null)
+ throw new Exception("No session");
+ if (!safeSession.isOpen())
+ throw new Exception("Session closed");
+ safeSession.sendMessage(new TextMessage(message));
+ } catch (final Exception ex) {
+ logger.log(Level.WARNING, ex, () -> "Cannot write '" + shorten(message) + "' for " + id);
+
+ // Clear queue
+ String drop = writeQueue.take();
+ while (drop != null) {
+ if (drop.equals(EXIT_MESSAGE)) {
+ logger.log(Level.FINE, () -> "Exiting write thread " + id);
+ return;
+ }
+ drop = writeQueue.take();
+ }
+ }
+ }
+ } catch (Throwable ex) {
+ logger.log(Level.WARNING, "Write thread error for " + id, ex);
+ }
+ }
+
+ /**
+ * Called when client sends a 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();
+ logger.log(Level.INFO, "Client message type: " + type);
+ }
+
+ /**
+ * 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
+ writeQueue.clear();
+ queueMessage(EXIT_MESSAGE);
+ // TODO: is this needed?
+ session.close();
+ } catch (Throwable ex) {
+ logger.log(Level.WARNING, "Error disposing " + getId(), ex);
+ }
+ logger.log(Level.INFO, () -> "Web socket " + session.getId() + " closed");
+ }
+}
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..2bcb98a0da
--- /dev/null
+++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/websocket/WebSocketHandler.java
@@ -0,0 +1,173 @@
+/*
+ * 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.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.NonNull;
+import org.springframework.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.ArrayList;
+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")
+ private List sockets = new ArrayList<>();
+
+ @SuppressWarnings("unused")
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @SuppressWarnings("unused")
+
+ private final Logger logger = Logger.getLogger(WebSocketHandler.class.getName());
+
+ /**
+ * Handles text message from web socket client
+ *
+ * @param session The {@link WebSocketSession} associated with the remote client.
+ * @param message Message sent by client
+ */
+ @Override
+ public void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) {
+ try {
+ // Find the WebSocket instance associated with the WebSocketSession
+ Optional webSocketOptional =
+ 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) {
+ logger.log(Level.INFO, "Opening web socket session from remote " + session.getRemoteAddress().getAddress());
+ WebSocket webSocket = new WebSocket(objectMapper, session);
+ sockets.add(webSocket);
+ }
+
+ /**
+ * Called when web socket is closed. Depending on the web browser, {@link #handleTransportError(WebSocketSession, Throwable)}
+ * may be called first.
+ *
+ * @param session Associated {@link WebSocketSession}
+ * @param status See {@link CloseStatus}
+ */
+ @Override
+ public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
+ Optional webSocketOptional =
+ 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());
+ }
+ }
+
+ /**
+ * 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.INFO, "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?
+ }
+ }
+
+ /**
+ * @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();
+ });
+ }
+
+ public void sendMessage(SaveAndRestoreWebSocketMessage webSocketMessage) {
+ sockets.forEach(ws -> {
+ try {
+ ws.queueMessage(objectMapper.writeValueAsString(webSocketMessage));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+}
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 c16a335dcd..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
@@ -22,18 +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.AfterEach;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -46,9 +63,20 @@
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.
@@ -56,7 +84,7 @@
*/
@TestInstance(Lifecycle.PER_CLASS)
@SpringBootTest
-@ContextConfiguration(classes = ElasticConfig.class)
+@ContextConfiguration(classes = {ElasticConfig.class})
@TestPropertySource(locations = "classpath:test_application.properties")
@Profile("IT")
@SuppressWarnings("unused")
@@ -65,9 +93,6 @@ public class DAOTestIT {
@Autowired
private ElasticsearchDAO nodeDAO;
- @Autowired
- private ConfigurationDataRepository configurationDataRepository;
-
@Autowired
private ElasticsearchClient client;
@@ -89,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
@@ -102,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
@@ -116,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
@@ -166,8 +194,6 @@ public void testDeleteConfigurationAndPvs() {
// This should not throw exception
nodeDAO.createNode(topLevelFolderNode.getUniqueId(), config);
-
- clearAllData();
}
@@ -215,7 +241,6 @@ public void testGetNodeAsConfig() {
Node configFromDB = nodeDAO.getNode(newConfig.getUniqueId());
assertEquals(newConfig.getUniqueId(), configFromDB.getUniqueId());
- clearAllData();
}
@Test
@@ -252,7 +277,6 @@ public void testGetConfigForSnapshot() {
assertNotNull(config);
- clearAllData();
}
@Test
@@ -297,8 +321,6 @@ public void testDeleteSnapshotReferencedInCompositeSnapshot() {
assertThrows(RuntimeException.class, () -> nodeDAO.deleteNode(snapshotNode.getUniqueId()));
nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId());
-
- clearAllData();
}
@Test
@@ -380,7 +402,6 @@ public void testUpdateCompositeSnapshot() {
nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId());
- clearAllData();
}
@Test
@@ -434,8 +455,6 @@ public void testGetAllCompositeSnapshotData() {
compositeSnapshotNodeIds.forEach(id -> nodeDAO.deleteNode(id));
- clearAllData();
-
}
@Test
@@ -486,7 +505,6 @@ public void testTakeSnapshot() {
snapshots = nodeDAO.getSnapshots(config.getUniqueId());
assertTrue(snapshots.isEmpty());
- clearAllData();
}
@Test
@@ -544,7 +562,6 @@ public void testUpdateSnapshot() {
assertEquals("other snapshot name", snapshotNode.getName());
assertEquals("other comment", snapshotNode.getDescription());
- clearAllData();
}
@Test
@@ -603,7 +620,6 @@ public void testGetSnapshotItemsWithNullPvValues() {
assertNull(snapshot1.getSnapshotData().getSnapshotItems());
- clearAllData();
}
@Test
@@ -679,7 +695,6 @@ public void testSnapshotTag() {
tagList2 = n2.getTags();
assertFalse(tagList2.stream().anyMatch(t -> t.getName().equals(newTag.getName())));
- clearAllData();
}
@Test
@@ -701,7 +716,6 @@ public void testGetChildNodes() {
List childNodes = nodeDAO.getChildNodes(rootNode.getUniqueId());
assertTrue(nodeDAO.getChildNodes(folder1.getUniqueId()).isEmpty());
- clearAllData();
}
@Test
@@ -722,7 +736,6 @@ public void testUpdateNode() {
assertEquals("folderB", folderA.getName());
assertEquals(uniqueId, folderA.getUniqueId());
- clearAllData();
}
@@ -824,8 +837,6 @@ public void testUpdateNodeType() {
Node node = folderNode;
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.updateNode(node, false));
-
- clearAllData();
}
@@ -842,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
@@ -854,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
@@ -877,7 +885,6 @@ public void testUpdateNodeNewNameInvalid() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.updateNode(node, false));
- clearAllData();
}
@Test
@@ -886,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
@@ -914,8 +920,6 @@ public void testCreateFolderInConfigNode() {
Node node = configNode;
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.createNode(node.getUniqueId(), folderNode));
-
- clearAllData();
}
@Test
@@ -936,8 +940,6 @@ public void testCreateConfigInConfigNode() {
Node node = configNode;
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.createNode(node.getUniqueId(), node));
-
- clearAllData();
}
@Test
@@ -971,8 +973,6 @@ public void testCreateNameClash() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.createNode(rootNode.getUniqueId(), anotherConfig));
-
- clearAllData();
}
@Test
@@ -1008,8 +1008,6 @@ public void testFindParentFromPathElements() {
found = nodeDAO.findParentFromPathElements(rootNode, "/a/d/c".split("/"), 1);
assertNull(found);
-
- clearAllData();
}
@Test
@@ -1022,8 +1020,6 @@ public void testGetFromPathTwoNodes() {
List nodes = nodeDAO.getFromPath("/a/b/c");
assertEquals(2, nodes.size());
-
- clearAllData();
}
@Test
@@ -1044,8 +1040,6 @@ public void testGetFromPathOneNode() {
nodes = nodeDAO.getFromPath("/a");
assertEquals(1, nodes.size());
assertEquals(a.getUniqueId(), nodes.get(0).getUniqueId());
-
- clearAllData();
}
@Test
@@ -1060,8 +1054,6 @@ public void testGetFromPathZeroNodes() {
nodes = nodeDAO.getFromPath("/a/x/c");
assertNull(nodes);
-
- clearAllData();
}
@Test
@@ -1091,8 +1083,6 @@ public void testGetFullPathNonExistingNode() {
// This will throw NodeNotFoundException
assertThrows(NodeNotFoundException.class,
() -> nodeDAO.getFullPath("nonExisting"));
-
- clearAllData();
}
@Test
@@ -1103,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
@@ -1115,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
@@ -1137,8 +1123,6 @@ public void testMoveNodesInvalidId() {
assertThrows(NodeNotFoundException.class,
() -> nodeDAO.moveNodes(nodeIds, rootNode.getUniqueId(), "userName"));
-
- clearAllData();
}
@Test
@@ -1167,8 +1151,6 @@ public void testMoveConfiguration() {
assertEquals(1, nodeDAO.getChildNodes(topLevelFolderNode2.getUniqueId()).size());
assertEquals(0, nodeDAO.getChildNodes(topLevelFolderNode.getUniqueId()).size());
-
- clearAllData();
}
@Test
@@ -1197,8 +1179,6 @@ public void testMoveConfigurationNameClash() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.moveNodes(List.of(_configNode.getUniqueId()), topLevelFolderNode2.getUniqueId(), "user"));
-
- clearAllData();
}
@Test
@@ -1222,8 +1202,6 @@ public void testMoveSnasphotToRoot() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.moveNodes(List.of(uniqueId), rootNode.getUniqueId(),
"user"));
-
- clearAllData();
}
@Test
@@ -1247,8 +1225,6 @@ public void testMoveSnasphotToConfiguration() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.moveNodes(List.of(uniqueId), rootNode.getUniqueId(),
"user"));
-
- clearAllData();
}
@Test
@@ -1285,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
@@ -1301,8 +1275,6 @@ public void testCopyFolderToSameParent() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(List.of(uniqueId), rootNode.getUniqueId(),
"username"));
-
- clearAllData();
}
@Test
@@ -1320,8 +1292,6 @@ public void testCopyConfigToSameParent() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(List.of(uniqueId), rootNode.getUniqueId(),
"username"));
-
- clearAllData();
}
@Test
@@ -1342,8 +1312,6 @@ public void testCopyFolderNotSupported() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(List.of(_childFolderNode.getUniqueId()), rootNode.getUniqueId(), "username"));
-
- clearAllData();
}
@Test
@@ -1375,8 +1343,6 @@ public void testCopyConfigToOtherParent() {
List childNodes = nodeDAO.getChildNodes(rootNode.getUniqueId());
assertEquals(2, childNodes.size());
-
- clearAllData();
}
@Test
@@ -1403,8 +1369,6 @@ public void testCopyMultipleFolders() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(Arrays.asList(f1, f2), rootNode.getUniqueId(), "username"));
-
- clearAllData();
}
@Test
@@ -1431,8 +1395,6 @@ public void testCopyFolderAndConfig() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(Arrays.asList(folderUniqueId,
configUniqueId), rootNode.getUniqueId(), "username"));
-
- clearAllData();
}
@Test
@@ -1474,8 +1436,6 @@ public void testCopySnapshotToFolderNotSupported() {
String uniqueId = folderNode1.getUniqueId();
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(List.of(snapshotId), uniqueId, "username"));
-
- clearAllData();
}
@Test
@@ -1516,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
@@ -1567,8 +1525,6 @@ public void testCopySnapshotToConfigurationPvListMismatch() {
assertThrows(IllegalArgumentException.class,
() -> nodeDAO.copyNodes(List.of(snapshotId), config2Id, "userName"));
-
- clearAllData();
}
@Test
@@ -1617,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
@@ -1692,8 +1647,6 @@ public void testCopyCompositeSnapshotToConfiguration() {
() -> nodeDAO.copyNodes(List.of(compositeSnapshotId), config2Id, "user"));
nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId());
-
- clearAllData();
}
@Test
@@ -1769,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
@@ -1812,8 +1763,6 @@ public void testGetAllTags() {
List tags = nodeDAO.getAllTags();
assertEquals(4, tags.size());
-
- clearAllData();
}
@Test
@@ -1853,8 +1802,6 @@ public void testGetAllSnapshots() {
List snapshotNodes = nodeDAO.getAllSnapshots();
assertEquals(1, snapshotNodes.size());
-
- clearAllData();
}
@Test
@@ -1872,8 +1819,6 @@ public void testGetAllNodes() {
List nodes = nodeDAO.getNodes(Arrays.asList(folderNode1.getUniqueId(), folderNode2.getUniqueId()));
assertEquals(2, nodes.size());
-
- clearAllData();
}
/**
@@ -1883,7 +1828,6 @@ private void clearAllData() {
List childNodes = nodeDAO.getChildNodes(Node.ROOT_FOLDER_UNIQUE_ID);
childNodes.forEach(node -> nodeDAO.deleteNode(node.getUniqueId()));
nodeDAO.deleteAllFilters();
-
}
@@ -2030,8 +1974,6 @@ public void testCheckForPVNameDuplicates() {
assertEquals("pv1", duplicates.get(0));
nodeDAO.deleteNode(compositeSnapshotNode.getUniqueId());
-
- clearAllData();
}
@Test
@@ -2116,8 +2058,6 @@ public void testCheckForRejectedReferencedNodesInCompositeSnapshot() {
compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData);
assertFalse(nodeDAO.checkCompositeSnapshotReferencedNodeTypes(compositeSnapshot));
-
- clearAllData();
}
@Test
@@ -2200,8 +2140,6 @@ public void testGetSnapshotItemsFromCompositeSnapshot() {
assertEquals(4, snapshotItems.size());
nodeDAO.deleteNode(compositeSnapshotNode.getUniqueId());
-
- clearAllData();
}
@Test
@@ -2254,12 +2192,10 @@ public void testFilters() {
unformattedQueryStringFilter = nodeDAO.saveFilter(unformattedQueryStringFilter);
assertTrue(unformattedQueryStringFilter.getQueryString().contains("type=Folder,Configuration"));
assertFalse(unformattedQueryStringFilter.getQueryString().contains("unsupoorted"));
-
- clearAllData();
}
@Test
- public void testSearchForPvs() {
+ public void testSearchForPvs() {
Node rootNode = nodeDAO.getRootNode();
Node folderNode =
Node.builder().name("folder").build();
@@ -2316,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/config/ControllersTestConfig.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java
index 588f8c2c9a..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
@@ -27,6 +27,7 @@
import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.FilterRepository;
import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.SnapshotDataRepository;
import org.phoebus.service.saveandrestore.search.SearchUtil;
+import org.phoebus.service.saveandrestore.websocket.WebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
@@ -34,6 +35,7 @@
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.util.Base64Utils;
+import org.springframework.web.socket.WebSocketSession;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -129,4 +131,15 @@ public ExecutorService executorService() {
public SnapshotUtil snapshotUtil() {
return new SnapshotUtil();
}
+
+ @Bean
+ public WebSocketSession webSocketSession(){
+ return Mockito.mock(WebSocketSession.class);
+ }
+
+ @Bean
+ public WebSocketHandler webSocketHandler(){
+ return Mockito.mock(WebSocketHandler.class);
+ }
+
}
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);
}
}
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 bf355ff07f..bc114aa464 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,17 +20,26 @@
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.Comparison;
import org.phoebus.applications.saveandrestore.model.ConfigPv;
+
import org.phoebus.applications.saveandrestore.model.Configuration;
import org.phoebus.applications.saveandrestore.model.ConfigurationData;
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
+
+import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage;
+
import org.phoebus.applications.saveandrestore.model.ComparisonMode;
+
import org.phoebus.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;
@@ -43,6 +52,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.post;
@@ -76,10 +87,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());
@@ -93,26 +110,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();
@@ -127,51 +174,97 @@ 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());
+ }
+
+ @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);
- request = post("/config")
+ 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));
+ mockMvc.perform(request).andExpect(status().isUnauthorized());
}
@Test
public void testCreateInvalidConfiguration() throws Exception {
- reset(nodeDAO);
-
Configuration configuration = new Configuration();
configuration.setConfigurationNode(Node.builder().build());
ConfigurationData configurationData = new ConfigurationData();
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 37fc550b12..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
@@ -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;
@@ -33,9 +34,12 @@
import org.phoebus.applications.saveandrestore.model.Node;
import org.phoebus.applications.saveandrestore.model.NodeType;
import org.phoebus.applications.saveandrestore.model.Tag;
+import org.phoebus.applications.saveandrestore.model.websocket.MessageType;
+import org.phoebus.applications.saveandrestore.model.websocket.SaveAndRestoreWebSocketMessage;
import org.phoebus.service.saveandrestore.NodeNotFoundException;
import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO;
import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig;
+import org.phoebus.service.saveandrestore.websocket.WebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
@@ -49,9 +53,12 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.reset;
+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.*;
@@ -77,6 +84,9 @@ public class NodeControllerTest {
@Autowired
private MockMvc mockMvc;
+ @Autowired
+ private WebSocketHandler webSocketHandler;
+
private static Node folderFromClient;
private static Node config1;
@@ -107,6 +117,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 {
@@ -161,8 +176,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();
@@ -273,7 +286,6 @@ public void testCreateConfigWithBadToleranceData2() throws Exception {
@Test
public void testUpdateConfig() throws Exception {
- reset(nodeDAO);
Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh")
.userName("user").build();
@@ -328,7 +340,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();
@@ -349,7 +360,6 @@ public void testCreateNodeBadRequests() throws Exception {
@Test
public void testGetChildNodes() throws Exception {
- reset(nodeDAO);
when(nodeDAO.getChildNodes("p")).thenAnswer((Answer>) invocation -> Collections.singletonList(config1));
@@ -367,7 +377,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);
@@ -419,6 +428,7 @@ public void testDeleteFolder() throws Exception {
mockMvc.perform(request).andExpect(status().isUnauthorized());
when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build());
+ when(nodeDAO.getParentNode("a")).thenReturn(Node.builder().uniqueId("b").build());
request =
delete("/node")
@@ -426,62 +436,115 @@ public void testDeleteFolder() throws Exception {
.header(HttpHeaders.AUTHORIZATION, userAuthorization);
mockMvc.perform(request).andExpect(status().isOk());
+ verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class));
+ }
+
+ @Test
+ public void testDeleteForbiddenAccess() throws Exception {
+
when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName("notDemoUser").build());
- request =
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, userAuthorization);
mockMvc.perform(request).andExpect(status().isForbidden());
+ verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b"));
+ }
+
+ @Test
+ 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());
- request =
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization);
mockMvc.perform(request).andExpect(status().isForbidden());
+ 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());
- request =
+ verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b"));
+
+ }
+
+ @Test
+ 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"));
+
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, userAuthorization);
mockMvc.perform(request).andExpect(status().isOk());
+ verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class));
+
+ }
+
+ @Test
+ 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.getChildNodes("a")).thenReturn(Collections.emptyList());
- request =
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, userAuthorization);
mockMvc.perform(request).andExpect(status().isOk());
+ verify(webSocketHandler, times(1)).sendMessage(Mockito.any(SaveAndRestoreWebSocketMessage.class));
+ }
+
+ @Test
+ public void testDeleteForbidden3() throws Exception{
+
when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build());
when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build()));
- request =
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, userAuthorization);
mockMvc.perform(request).andExpect(status().isForbidden());
+ verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b"));
+ }
+
+ @Test
+ public void testDeleteForbidden4() throws Exception{
+
when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.FOLDER).userName(demoUser).build());
when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build()));
- request =
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, userAuthorization);
mockMvc.perform(request).andExpect(status().isForbidden());
+ verify(webSocketHandler, times(0)).sendMessage(new SaveAndRestoreWebSocketMessage(MessageType.NODE_REMOVED, "b"));
+
+ }
+
+ @Test
+ public void testDeleteFolder5() throws Exception{
+
when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build());
when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build()));
- request =
+ MockHttpServletRequestBuilder request =
delete("/node")
.contentType(JSON).content(objectMapper.writeValueAsString(List.of("a")))
.header(HttpHeaders.AUTHORIZATION, adminAuthorization);
@@ -513,8 +576,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");
@@ -528,7 +589,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");
@@ -540,8 +600,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");
@@ -570,8 +628,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);
@@ -726,7 +782,6 @@ public void testCreateNodeWithValidTags1() throws Exception {
mockMvc.perform(request).andExpect(status().isOk());
- reset(nodeDAO);
}
@Test
@@ -750,6 +805,5 @@ public void testCreateNodeWithValidTags2() throws Exception {
mockMvc.perform(request).andExpect(status().isOk());
- reset(nodeDAO);
}
}
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..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
@@ -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));
}
}
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));
}
}