diff --git a/app/save-and-restore/app/pom.xml b/app/save-and-restore/app/pom.xml index af34067afc..7d73eb67b4 100644 --- a/app/save-and-restore/app/pom.xml +++ b/app/save-and-restore/app/pom.xml @@ -9,6 +9,10 @@ save-and-restore + + true + + @@ -43,18 +47,6 @@ ${jgit.version} - - - com.sun.jersey - jersey-core - 1.19 - - - com.sun.jersey - jersey-client - 1.19 - - com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider @@ -96,5 +88,24 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + ${skipITs} + + **/*IT.java + + + + diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java index 4fdf741a7a..cc81f3c8d8 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java @@ -55,11 +55,11 @@ public interface SaveAndRestoreClient { List getCompositeSnapshotItems(String uniqueNodeId); /** - * @param unqiueNodeId Unique id of a {@link Node} + * @param uniqueNodeId Unique id of a {@link Node} * @return The parent {@link Node} of the specified id. May be null if the unique id is associated with the root * {@link Node} */ - Node getParentNode(String unqiueNodeId); + Node getParentNode(String uniqueNodeId); /** * @param uniqueNodeId Id of an existing {@link Node} @@ -107,10 +107,6 @@ public interface SaveAndRestoreClient { */ List getAllTags(); - /** - * @return All snapshot {@link Node}s persisted on the remote service - */ - List getAllSnapshots(); /** * Move a set of {@link Node}s to a new parent {@link Node} @@ -130,10 +126,13 @@ public interface SaveAndRestoreClient { */ Node copyNodes(List sourceNodeIds, String targetNodeId); + /** + * Constructs a path like string to facilitate location of a {@link Node} in the tree structure. + * @param uniqueNodeId Unique id + * @return Path like /Root folder/foo/bar/my/favourite/node + */ String getFullPath(String uniqueNodeId); - List getFromPath(String path); - ConfigurationData getConfigurationData(String nodeId); /** @@ -151,6 +150,12 @@ public interface SaveAndRestoreClient { SnapshotData getSnapshotData(String uniqueId); + /** + * Creates a {@link Snapshot} + * @param parentNodeId The unique id of the configuration {@link Node} associated with the {@link Snapshot} + * @param snapshot The {@link Snapshot} data object. + * @return The new {@link Snapshot} as persisted by the service + */ Snapshot createSnapshot(String parentNodeId, Snapshot snapshot); Snapshot updateSnapshot(Snapshot snapshot); 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 new file mode 100644 index 0000000000..e9b99b8e70 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java @@ -0,0 +1,714 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.client; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.phoebus.applications.saveandrestore.Messages; +import org.phoebus.applications.saveandrestore.SaveAndRestoreClientException; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; +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.RestoreResult; +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.UserData; +import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.applications.saveandrestore.model.search.SearchResult; +import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; +import org.phoebus.security.tokens.ScopedAuthenticationToken; + +import javax.ws.rs.core.MultivaluedMap; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SaveAndRestoreClientImpl implements SaveAndRestoreClient { + + private static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8"; + private static final Logger LOGGER = Logger.getLogger(SaveAndRestoreClientImpl.class.getName()); + + private static final int DEFAULT_READ_TIMEOUT = 5000; // ms + private static final int DEFAULT_CONNECT_TIMEOUT = 5000; // ms + + private static final ObjectMapper OBJECT_MAPPER; + + private static final HttpClient CLIENT; + + static { + int httpClientReadTimeout = Preferences.httpClientReadTimeout > 0 ? Preferences.httpClientReadTimeout : DEFAULT_READ_TIMEOUT; + LOGGER.log(Level.INFO, "Save&restore client using read timeout " + httpClientReadTimeout + " ms"); + + int httpClientConnectTimeout = Preferences.httpClientConnectTimeout > 0 ? Preferences.httpClientConnectTimeout : DEFAULT_CONNECT_TIMEOUT; + LOGGER.log(Level.INFO, "Save&restore client using connect timeout " + httpClientConnectTimeout + " ms"); + CookieHandler.setDefault(new CookieManager()); + + CLIENT = HttpClient.newBuilder() + .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL)) + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofMillis(httpClientConnectTimeout)) + .build(); + OBJECT_MAPPER = new ObjectMapper(); + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + private String getBasicAuthenticationHeader() { + try { + SecureStore store = new SecureStore(); + ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE); + if (scopedAuthenticationToken != null) { + String username = scopedAuthenticationToken.getUsername(); + String password = scopedAuthenticationToken.getPassword(); + return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to retrieve credentials from secure store", e); + } + return null; + } + + @Override + public String getServiceUrl() { + return Preferences.jmasarServiceUrl; + } + + @Override + public Node getRoot() { + return getNode(Node.ROOT_FOLDER_UNIQUE_ID); + } + + @Override + public Node getNode(String uniqueNodeId) { + return getCall("/node/" + uniqueNodeId, Node.class); + } + + @Override + public List getCompositeSnapshotReferencedNodes(String uniqueNodeId) { + return getCall("/composite-snapshot/" + uniqueNodeId + "/nodes", new TypeReference<>() { + }); + } + + @Override + public List getCompositeSnapshotItems(String uniqueNodeId) { + return getCall("/composite-snapshot/" + uniqueNodeId + "/items", new TypeReference<>() { + }); + } + + @Override + public Node getParentNode(String uniqueNodeId) { + return getCall("/node/" + uniqueNodeId + "/parent", Node.class); + } + + @Override + public List getChildNodes(String uniqueNodeId) throws SaveAndRestoreClientException { + return getCall("/node/" + uniqueNodeId + "/children", new TypeReference<>() { + }); + } + + /** + * {@inheritDoc} + * + * @param parentsUniqueId Unique id of the parent {@link Node} for the new {@link Node} + * @param node A {@link Node} object that should be created (=persisted). + * @return {@inheritDoc} + */ + @Override + public Node createNewNode(String parentsUniqueId, Node node) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/node?parentNodeId=" + parentsUniqueId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .PUT(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(node))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Node.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Node updateNode(Node nodeToUpdate) { + return updateNode(nodeToUpdate, false); + } + + @Override + public Node updateNode(Node nodeToUpdate, boolean customTimeForMigration) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/node?customTimeForMigration=" + customTimeForMigration)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(nodeToUpdate))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Node.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param nodeIds List of unique {@link Node} ids. + */ + @Override + public void deleteNodes(List nodeIds) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/node")) + .method("DELETE", HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(nodeIds))) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException("Failed to delete nodes: " + response.body()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + */ + @Override + public List getAllTags() { + return getCall("/tags", new TypeReference<>() { + }); + } + + /** + * {@inheritDoc} + * + * @param sourceNodeIds List of unique {@link Node} ids. + * @param targetNodeId The unique id of the parent {@link Node} to which the source {@link Node}s are moved. + * @return + */ + @Override + public Node moveNodes(List sourceNodeIds, String targetNodeId) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/move?to=" + targetNodeId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(sourceNodeIds))) + .build(); + + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Node.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param sourceNodeIds List of unique {@link Node} ids. + * @param targetNodeId The unique id of the parent {@link Node} to which the source {@link Node}s are copied. + * @return + */ + @Override + public Node copyNodes(List sourceNodeIds, String targetNodeId) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/copy?to=" + targetNodeId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(sourceNodeIds))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Node.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param uniqueNodeId Unique id + * @return + */ + @Override + public String getFullPath(String uniqueNodeId) { + return getCall("/path/" + uniqueNodeId, String.class); + } + + @Override + public ConfigurationData getConfigurationData(String nodeId) { + return getCall("/config/" + nodeId, ConfigurationData.class); + } + + /** + * {@inheritDoc} + * + * @param parentNodeId Non-null and non-empty unique id of an existing parent {@link Node}, + * which must be of type {@link org.phoebus.applications.saveandrestore.model.NodeType#FOLDER}. + * @param configuration {@link ConfigurationData} object + * @return + */ + @Override + public Configuration createConfiguration(String parentNodeId, Configuration configuration) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/config?parentNodeId=" + parentNodeId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .PUT(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(configuration))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Configuration.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param configuration + * @return + */ + @Override + public Configuration updateConfiguration(Configuration configuration) { + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/config")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(configuration))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Configuration.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public SnapshotData getSnapshotData(String uniqueId) { + return getCall("/snapshot/" + uniqueId, SnapshotData.class); + } + + /** + * {@inheritDoc} + * + * @param parentNodeId The unique id of the configuration {@link Node} associated with the {@link Snapshot} + * @param snapshot The {@link Snapshot} data object. + * @return {@inheritDoc} + */ + @Override + public Snapshot createSnapshot(String parentNodeId, Snapshot snapshot) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/snapshot?parentNodeId=" + parentNodeId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .PUT(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(snapshot))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Snapshot.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Snapshot updateSnapshot(Snapshot snapshot) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/snapshot")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(snapshot))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Snapshot.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param parentNodeId The parent {@link Node} for the new {@link CompositeSnapshot} + * @param compositeSnapshot The data object + * @return A {@link CompositeSnapshot} as persisted by the service. + */ + @Override + public CompositeSnapshot createCompositeSnapshot(String parentNodeId, CompositeSnapshot compositeSnapshot) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/composite-snapshot?parentNodeId=" + parentNodeId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .PUT(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(compositeSnapshot))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), CompositeSnapshot.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param snapshotNodeIds List of {@link Node} ids corresponding to {@link Node}s of types + * {@link org.phoebus.applications.saveandrestore.model.NodeType#SNAPSHOT} + * and {@link org.phoebus.applications.saveandrestore.model.NodeType#COMPOSITE_SNAPSHOT} + * @return + */ + @Override + public List checkCompositeSnapshotConsistency(List snapshotNodeIds) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/composite-snapshot-consistency-check")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(snapshotNodeIds))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), new TypeReference<>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public CompositeSnapshot updateCompositeSnapshot(CompositeSnapshot compositeSnapshot) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/composite-snapshot")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(compositeSnapshot))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), CompositeSnapshot.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public SearchResult search(MultivaluedMap searchParams) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/search?" + mapToQueryParams(searchParams))) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), SearchResult.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Filter saveFilter(Filter filter) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/filter")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .PUT(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(filter))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), Filter.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + */ + @Override + public List getAllFilters() { + return getCall("/filters", new TypeReference<>() { + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteFilter(String name) { + String filterName = name.replace(" ", "%20"); + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/filter/" + filterName)) + .DELETE() + .header("Authorization", getBasicAuthenticationHeader()) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException(response.body() != null ? response.body() : Messages.deleteFilterFailed); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param tagData see {@link TagData} + * @return + */ + @Override + public List addTag(TagData tagData) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/tags")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(tagData))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), new TypeReference<>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public List deleteTag(TagData tagData) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/tags")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .method("DELETE", HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(tagData))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), new TypeReference<>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param userName User's account name + * @param password User's password + * @return {@inheritDoc} + */ + @Override + public UserData authenticate(String userName, String password) { + String stringBuilder = Preferences.jmasarServiceUrl + + "/login?username=" + + userName + + "&password=" + + password; + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(stringBuilder)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + try { + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + return OBJECT_MAPPER.readValue(response.body(), UserData.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param snapshotItems A {@link List} of {@link SnapshotItem}s + * @return {@inheritDoc} + */ + @Override + public List restore(List snapshotItems) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/restore/items")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(snapshotItems))) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), new TypeReference<>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param snapshotNodeId Unique id of a snapshot + * @return {@inheritDoc} + */ + @Override + public List restore(String snapshotNodeId) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + "/restore/node?nodeId=" + snapshotNodeId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", getBasicAuthenticationHeader()) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new SaveAndRestoreClientException(response.body()); + } + return OBJECT_MAPPER.readValue(response.body(), new TypeReference<>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + * + * @param configurationNodeId The unique id of the {@link Configuration} for which to take the snapshot + * @return {@inheritDoc} + */ + @Override + public List takeSnapshot(String configurationNodeId) { + return getCall("/take-snapshot/" + configurationNodeId, new TypeReference<>() { + }); + } + + private T getCall(String relativeUrl, Class clazz) { + HttpResponse response = getCall(relativeUrl); + try { + return OBJECT_MAPPER.readValue(response.body(), clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private T getCall(String relativeUrl, TypeReference typeReference) { + HttpResponse response = getCall(relativeUrl); + try { + return OBJECT_MAPPER.readValue(response.body(), typeReference); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private HttpResponse getCall(String relativeUrl) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.jmasarServiceUrl + relativeUrl)) + .GET() + .build(); + try { + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + String responseBody = response.body(); + if (response.statusCode() != 200) { + if (responseBody == null || responseBody.isEmpty()) { + responseBody = "N/A"; + } + throw new SaveAndRestoreClientException("Failed : HTTP error code : " + response.statusCode() + ", error message: " + responseBody); + } + return response; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String mapToQueryParams(MultivaluedMap map) { + StringBuilder stringBuilder = new StringBuilder(); + map.keySet().forEach(k -> { + List value = map.get(k); + if (value != null && !value.isEmpty()) { + stringBuilder.append(k).append("="); + stringBuilder.append(String.join(",", value)); + stringBuilder.append("&"); + } + }); + return stringBuilder.toString(); + } +} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java deleted file mode 100644 index f9b1dc332c..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreJerseyClient.java +++ /dev/null @@ -1,716 +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.client; - -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; -import com.sun.jersey.api.client.*; -import com.sun.jersey.api.client.config.ClientConfig; -import com.sun.jersey.api.client.config.DefaultClientConfig; -import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; -import org.phoebus.applications.saveandrestore.Messages; -import org.phoebus.applications.saveandrestore.SaveAndRestoreClientException; -import org.phoebus.applications.saveandrestore.model.*; -import org.phoebus.applications.saveandrestore.model.search.Filter; -import org.phoebus.applications.saveandrestore.model.search.SearchResult; -import org.phoebus.security.store.SecureStore; -import org.phoebus.security.tokens.AuthenticationScope; -import org.phoebus.security.tokens.ScopedAuthenticationToken; - -import javax.ws.rs.core.MultivaluedMap; -import java.io.IOException; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class SaveAndRestoreJerseyClient implements org.phoebus.applications.saveandrestore.client.SaveAndRestoreClient { - - private static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8"; - private static final Logger logger = Logger.getLogger(SaveAndRestoreJerseyClient.class.getName()); - - private static final int DEFAULT_READ_TIMEOUT = 5000; // ms - private static final int DEFAULT_CONNECT_TIMEOUT = 5000; // ms - - private static final ObjectMapper mapper = new ObjectMapper(); - - /** - * Should be accessed through {@link #getClient()} to ensure proper usage of cached credentials, if available. - */ - private static final Client client; - - private static HTTPBasicAuthFilter httpBasicAuthFilter; - - static { - int httpClientReadTimeout = Preferences.httpClientReadTimeout > 0 ? Preferences.httpClientReadTimeout : DEFAULT_READ_TIMEOUT; - logger.log(Level.INFO, "Save&restore client using read timeout " + httpClientReadTimeout + " ms"); - - int httpClientConnectTimeout = Preferences.httpClientConnectTimeout > 0 ? Preferences.httpClientConnectTimeout : DEFAULT_CONNECT_TIMEOUT; - logger.log(Level.INFO, "Save&restore client using connect timeout " + httpClientConnectTimeout + " ms"); - - DefaultClientConfig defaultClientConfig = new DefaultClientConfig(); - defaultClientConfig.getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, httpClientReadTimeout); - defaultClientConfig.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, httpClientConnectTimeout); - - JacksonJsonProvider jacksonJsonProvider = new JacksonJsonProvider(mapper); - defaultClientConfig.getSingletons().add(jacksonJsonProvider); - - client = Client.create(defaultClientConfig); - } - - public SaveAndRestoreJerseyClient() { - mapper.registerModule(new JavaTimeModule()); - mapper.setSerializationInclusion(Include.NON_NULL); - } - - private Client getClient(){ - try { - SecureStore store = new SecureStore(); - ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE); - if (scopedAuthenticationToken != null) { - String username = scopedAuthenticationToken.getUsername(); - String password = scopedAuthenticationToken.getPassword(); - httpBasicAuthFilter = new HTTPBasicAuthFilter(username, password); - client.addFilter(httpBasicAuthFilter); - } else if (httpBasicAuthFilter != null) { - client.removeFilter(httpBasicAuthFilter); - } - } catch (Exception e) { - logger.log(Level.WARNING, "Unable to retrieve credentials from secure store", e); - } - return client; - } - - @Override - public String getServiceUrl() { - return Preferences.jmasarServiceUrl; - } - - @Override - public Node getRoot() { - return getCall("/node/" + Node.ROOT_FOLDER_UNIQUE_ID, Node.class); - } - - @Override - public Node getNode(String uniqueNodeId) { - return getCall("/node/" + uniqueNodeId, Node.class); - } - - @Override - public List getCompositeSnapshotReferencedNodes(String uniqueNodeId) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/composite-snapshot/" + uniqueNodeId + "/nodes"); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON).get(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - message = "N/A"; - } - throw new SaveAndRestoreClientException("Failed : HTTP error code : " + response.getStatus() + ", error message: " + message); - } - - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public List getCompositeSnapshotItems(String uniqueNodeId) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/composite-snapshot/" + uniqueNodeId + "/items"); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON).get(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - message = "N/A"; - } - throw new SaveAndRestoreClientException("Failed : HTTP error code : " + response.getStatus() + ", error message: " + message); - } - - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public Node getParentNode(String uniqueNodeId) { - return getCall("/node/" + uniqueNodeId + "/parent", Node.class); - } - - @Override - public List getChildNodes(String uniqueNodeId) throws SaveAndRestoreClientException { - ClientResponse response = getCall("/node/" + uniqueNodeId + "/children"); - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public Node createNewNode(String parentNodeId, Node node) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/node") - .queryParam("parentNodeId", parentNodeId); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(node, CONTENT_TYPE_JSON) - .put(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.createNodeFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(Node.class); - } - - @Override - public Node updateNode(Node nodeToUpdate) { - return updateNode(nodeToUpdate, false); - } - - @Override - public Node updateNode(Node nodeToUpdate, boolean customTimeForMigration) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/node") - .queryParam("customTimeForMigration", customTimeForMigration ? "true" : "false"); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(nodeToUpdate, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.updateNodeFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - - return response.getEntity(Node.class); - } - - private T getCall(String relativeUrl, Class clazz) { - ClientResponse response = getCall(relativeUrl); - return response.getEntity(clazz); - } - - private ClientResponse getCall(String relativeUrl) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + relativeUrl); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON).get(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - message = "N/A"; - } - throw new SaveAndRestoreClientException("Failed : HTTP error code : " + response.getStatus() + ", error message: " + message); - } - - return response; - } - - @Override - public void deleteNodes(List nodeIds) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/node"); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(nodeIds, CONTENT_TYPE_JSON) - .delete(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = response.getEntity(String.class); - throw new SaveAndRestoreClientException("Failed : HTTP error code : " + response.getStatus() + ", error message: " + message); - } - } - - @Override - public List getAllTags() { - ClientResponse response = getCall("/tags"); - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public List getAllSnapshots() { - ClientResponse response = getCall("/snapshots"); - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public Node moveNodes(List sourceNodeIds, String targetNodeId) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/move") - .queryParam("to", targetNodeId); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(sourceNodeIds, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.copyOrMoveNotAllowedBody; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(Node.class); - } - - @Override - public Node copyNodes(List sourceNodeIds, String targetNodeId) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/copy") - .queryParam("to", targetNodeId); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(sourceNodeIds, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.copyOrMoveNotAllowedBody; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(Node.class); - } - - @Override - public String getFullPath(String uniqueNodeId) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/path/" + uniqueNodeId); - ClientResponse response = webResource.get(ClientResponse.class); - - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - return null; - } - return response.getEntity(String.class); - } - - @Override - public List getFromPath(String path) { - return null; - } - - @Override - public ConfigurationData getConfigurationData(String nodeId) { - ClientResponse clientResponse = getCall("/config/" + nodeId); - return clientResponse.getEntity(ConfigurationData.class); - } - - @Override - public Configuration createConfiguration(String parentNodeId, Configuration configuration) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/config") - .queryParam("parentNodeId", parentNodeId); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(configuration, CONTENT_TYPE_JSON) - .put(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.createConfigurationFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(Configuration.class); - } - - @Override - public Configuration updateConfiguration(Configuration configuration) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/config"); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(configuration, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.updateConfigurationFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new RuntimeException(message); - } - return response.getEntity(Configuration.class); - } - - @Override - public SnapshotData getSnapshotData(String nodeId) { - ClientResponse clientResponse = getCall("/snapshot/" + nodeId); - return clientResponse.getEntity(SnapshotData.class); - } - - @Override - public Snapshot createSnapshot(String parentNodeId, Snapshot snapshot) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/snapshot") - .queryParam("parentNodeId", parentNodeId); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .entity(snapshot, CONTENT_TYPE_JSON) - .put(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.searchFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(Snapshot.class); - } - - @Override - public Snapshot updateSnapshot(Snapshot snapshot) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/snapshot"); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .entity(snapshot, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.searchFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(Snapshot.class); - } - - - @Override - public CompositeSnapshot createCompositeSnapshot(String parentNodeId, CompositeSnapshot compositeSnapshot) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/composite-snapshot") - .queryParam("parentNodeId", parentNodeId); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(compositeSnapshot, CONTENT_TYPE_JSON) - .put(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.createConfigurationFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(CompositeSnapshot.class); - } - - @Override - public List checkCompositeSnapshotConsistency(List snapshotNodeIds) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/composite-snapshot-consistency-check"); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(snapshotNodeIds, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.compositeSnapshotConsistencyCheckFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public CompositeSnapshot updateCompositeSnapshot(CompositeSnapshot compositeSnapshot) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/composite-snapshot"); - - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(compositeSnapshot, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.updateConfigurationFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new RuntimeException(message); - } - return response.getEntity(CompositeSnapshot.class); - } - - @Override - public SearchResult search(MultivaluedMap searchParams) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/search") - .queryParams(searchParams); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .get(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.searchFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new RuntimeException(message); - } - return response.getEntity(SearchResult.class); - } - - @Override - public Filter saveFilter(Filter filter) { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/filter"); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .entity(filter, CONTENT_TYPE_JSON) - .put(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.saveFilterFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new RuntimeException(message); - } - return response.getEntity(Filter.class); - } - - @Override - public List getAllFilters() { - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/filters"); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .get(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.searchFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new RuntimeException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public void deleteFilter(String name) { - // Filter name may contain space chars, need to URL encode these. - String filterName = name.replace(" ", "%20"); - WebResource webResource = getClient().resource(Preferences.jmasarServiceUrl + "/filter/" + filterName); - ClientResponse response = webResource.accept(CONTENT_TYPE_JSON) - .delete(ClientResponse.class); - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.deleteFilterFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new RuntimeException(message); - } - } - - /** - * Adds a tag to a list of unique node ids, see {@link TagData}. - * - * @param tagData see {@link TagData} - * @return A list of updated {@link Node}s. This may contain fewer elements than the list of unique node ids - * passed in the tagData parameter. - */ - public List addTag(TagData tagData) { - - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/tags"); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .entity(tagData, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.tagAddFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - /** - * Deletes a tag from a list of unique node ids, see {@link TagData} - * - * @param tagData see {@link TagData} - * @return A list of updated {@link Node}s. This may contain fewer elements than the list of unique node ids - * passed in the tagData parameter. - */ - public List deleteTag(TagData tagData) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/tags"); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .entity(tagData, CONTENT_TYPE_JSON) - .delete(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.tagAddFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public UserData authenticate(String userName, String password) { - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/login") - .queryParam("username", userName) - .queryParam("password", password); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .post(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = Messages.authenticationFailed; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public List restore(List snapshotItems){ - WebResource webResource = - getClient().resource(Preferences.jmasarServiceUrl + "/restore/items"); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .entity(snapshotItems, CONTENT_TYPE_JSON) - .post(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = "Restore failed"; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - public List restore(String snapshotNodeId){ - WebResource webResource = - getClient() - .resource(Preferences.jmasarServiceUrl + "/restore/node") - .queryParam("nodeId", snapshotNodeId); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .post(ClientResponse.class); - } catch (UniformInterfaceException e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = "Restore failed"; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } - - @Override - public List takeSnapshot(String configNodeId){ - WebResource webResource = - getClient() - .resource(Preferences.jmasarServiceUrl + "/take-snapshot/" + configNodeId); - ClientResponse response; - try { - response = webResource.accept(CONTENT_TYPE_JSON) - .get(ClientResponse.class); - } catch (Exception e) { - throw new RuntimeException(e); - } - if (response.getStatus() != ClientResponse.Status.OK.getStatusCode()) { - String message = "Take snapshot failed"; - try { - message = new String(response.getEntityInputStream().readAllBytes()); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to parse response", e); - } - throw new SaveAndRestoreClientException(message); - } - return response.getEntity(new GenericType<>() { - }); - } -} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/ContextMenuSnapshot.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/ContextMenuSnapshot.java index 3334fa3919..5c1bd9f03b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/ContextMenuSnapshot.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/ContextMenuSnapshot.java @@ -118,5 +118,6 @@ protected void runChecks() { mayTagProperty.set(saveAndRestoreController.checkTaggable()); mayCompareSnapshotsProperty.set(saveAndRestoreController.compareSnapshotsPossible()); mayTagOrUntagGoldenProperty.set(saveAndRestoreController.configureGoldenItem(tagGoldenMenuItem)); + mayCopyProperty.set(saveAndRestoreController.mayCopy()); } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java index f1cc1af7f4..61c5986035 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java @@ -19,6 +19,7 @@ package org.phoebus.applications.saveandrestore.ui; import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.client.SaveAndRestoreClientImpl; import org.phoebus.applications.saveandrestore.model.*; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; import org.phoebus.applications.saveandrestore.model.Configuration; @@ -33,7 +34,6 @@ import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchResult; import org.phoebus.applications.saveandrestore.client.SaveAndRestoreClient; -import org.phoebus.applications.saveandrestore.client.SaveAndRestoreJerseyClient; import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; @@ -70,7 +70,7 @@ public class SaveAndRestoreService { private final SaveAndRestoreClient saveAndRestoreClient; private SaveAndRestoreService() { - saveAndRestoreClient = new SaveAndRestoreJerseyClient(); + saveAndRestoreClient = new SaveAndRestoreClientImpl(); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); } diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/client/HttpClientTestIT.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/client/HttpClientTestIT.java new file mode 100644 index 0000000000..d690411e98 --- /dev/null +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/client/HttpClientTestIT.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.client; + +import org.epics.vtype.Alarm; +import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VInt; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.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.RestoreResult; +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.security.PhoebusSecurity; +import org.phoebus.security.store.SecureStore; +import org.phoebus.security.store.SecureStoreTarget; +import org.phoebus.security.tokens.AuthenticationScope; +import org.phoebus.security.tokens.ScopedAuthenticationToken; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration test to verify that save&restore client supports all + * calls to the service. + * + * To run it, the save-and-restore service must be up and running on localhost port 8080, + * and must apply the in-memory (hard-coded) authentication scheme. + * + * Disable by default, to enable: + * + * mvn -DskipITs=false ... + */ +public class HttpClientTestIT { + + private static SaveAndRestoreClient saveAndRestoreClient; + private static Node topLevelTestNode; + + @BeforeAll + public static void init() { + PhoebusSecurity.secure_store_target = SecureStoreTarget.IN_MEMORY; + try { + SecureStore store = new SecureStore(); + ScopedAuthenticationToken scopedAuthenticationToken = + new ScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE, "admin", "adminPass"); + store.setScopedAuthentication(scopedAuthenticationToken); + } catch (Exception e) { + throw new RuntimeException(e); + } + saveAndRestoreClient = new SaveAndRestoreClientImpl(); + Node node = Node.builder() + .name("IT test folder") + .build(); + Node rootNode = saveAndRestoreClient.getRoot(); + List nodes = saveAndRestoreClient.getChildNodes(rootNode.getUniqueId()); + + nodes.stream().forEach(n -> { + if (n.getName().equals("IT test folder")) { + saveAndRestoreClient.deleteNodes(List.of(n.getUniqueId())); + } + }); + + topLevelTestNode = saveAndRestoreClient.createNewNode(rootNode.getUniqueId(), node); + } + + @AfterAll + public static void cleanUp() { + saveAndRestoreClient.deleteNodes(List.of(topLevelTestNode.getUniqueId())); + } + + @Test + public void testAuthenticate() { + saveAndRestoreClient.authenticate("admin", "adminPass"); + } + + @Test + public void testCreateFoldersAndMove() { + Node node = Node.builder() + .name("Folder 1") + .build(); + node = saveAndRestoreClient.createNewNode(topLevelTestNode.getUniqueId(), node); + + Node node2 = Node.builder() + .name("Folder 2") + .build(); + node2 = saveAndRestoreClient.createNewNode(topLevelTestNode.getUniqueId(), node2); + + saveAndRestoreClient.moveNodes(List.of(node2.getUniqueId()), node.getUniqueId()); + saveAndRestoreClient.deleteNodes(List.of(node.getUniqueId())); + } + + @Test + public void testCreateAndUpdateConfiguration() { + Node node = Node.builder() + .nodeType(NodeType.CONFIGURATION) + .name("config") + .description("description") + .build(); + + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = ConfigPv.builder() + .pvName("pv") + .readbackPvName("readback") + .build(); + configurationData.setPvList(List.of(configPv)); + Configuration configuration = new Configuration(); + configuration.setConfigurationData(configurationData); + configuration.setConfigurationNode(node); + + configuration = saveAndRestoreClient.createConfiguration(topLevelTestNode.getUniqueId(), configuration); + + node = configuration.getConfigurationNode(); + node.setName("another"); + node.setDescription("Foo"); + + configuration = saveAndRestoreClient.updateConfiguration(configuration); + assertEquals("another", configuration.getConfigurationNode().getName()); + + saveAndRestoreClient.deleteNodes(List.of(configuration.getConfigurationNode().getUniqueId())); + } + + @Test + public void testCreateTagAndUpdateSnapshot() { + Node node = Node.builder() + .nodeType(NodeType.CONFIGURATION) + .name("config") + .description("description") + .build(); + + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = ConfigPv.builder() + .pvName("pv") + .readbackPvName("readback") + .build(); + configurationData.setPvList(List.of(configPv)); + Configuration configuration = new Configuration(); + configuration.setConfigurationData(configurationData); + configuration.setConfigurationNode(node); + + configuration = saveAndRestoreClient.createConfiguration(topLevelTestNode.getUniqueId(), configuration); + + SnapshotItem snapshotItem = new SnapshotItem(); + snapshotItem.setConfigPv(configPv); + snapshotItem.setValue(VInt.of(777, Alarm.none(), Time.now(), Display.none())); + snapshotItem.setReadbackValue(VInt.of(42, Alarm.none(), Time.now(), Display.none())); + + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(snapshotItem)); + + Node snapshotNode = Node.builder() + .nodeType(NodeType.SNAPSHOT) + .name("snapshot") + .description("description") + .build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotData(snapshotData); + snapshot.setSnapshotNode(snapshotNode); + + snapshot = saveAndRestoreClient.createSnapshot(configuration.getConfigurationNode().getUniqueId(), + snapshot); + + snapshotNode = snapshot.getSnapshotNode(); + snapshotNode.setName("another"); + + snapshot = saveAndRestoreClient.updateSnapshot(snapshot); + + assertEquals("another", snapshot.getSnapshotNode().getName()); + + saveAndRestoreClient.deleteNodes(List.of(configuration.getConfigurationNode().getUniqueId())); + } + + @Test + public void testTaggingAndDeleteTags() { + Node node = Node.builder() + .nodeType(NodeType.CONFIGURATION) + .name("config") + .description("description") + .build(); + + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = ConfigPv.builder() + .pvName("pv") + .readbackPvName("readback") + .build(); + configurationData.setPvList(List.of(configPv)); + Configuration configuration = new Configuration(); + configuration.setConfigurationData(configurationData); + configuration.setConfigurationNode(node); + + configuration = saveAndRestoreClient.createConfiguration(topLevelTestNode.getUniqueId(), configuration); + + SnapshotItem snapshotItem = new SnapshotItem(); + snapshotItem.setConfigPv(configPv); + snapshotItem.setValue(VInt.of(777, Alarm.none(), Time.now(), Display.none())); + snapshotItem.setReadbackValue(VInt.of(42, Alarm.none(), Time.now(), Display.none())); + + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(snapshotItem)); + + Node snapshotNode = Node.builder() + .nodeType(NodeType.SNAPSHOT) + .name("snapshot") + .description("description") + .build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotData(snapshotData); + snapshot.setSnapshotNode(snapshotNode); + + snapshot = saveAndRestoreClient.createSnapshot(configuration.getConfigurationNode().getUniqueId(), + snapshot); + + List tags = saveAndRestoreClient.getAllTags(); + int countBefore = tags.size(); + + TagData tagData = new TagData(); + tagData.setUniqueNodeIds(List.of(snapshot.getSnapshotNode().getUniqueId())); + tagData.setTag(Tag.goldenTag("admin")); + + saveAndRestoreClient.addTag(tagData); + + tags = saveAndRestoreClient.getAllTags(); + assertEquals(1, tags.size() - countBefore); + + saveAndRestoreClient.deleteTag(tagData); + + tags = saveAndRestoreClient.getAllTags(); + assertEquals(countBefore, tags.size()); + + saveAndRestoreClient.deleteNodes(List.of(configuration.getConfigurationNode().getUniqueId())); + } + + @Test + public void testCreateAndUpdateCompositeSnapshot() { + Node node = Node.builder() + .nodeType(NodeType.CONFIGURATION) + .name("config") + .description("description") + .build(); + + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = ConfigPv.builder() + .pvName("pv") + .readbackPvName("readback") + .build(); + configurationData.setPvList(List.of(configPv)); + Configuration configuration = new Configuration(); + configuration.setConfigurationData(configurationData); + configuration.setConfigurationNode(node); + + configuration = saveAndRestoreClient.createConfiguration(topLevelTestNode.getUniqueId(), configuration); + + SnapshotItem snapshotItem = new SnapshotItem(); + snapshotItem.setConfigPv(configPv); + snapshotItem.setValue(VInt.of(777, Alarm.none(), Time.now(), Display.none())); + snapshotItem.setReadbackValue(VInt.of(42, Alarm.none(), Time.now(), Display.none())); + + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(snapshotItem)); + + Node snapshotNode = Node.builder() + .nodeType(NodeType.SNAPSHOT) + .name("snapshot") + .description("description") + .build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotData(snapshotData); + snapshot.setSnapshotNode(snapshotNode); + + snapshot = saveAndRestoreClient.createSnapshot(configuration.getConfigurationNode().getUniqueId(), + snapshot); + + Node compositeSnapshotNode = Node.builder() + .nodeType(NodeType.COMPOSITE_SNAPSHOT) + .name("composite") + .description("description") + .build(); + CompositeSnapshotData compositeSnapshotData = new CompositeSnapshotData(); + compositeSnapshotData.setReferencedSnapshotNodes(List.of(snapshot.getSnapshotNode().getUniqueId())); + CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); + compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); + compositeSnapshot.setCompositeSnapshotNode(compositeSnapshotNode); + + compositeSnapshot = saveAndRestoreClient.createCompositeSnapshot(topLevelTestNode.getUniqueId(), compositeSnapshot); + + compositeSnapshot.getCompositeSnapshotNode().setName("another"); + compositeSnapshot = saveAndRestoreClient.updateCompositeSnapshot(compositeSnapshot); + + assertEquals("another", compositeSnapshot.getCompositeSnapshotNode().getName()); + + saveAndRestoreClient.deleteNodes(List.of(compositeSnapshot.getCompositeSnapshotNode().getUniqueId())); + } + + @Test + public void testCreateFilter() { + + List filters = saveAndRestoreClient.getAllFilters(); + int countBefore = filters.size(); + + Filter filter = new Filter(); + filter.setName("filter"); + filter.setQueryString("name=foo"); + + filter = saveAndRestoreClient.saveFilter(filter); + filters = saveAndRestoreClient.getAllFilters(); + + assertEquals(1, filters.size() - countBefore); + + saveAndRestoreClient.deleteFilter(filter.getName()); + + filters = saveAndRestoreClient.getAllFilters(); + + assertEquals(countBefore, filters.size()); + } + + @Test + public void testSearch() { + MultivaluedMap map = new MultivaluedHashMap<>(); + map.put("type", List.of("FOLDER")); + SearchResult searchResult = saveAndRestoreClient.search(map); + assertTrue(searchResult.getHitCount() > 0); + } + + @Test + public void testRestore() { + Node node = Node.builder() + .nodeType(NodeType.CONFIGURATION) + .name("config") + .description("description") + .build(); + + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = ConfigPv.builder() + .pvName("pv") + .readbackPvName("readback") + .build(); + configurationData.setPvList(List.of(configPv)); + Configuration configuration = new Configuration(); + configuration.setConfigurationData(configurationData); + configuration.setConfigurationNode(node); + + configuration = saveAndRestoreClient.createConfiguration(topLevelTestNode.getUniqueId(), configuration); + + SnapshotItem snapshotItem = new SnapshotItem(); + snapshotItem.setConfigPv(configPv); + snapshotItem.setValue(VInt.of(777, Alarm.none(), Time.now(), Display.none())); + snapshotItem.setReadbackValue(VInt.of(42, Alarm.none(), Time.now(), Display.none())); + + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(snapshotItem)); + + Node snapshotNode = Node.builder() + .nodeType(NodeType.SNAPSHOT) + .name("snapshot") + .description("description") + .build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotData(snapshotData); + snapshot.setSnapshotNode(snapshotNode); + + snapshot = saveAndRestoreClient.createSnapshot(configuration.getConfigurationNode().getUniqueId(), + snapshot); + + List results = saveAndRestoreClient.restore(snapshot.getSnapshotNode().getUniqueId()); + assertFalse(results.get(0).getErrorMsg().isEmpty()); + + results = saveAndRestoreClient.restore(List.of(snapshotItem)); + assertFalse(results.get(0).getErrorMsg().isEmpty()); + + saveAndRestoreClient.deleteNodes(List.of(configuration.getConfigurationNode().getUniqueId())); + } + + @Test + public void testTakeSnapshot() { + Node node = Node.builder() + .nodeType(NodeType.CONFIGURATION) + .name("config") + .description("description") + .build(); + + ConfigurationData configurationData = new ConfigurationData(); + ConfigPv configPv = ConfigPv.builder() + .pvName("pv") + .readbackPvName("readback") + .build(); + configurationData.setPvList(List.of(configPv)); + Configuration configuration = new Configuration(); + configuration.setConfigurationData(configurationData); + configuration.setConfigurationNode(node); + + configuration = saveAndRestoreClient.createConfiguration(topLevelTestNode.getUniqueId(), configuration); + + List snapshotItems = saveAndRestoreClient.takeSnapshot(configuration.getConfigurationNode().getUniqueId()); + assertEquals(1, snapshotItems.size()); + assertNull(snapshotItems.get(0).getValue()); + + saveAndRestoreClient.deleteNodes(List.of(configuration.getConfigurationNode().getUniqueId())); + + } + + /* + @Test + public void tetsFoo() throws Exception{ + FileInputStream fileInputStream = new FileInputStream(new File("/Users/georgweiss/tmp/tags")); + ObjectMapper objectMapper = new ObjectMapper(); + List tags = objectMapper.readValue(fileInputStream, new TypeReference<>() { + }); + System.out.println(tags.size()); + int withComments = 0; + int nonGolden = 0; + for(Tag t : tags){ + if(!t.getName().equals("golden")){ + nonGolden++; + } + if(t.getComment() != null && !t.getComment().isEmpty()){ + withComments++; + System.out.println(t.getName() + " " + t.getComment()); + } + } + System.out.println(nonGolden); + System.out.println(withComments); + fileInputStream.close(); + } + + */ +} 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 62dd0a91c9..cf4601bcbd 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 @@ -67,7 +67,6 @@ public interface NodeDAO { * * @param nodeId The unique id of the node to delete. */ - @Deprecated void deleteNode(String nodeId); /** diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java index 0a404324e3..3b4b41e0ee 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java @@ -67,16 +67,32 @@ public class AuthorizationHelper { * @return true only if all if the nodes can be deleted by the user. */ public boolean mayDelete(List nodeIds, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + for (String nodeId : nodeIds) { + if (!mayDelete(nodeId, methodSecurityExpressionOperations)) { + return false; + } + } + return true; + } + + /** + * Checks if all the provided node id to this method can be deleted by the user. User with admin privileges is always + * permitted to delete, while a user not having required role may never delete. + * + * @param nodeId A {@link Node} id subject to the check. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return true only if all if the node can be deleted by the user. + */ + public boolean mayDelete(String nodeId, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { return true; } if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { return false; } - for (String nodeId : nodeIds) { - if (!mayDelete(nodeId, ((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername())) { - return false; - } + if (!mayDelete(nodeId, ((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername())) { + return false; } return true; } 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 842beae6f5..cf385aaae4 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 @@ -23,16 +23,18 @@ import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.security.access.expression.SecurityExpressionRoot; -import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.List; -import java.util.logging.Logger; /** * Controller offering endpoints for CRUD operations on {@link Node}s, which represent @@ -45,7 +47,6 @@ public class NodeController extends BaseController { @Autowired private NodeDAO nodeDAO; - private final Logger logger = Logger.getLogger(NodeController.class.getName()); /** * Create a new folder in the tree structure. @@ -108,7 +109,6 @@ public List getNodes(@RequestBody List uniqueNodeIds) { } /** - * * @param uniqueNodeId Unique {@link Node} id. * @return The parent {@link Node} of #uniqueNodeId. */ @@ -119,7 +119,6 @@ public Node getParentNode(@PathVariable String uniqueNodeId) { } /** - * * @param uniqueNodeId Unique {@link Node} id. * @return Potentially empty list of child {@link Node}s of the {@link Node} identified by #uniqueNodeId. */ @@ -129,33 +128,6 @@ public List getChildNodes(@PathVariable final String uniqueNodeId) { return nodeDAO.getChildNodes(uniqueNodeId); } - /** - * Recursively deletes a node and all its child nodes, if any. In particular, if the node id points to a configuration, - * all snapshots associated with that configuration will also be deleted. A client may wish to alert the - * user of this side effect. - *

- * A {@link HttpStatus#NOT_FOUND} is returned if the specified unique node id does not exist. - *

- *

- * A {@link HttpStatus#BAD_REQUEST} is returned if the specified unique node id is the tree root node id, - * see {@link Node#ROOT_FOLDER_UNIQUE_ID}. - *

- * - * @param uniqueNodeId The non-zero id of the node to delete - * @param authentication {@link Authentication} of authenticated user. - */ - /* - @SuppressWarnings("unused") - @DeleteMapping(value = "/node/{uniqueNodeId}", produces = JSON) - @PreAuthorize("hasRole(this.roleAdmin) or @authorizationHelper.mayDelete(#uniqueNodeId, #authentication)") - @Deprecated - public void deleteNode(@PathVariable final String uniqueNodeId, Authentication authentication) { - logger.info("Deleting node with unique id " + uniqueNodeId); - nodeDAO.deleteNode(uniqueNodeId); - } - - */ - /** * Deletes all {@link Node}s contained in the provided list. *
@@ -168,7 +140,8 @@ public void deleteNode(@PathVariable final String uniqueNodeId, Authentication a * authorities of the user. *
* Note also that an unauthenticated user (e.g. no basic authentication header in client's request) will - * receive a HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. + * receive an HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. + * * @param nodeIds List of {@link Node} ids to remove. */ @SuppressWarnings("unused") @@ -178,6 +151,29 @@ public void deleteNodes(@RequestBody List nodeIds) { nodeDAO.deleteNodes(nodeIds); } + /** + * Deletes one {@link Node}. + *
+ * Checks are made to make sure user may delete + * the {@link Node}, see {@link AuthorizationHelper}. If the checks fail on any of the {@link Node} ids, + * checks are aborted and client will receive an HTTP 403 response. + *
+ * Note that the {@link PreAuthorize} annotations calls a helper method in {@link AuthorizationHelper}, using + * the list of {@link Node} ids and a Spring injected object - root - used to check + * authorities of the user. + *
+ * Note also that an unauthenticated user (e.g. no basic authentication header in client's request) will + * receive an HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. + * + * @param nodeId {@link Node} id to remove. + */ + @SuppressWarnings("unused") + @DeleteMapping(value = "/node/{nodeId}", produces = JSON) + @PreAuthorize("@authorizationHelper.mayDelete(#nodeId, #root)") + public void deleteNode(@PathVariable String nodeId) { + deleteNodes(List.of(nodeId)); + } + /** * Updates a {@link Node}. The purpose is to support modification of name or comment/description, or both. Modification of * node type is not supported. @@ -191,7 +187,7 @@ public void deleteNodes(@RequestBody List nodeIds) { * authorities of the user. *
* Note also that an unauthenticated user (e.g. no basic authentication header in client's request) will - * receive a HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. + * receive an HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. * *

* A {@link HttpStatus#BAD_REQUEST} is returned if a node of the same name and type already exists in the parent folder, @@ -213,7 +209,7 @@ 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.valueOf(customTimeForMigration)); + return nodeDAO.updateNode(nodeToUpdate, Boolean.parseBoolean(customTimeForMigration)); } /** 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 7ca04e5036..56c9b2057a 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 @@ -27,7 +27,11 @@ import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.List; @@ -46,7 +50,6 @@ public class TagController extends BaseController { private NodeDAO nodeDAO; /** - * * @return A {@link List} of all {@link Tag}s. */ @GetMapping("/tags") @@ -72,7 +75,7 @@ public List addTag(@RequestBody TagData tagData, /** * Removes a {@link Tag} from specified list of target {@link Node}s. The {@link Tag} contained - * * in tagData must be non-null, and its name must be non-null and non-empty. + * in tagData must be non-null, and its name must be non-null and non-empty. * * @param tagData See {@link TagData} * @return The list of updated {@link Node}s