diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/services/BookmarkManagerServiceTest.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/services/BookmarkManagerServiceTest.java new file mode 100644 index 000000000..fabc5bef6 --- /dev/null +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/services/BookmarkManagerServiceTest.java @@ -0,0 +1,459 @@ +/******************************************************************************* + * Copyright (c) 2024 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.services; + +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.views.QueryParameters; +import org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.stubs.BookmarkModelStub; +import org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.stubs.ExperimentModelStub; +import org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.utils.RestServerTest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Test class for BookmarkManagerService + * + * @author Kaveh Shahedi + */ +@SuppressWarnings("null") +public class BookmarkManagerServiceTest extends RestServerTest { + + private static final String BOOKMARK_NAME = "TEST"; + private static final long START_TIME = 0L; + private static final long END_TIME = 10L; + private static final @NonNull BookmarkModelStub BOOKMARK = new BookmarkModelStub(BOOKMARK_NAME, START_TIME, END_TIME); + private ExperimentModelStub experiment; + + private static final String START = "start"; + private static final String END = "end"; + + private static final String BOOKMARK_END_TIME_MATCH = "End time should match"; + private static final String BOOKMARK_NAME_MATCH = "Bookmark name should match"; + private static final String BOOKMARK_START_TIME_MATCH = "Start time should match"; + private static final String NON_EXISTENT_BOOKMARK_STATUS_CODE = "Should return 404 for non-existent bookmark"; + private static final String NON_EXISTENT_EXPERIMENT_STATUS_CODE = "Should return 404 for non-existent experiment"; + private static final String NON_NULL_BOOKMARK = "Created bookmark should not be null"; + private static final String NON_NULL_RESPONSE_BODY = "Response body should not be null"; + private static final String NON_NULL_UUID = "UUID should not be null"; + private static final String NON_NUMERIC_TIMES_STATUS_CODE = "Should return 400 for non-numeric times"; + private static final String SUCCESSFUL_BOOKMARK_CREATION = "Bookmark creation should succeed"; + private static final String SUCCESSFUL_STATUS_CODE = "Response status should be 200"; + + /** + * Setup method to run before each test + */ + @Before + public void setUp() { + // Create the experiment first + experiment = assertPostExperiment(CONTEXT_SWITCHES_UST_NOT_INITIALIZED_STUB.getName(), + CONTEXT_SWITCHES_UST_NOT_INITIALIZED_STUB); + assertNotNull("Experiment should not be null", experiment); + assertNotNull(NON_NULL_UUID, experiment.getUUID()); + } + + /** + * Tear down method to run after each test + */ + @After + public void tearDown() { + // Remove all bookmarks + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + try (Response response = bookmarkTarget.request().get()) { + assertEquals("GET request for bookmarks should return 200", 200, response.getStatus()); + + BookmarkModelStub[] existingBookmarks = response.readEntity(BookmarkModelStub[].class); + assertNotNull("Bookmark array should not be null", existingBookmarks); + + for (BookmarkModelStub bookmark : existingBookmarks) { + try (Response deleteResponse = bookmarkTarget.path(bookmark.getUUID().toString()) + .request() + .delete()) { + assertEquals("DELETE request should return 200", 200, deleteResponse.getStatus()); + } + } + } + } + + /** + * Test the bookmark endpoints with invalid experiment UUID (i.e., + * non-existent). + */ + @Test + public void testBookmarkEndpointsInvalidExperiment() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(UUID.randomUUID().toString()) + .path(BOOKMARKS); + + // Test getting all bookmarks + try (Response response = bookmarkTarget.request().get()) { + assertEquals(NON_EXISTENT_EXPERIMENT_STATUS_CODE, 404, response.getStatus()); + } + + // Test getting a specific bookmark + try (Response response = bookmarkTarget.path(BOOKMARK.getUUID().toString()).request().get()) { + assertEquals(NON_EXISTENT_EXPERIMENT_STATUS_CODE, 404, response.getStatus()); + } + + // Test creating a bookmark + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK_NAME); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(NON_EXISTENT_EXPERIMENT_STATUS_CODE, 404, response.getStatus()); + } + + // Test updating a bookmark + try (Response response = bookmarkTarget.path(BOOKMARK.getUUID().toString()).request().put(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(NON_EXISTENT_EXPERIMENT_STATUS_CODE, 404, response.getStatus()); + } + + // Test deleting a bookmark + try (Response response = bookmarkTarget.path(BOOKMARK.getUUID().toString()).request().delete()) { + assertEquals(NON_EXISTENT_EXPERIMENT_STATUS_CODE, 404, response.getStatus()); + } + } + + /** + * Test the creation of a bookmark with invalid parameters. + */ + @Test + public void testCreateBookmarkInvalidParams() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + // Test with null name + Map parameters = new HashMap<>(); + parameters.put(NAME, null); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals("Should return 400 for null name", 400, response.getStatus()); + + } + + // Test with non-numeric start + parameters.put(NAME, BOOKMARK_NAME); + parameters.put(START, "not a number"); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(NON_NUMERIC_TIMES_STATUS_CODE, 400, response.getStatus()); + } + + // Test with non-numeric end + parameters.put(START, START_TIME); + parameters.put(END, "not a number"); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(NON_NUMERIC_TIMES_STATUS_CODE, 400, response.getStatus()); + } + + // Test with end time before start time + parameters.put(NAME, BOOKMARK_NAME); + parameters.put(START, END_TIME); + parameters.put(END, START_TIME); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals("Should return 400 for invalid time range", 400, response.getStatus()); + } + + } + + /** + * Test the creation of a bookmark. + */ + @Test + public void testCreateBookmark() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(SUCCESSFUL_STATUS_CODE, 200, response.getStatus()); + + BookmarkModelStub expStub = response.readEntity(BookmarkModelStub.class); + assertNotNull(NON_NULL_RESPONSE_BODY, expStub); + assertEquals(BOOKMARK_NAME_MATCH, BOOKMARK.getName(), expStub.getName()); + assertEquals(BOOKMARK_START_TIME_MATCH, BOOKMARK.getStart(), expStub.getStart()); + assertEquals(BOOKMARK_END_TIME_MATCH, BOOKMARK.getEnd(), expStub.getEnd()); + assertNotNull(NON_NULL_UUID, expStub.getUUID()); + } + } + + /** + * Test the creation of a bookmark with no end time (i.e., just start time). + */ + @Test + public void testCreateBookmarkNoEndTime() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put(START, START_TIME); + parameters.put(END, START_TIME); + + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(SUCCESSFUL_STATUS_CODE, 200, response.getStatus()); + + BookmarkModelStub expStub = response.readEntity(BookmarkModelStub.class); + assertNotNull(NON_NULL_RESPONSE_BODY, expStub); + assertEquals(BOOKMARK_NAME_MATCH, BOOKMARK.getName(), expStub.getName()); + assertEquals(BOOKMARK_START_TIME_MATCH, BOOKMARK.getStart(), expStub.getStart()); + assertEquals(BOOKMARK_END_TIME_MATCH, BOOKMARK.getStart(), expStub.getEnd()); + assertNotNull(NON_NULL_UUID, expStub.getUUID()); + } + } + + /** + * Test the creation of a bookmark with a repetitive data. + */ + @Test + public void testCreateIdenticalBookmarks() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + Set uuids = new HashSet<>(); + for (int i = 0; i < 3; i++) { + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(SUCCESSFUL_STATUS_CODE, 200, response.getStatus()); + + BookmarkModelStub expStub = response.readEntity(BookmarkModelStub.class); + assertNotNull(NON_NULL_RESPONSE_BODY, expStub); + assertEquals(BOOKMARK_NAME_MATCH, BOOKMARK.getName(), expStub.getName()); + assertEquals(BOOKMARK_START_TIME_MATCH, BOOKMARK.getStart(), expStub.getStart()); + assertEquals(BOOKMARK_END_TIME_MATCH, BOOKMARK.getEnd(), expStub.getEnd()); + assertNotNull(NON_NULL_UUID, expStub.getUUID()); + + // Check if the UUID is unique + assertFalse("UUID should be unique", uuids.contains(expStub.getUUID())); + uuids.add(expStub.getUUID()); + } + } + + } + + /** + * Test the fetching of all bookmarks. + */ + @Test + public void testGetAllBookmarks() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + // Initially there should be no bookmarks + try (Response response = bookmarkTarget.request().get()) { + BookmarkModelStub[] initialBookmarks = response.readEntity(BookmarkModelStub[].class); + assertEquals("Should start with no bookmarks", 0, initialBookmarks.length); + } + + // Create multiple bookmarks + Map parameters = new HashMap<>(); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + + // Create first bookmark + parameters.put(NAME, "Bookmark1"); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals("First bookmark creation should succeed", 200, response.getStatus()); + } + + // Create second bookmark + parameters.put(NAME, "Bookmark2"); + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals("Second bookmark creation should succeed", 200, response.getStatus()); + } + + // Get all bookmarks + try (Response response = bookmarkTarget.request().get()) { + BookmarkModelStub[] allBookmarks = response.readEntity(BookmarkModelStub[].class); + assertEquals("Should have 2 bookmarks", 2, allBookmarks.length); + + // Verify bookmark properties + for (BookmarkModelStub bookmark : allBookmarks) { + assertNotNull("Bookmark should not be null", bookmark); + assertNotNull("Bookmark UUID should not be null", bookmark.getUUID()); + assertEquals(BOOKMARK_START_TIME_MATCH, START_TIME, bookmark.getStart()); + assertEquals(BOOKMARK_END_TIME_MATCH, END_TIME, bookmark.getEnd()); + assertTrue("Name should be either Bookmark1 or Bookmark2", + bookmark.getName().equals("Bookmark1") || bookmark.getName().equals("Bookmark2")); + } + } + } + + /** + * Test the fetching of a specific bookmark. + */ + @Test + public void testGetSpecificBookmark() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + // Create a bookmark + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + + BookmarkModelStub createdBookmark = null; + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals(SUCCESSFUL_BOOKMARK_CREATION, 200, response.getStatus()); + createdBookmark = response.readEntity(BookmarkModelStub.class); + assertNotNull(NON_NULL_BOOKMARK, createdBookmark); + } + + // Test getting non-existent bookmark + try (Response nonExistentResponse = bookmarkTarget.path(experiment.getUUID().toString()).request().get()) { + assertEquals(NON_EXISTENT_BOOKMARK_STATUS_CODE, 404, nonExistentResponse.getStatus()); + } + + // Test getting existing bookmark + try (Response response = bookmarkTarget.path(createdBookmark.getUUID().toString()).request().get()) { + assertEquals("Should successfully get existing bookmark", 200, response.getStatus()); + + BookmarkModelStub retrievedBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals("Retrieved bookmark should match created bookmark", createdBookmark, retrievedBookmark); + } + } + + /** + * Test updating a bookmark. + */ + @Test + public void testUpdateBookmark() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + // Create initial bookmark + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + + BookmarkModelStub originalBookmark = null; + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + originalBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals(SUCCESSFUL_BOOKMARK_CREATION, 200, response.getStatus()); + assertNotNull(NON_NULL_BOOKMARK, originalBookmark); + } + + // Test updating with invalid parameters + parameters.put(START, END_TIME); + parameters.put(END, START_TIME); + try (Response invalidResponse = bookmarkTarget.path(originalBookmark.getUUID().toString()).request().put(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals("Should return 400 for invalid parameters", 400, invalidResponse.getStatus()); + } + + // Test successful update + parameters.put("name", "Updated Name"); + parameters.put(START, START_TIME + 5); + parameters.put(END, END_TIME + 5); + try (Response response = bookmarkTarget.path(originalBookmark.getUUID().toString()).request().put(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + assertEquals("Update should succeed", 200, response.getStatus()); + + BookmarkModelStub updatedBookmark = response.readEntity(BookmarkModelStub.class); + assertNotNull(NON_NULL_BOOKMARK, updatedBookmark); + assertEquals("UUID should be the same", originalBookmark.getUUID(), updatedBookmark.getUUID()); + assertEquals("Name should be updated", "Updated Name", updatedBookmark.getName()); + assertEquals("Start time should be updated", START_TIME + 5, updatedBookmark.getStart()); + assertEquals("End time should be updated", END_TIME + 5, updatedBookmark.getEnd()); + } + } + + /** + * Test the deletion of a bookmark with various scenarios. + */ + @Test + public void testDeleteBookmark() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + // Try deleting non-existent bookmark + try (Response nonExistentResponse = bookmarkTarget.path(experiment.getUUID().toString()).request().delete()) { + assertEquals(NON_EXISTENT_BOOKMARK_STATUS_CODE, 404, nonExistentResponse.getStatus()); + } + + // Create a bookmark to delete + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); + + BookmarkModelStub createdBookmark = null; + try (Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList())))) { + createdBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals(SUCCESSFUL_BOOKMARK_CREATION, 200, response.getStatus()); + assertNotNull(NON_NULL_BOOKMARK, createdBookmark); + } + + // Delete the bookmark + try (Response response = bookmarkTarget.path(createdBookmark.getUUID().toString()).request().delete()) { + assertEquals("Delete should succeed", 200, response.getStatus()); + BookmarkModelStub deletedBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals("Deleted bookmark should match created bookmark", createdBookmark, deletedBookmark); + } + + // Verify the bookmark is actually deleted + try (Response getResponse = bookmarkTarget.path(createdBookmark.getUUID().toString()).request().get()) { + assertEquals("Should return 404 for deleted bookmark", 404, getResponse.getStatus()); + } + + // Verify it's not in the list of all bookmarks + try (Response getAllResponse = bookmarkTarget.request().get()) { + BookmarkModelStub[] allBookmarks = getAllResponse.readEntity(BookmarkModelStub[].class); + for (BookmarkModelStub bookmark : allBookmarks) { + assertNotEquals("Deleted bookmark should not be in list of all bookmarks", createdBookmark.getUUID(), bookmark.getUUID()); + } + } + } +} \ No newline at end of file diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/BookmarkModelStub.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/BookmarkModelStub.java new file mode 100644 index 000000000..ad3c45c20 --- /dev/null +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/BookmarkModelStub.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) 2024 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.stubs; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A Stub class for the bookmark model. It matches the trace server protocol's + * BookmarkModel schema + * + * @author Kaveh Shahedi + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class BookmarkModelStub implements Serializable { + private static final long serialVersionUID = -1945923534635091200L; + + private final UUID fUUID; + private final String fName; + private final long fStart; + private final long fEnd; + + /** + * {@link JsonCreator} Constructor for final fields + * + * @param uuid + * The bookmark's UUID + * @param name + * The bookmark name + * @param start + * The start time + * @param end + * The end time + */ + @JsonCreator + public BookmarkModelStub( + @JsonProperty("uuid") UUID uuid, + @JsonProperty("name") String name, + @JsonProperty("start") long start, + @JsonProperty("end") long end) { + fUUID = Objects.requireNonNull(uuid, "The 'UUID' json field was not set"); + fName = Objects.requireNonNull(name, "The 'name' json field was not set"); + fStart = start; + fEnd = end; + } + + /** + * Constructor for comparing equality + * + * @param name + * bookmark name + * @param start + * start time + * @param end + * end time + */ + public BookmarkModelStub(String name, long start, long end) { + this(UUID.randomUUID(), name, start, end); + } + + /** + * Get the UUID + * + * @return The UUID + */ + public UUID getUUID() { + return fUUID; + } + + /** + * Get the bookmark name + * + * @return The bookmark name + */ + public String getName() { + return fName; + } + + /** + * Get the start time + * + * @return The start time + */ + public long getStart() { + return fStart; + } + + /** + * Get the end time + * + * @return The end time + */ + public long getEnd() { + return fEnd; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BookmarkModelStub other = (BookmarkModelStub) obj; + if (fEnd != other.fEnd) { + return false; + } + if (fName == null) { + if (other.fName != null) { + return false; + } + } else if (!fName.equals(other.fName)) { + return false; + } + if (fStart != other.fStart) { + return false; + } + if (fUUID == null) { + if (other.fUUID != null) { + return false; + } + } else if (!fUUID.equals(other.fUUID)) { + return false; + } + return true; + } + + + @Override + public String toString() { + return "BookmarkModelStub [fUUID=" + fUUID + ", fName=" + fName + ", fStart=" + fStart + ", fEnd=" + fEnd + "]"; + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/webapp/TestWebApplication.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/webapp/TestWebApplication.java index 0a5e92773..54af8e84b 100644 --- a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/webapp/TestWebApplication.java +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/stubs/webapp/TestWebApplication.java @@ -11,6 +11,7 @@ package org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.stubs.webapp; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.BookmarkManagerService; import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.ConfigurationManagerService; import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.ExperimentManagerService; import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.FilterService; @@ -52,5 +53,6 @@ protected void registerResourcesAndMappers(ResourceConfig rc) { rc.register(CORSFilter.class); rc.register(JacksonObjectMapperProvider.class); rc.register(OpenApiResource.class); + rc.register(BookmarkManagerService.class); } } diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/utils/RestServerTest.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/utils/RestServerTest.java index 98772095a..7899947e0 100644 --- a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/utils/RestServerTest.java +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests/src/org/eclipse/tracecompass/incubator/trace/server/jersey/rest/core/tests/utils/RestServerTest.java @@ -79,6 +79,10 @@ public abstract class RestServerTest { * Experiments endpoint path (relative to application). */ public static final String EXPERIMENTS = "experiments"; + /** + * Bookmarks endpoint path (relative to application). + */ + public static final String BOOKMARKS = "bookmarks"; /** * Outputs path segment diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/model/Bookmark.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/model/Bookmark.java new file mode 100644 index 000000000..b85b33bab --- /dev/null +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/model/Bookmark.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2024 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model; + +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNull; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Contributes to the model used for TSP swagger-core annotations. + * + * @author Kaveh Shahedi + */ +public interface Bookmark { + + /** + * @return The bookmark UUID. + */ + @JsonProperty("uuid") + @Schema(description = "The bookmark's unique identifier") + UUID getUUID(); + + /** + * @return The bookmark name. + */ + @NonNull + @Schema(description = "User defined name for the bookmark") + String getName(); + + /** + * @return The start time. + */ + @Schema(description = "The bookmark's start time") + long getStart(); + + /** + * @return The end time. + */ + @Schema(description = "The bookmark's end time") + long getEnd(); + +} diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/model/BookmarkQueryParameters.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/model/BookmarkQueryParameters.java new file mode 100644 index 000000000..e948710ca --- /dev/null +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/model/BookmarkQueryParameters.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2024 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; + +/** + * Parameters for bookmark creation and update operations + * + * @author Kaveh Shahedi + */ +public interface BookmarkQueryParameters { + + /** + * @return The bookmark parameters + */ + @JsonProperty("parameters") + @Schema(description = "The bookmark parameters", requiredMode = RequiredMode.REQUIRED) + BookmarkParameters getParameters(); + + + /** + * Bookmark parameters + */ + interface BookmarkParameters { + /** + * @return The bookmark name + */ + @JsonProperty("name") + @Schema(description = "The name to give this bookmark", requiredMode = RequiredMode.REQUIRED) + String getName(); + + /** + * @return The start time + */ + @JsonProperty("start") + @Schema(description = "The bookmark's start time", requiredMode = RequiredMode.REQUIRED) + long getStart(); + + /** + * @return The end time + */ + @JsonProperty("end") + @Schema(description = "The bookmark's end time", requiredMode = RequiredMode.REQUIRED) + long getEnd(); + } +} \ No newline at end of file diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/Bookmark.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/Bookmark.java new file mode 100644 index 000000000..83a451fb6 --- /dev/null +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/Bookmark.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2024 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services; + +import java.io.Serializable; +import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Bookmark model for TSP + * + * @author Kaveh Shahedi + */ +public class Bookmark implements Serializable { + + private static final long serialVersionUID = 6126770413230064175L; + + private final UUID fUUID; + private final String fName; + private final long fStart; + private final long fEnd; + + /** + * {@link JsonCreator} Constructor for final fields + * + * @param uuid + * the stub's UUID + * @param name + * bookmark name + * @param start + * start time + * @param end + * end time + */ + @JsonCreator + public Bookmark( + @JsonProperty("uuid") UUID uuid, + @JsonProperty("name") String name, + @JsonProperty("start") long start, + @JsonProperty("end") long end) { + fUUID = uuid; + fName = name; + fStart = start; + fEnd = end; + } + + /** + * Get the UUID + * + * @return the UUID + */ + public UUID getUUID() { + return fUUID; + } + + /** + * Get the bookmark name + * + * @return the bookmark name + */ + public String getName() { + return fName; + } + + /** + * Get the start time + * + * @return the start time + */ + public long getStart() { + return fStart; + } + + /** + * Get the end time + * + * @return the end time + */ + public long getEnd() { + return fEnd; + } + + @Override + public String toString() { + return "Bookmark [fUUID=" + fUUID + ", fName=" + fName //$NON-NLS-1$ //$NON-NLS-2$ + + ", fStart=" + fStart + ", fEnd=" + fEnd + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } +} \ No newline at end of file diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/BookmarkManagerService.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/BookmarkManagerService.java new file mode 100644 index 000000000..020dc2a95 --- /dev/null +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/BookmarkManagerService.java @@ -0,0 +1,451 @@ +/******************************************************************************* + * Copyright (c) 2024 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services; + +import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.EXP_UUID; +import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.INVALID_PARAMETERS; +import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.MISSING_PARAMETERS; +import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.NO_SUCH_EXPERIMENT; +import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.BKM; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.osgi.util.NLS; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.Activator; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.BookmarkQueryParameters; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.views.QueryParameters; +import org.eclipse.tracecompass.tmf.core.resources.ITmfMarker; +import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp; +import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager; +import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * Service to manage bookmarks for experiments + * + * @author Kaveh Shahedi + */ +@Path("/experiments/{expUUID}/bookmarks") +@Tag(name = BKM) +public class BookmarkManagerService { + + // Bookmark attribute constants + private static final String BOOKMARK_UUID = "uuid"; //$NON-NLS-1$ + private static final String BOOKMARK_NAME = "name"; //$NON-NLS-1$ + private static final String BOOKMARK_START = "start"; //$NON-NLS-1$ + private static final String BOOKMARK_END = "end"; //$NON-NLS-1$ + + private static final String BOOKMARK_DEFAULT_COLOR = "RGBA {255, 0, 0, 128}"; //$NON-NLS-1$ + + /** + * Retrieve all bookmarks for a specific experiment + * + * @param expUUID + * UUID of the experiment + * @return Response containing the list of bookmarks + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get all bookmarks for an experiment", responses = { + @ApiResponse(responseCode = "200", description = "Returns the list of bookmarks", content = @Content(array = @ArraySchema(schema = @Schema(implementation = org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.Bookmark.class)))), + @ApiResponse(responseCode = "404", description = NO_SUCH_EXPERIMENT, content = @Content(schema = @Schema(implementation = String.class))) + }) + public Response getBookmarks(@Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID) { + TmfExperiment experiment = ExperimentManagerService.getExperimentByUUID(expUUID); + if (experiment == null) { + return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); + } + + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { + return Response.ok(Collections.emptyList()).build(); + } + + try { + IMarker[] markers = findBookmarkMarkers(editorFile); + List bookmarks = markersToBookmarks(markers); + return Response.ok(bookmarks).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to get bookmarks", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Get a specific bookmark from an experiment + * + * @param expUUID + * UUID of the experiment + * @param bookmarkUUID + * UUID of the bookmark to retrieve + * @return Response containing the bookmark + */ + @GET + @Path("/{bookmarkUUID}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get a specific bookmark from an experiment", responses = { + @ApiResponse(responseCode = "200", description = "Returns the bookmark", content = @Content(schema = @Schema(implementation = org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.Bookmark.class))), + @ApiResponse(responseCode = "404", description = "Experiment or bookmark not found", content = @Content(schema = @Schema(implementation = String.class))) + }) + public Response getBookmark( + @Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID, + @Parameter(description = "Bookmark UUID") @PathParam("bookmarkUUID") UUID bookmarkUUID) { + + TmfExperiment experiment = ExperimentManagerService.getExperimentByUUID(expUUID); + if (experiment == null) { + return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); + } + + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + try { + IMarker[] markers = findBookmarkMarkers(editorFile); + IMarker marker = findMarkerByUUID(markers, bookmarkUUID); + if (marker == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + Bookmark bookmark = markerToBookmark(marker); + if (bookmark == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + return Response.ok(bookmark).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to get bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Create a new bookmark in an experiment + * + * @param expUUID + * UUID of the experiment + * @param queryParameters + * Parameters for creating the bookmark + * @return Response containing the created bookmark + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Create a new bookmark in an experiment", responses = { + @ApiResponse(responseCode = "200", description = "Bookmark created successfully", content = @Content(schema = @Schema(implementation = org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.Bookmark.class))), + @ApiResponse(responseCode = "400", description = INVALID_PARAMETERS, content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "404", description = NO_SUCH_EXPERIMENT, content = @Content(schema = @Schema(implementation = String.class))) + }) + public Response createBookmark( + @Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID, + @RequestBody(content = { + @Content(schema = @Schema(implementation = BookmarkQueryParameters.class)) + }, required = true) QueryParameters queryParameters) { + + if (queryParameters == null) { + return Response.status(Status.BAD_REQUEST).entity(MISSING_PARAMETERS).build(); + } + + Map parameters = queryParameters.getParameters(); + String errorMessage = QueryParametersUtil.validateBookmarkQueryParameters(parameters); + if (errorMessage != null) { + return Response.status(Status.BAD_REQUEST).entity(errorMessage).build(); + } + + TmfExperiment experiment = ExperimentManagerService.getExperimentByUUID(expUUID); + if (experiment == null) { + return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); + } + + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + try { + String name = Objects.requireNonNull((String) parameters.get(BOOKMARK_NAME)); + long start = Objects.requireNonNull((Number) parameters.get(BOOKMARK_START)).longValue(); + long end = Objects.requireNonNull((Number) parameters.get(BOOKMARK_END)).longValue(); + UUID uuid = generateUUID(editorFile); + + Bookmark bookmark = new Bookmark(uuid, name, start, end); + + IMarker marker = editorFile.createMarker(IMarker.BOOKMARK); + setMarkerAttributes(marker, bookmark); + return Response.ok(bookmark).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to create bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Update an existing bookmark in an experiment + * + * @param expUUID + * UUID of the experiment + * @param bookmarkUUID + * UUID of the bookmark to update + * @param queryParameters + * Parameters for updating the bookmark + * @return Response containing the updated bookmark + */ + @PUT + @Path("/{bookmarkUUID}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Update an existing bookmark in an experiment", responses = { + @ApiResponse(responseCode = "200", description = "Bookmark updated successfully", content = @Content(schema = @Schema(implementation = org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.Bookmark.class))), + @ApiResponse(responseCode = "400", description = INVALID_PARAMETERS, content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "404", description = "Experiment or bookmark not found", content = @Content(schema = @Schema(implementation = String.class))) + }) + public Response updateBookmark( + @Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID, + @Parameter(description = "Bookmark UUID") @PathParam("bookmarkUUID") UUID bookmarkUUID, + @RequestBody(content = { + @Content(schema = @Schema(implementation = BookmarkQueryParameters.class)) + }, required = true) QueryParameters queryParameters) { + + if (queryParameters == null) { + return Response.status(Status.BAD_REQUEST).entity(MISSING_PARAMETERS).build(); + } + + Map parameters = queryParameters.getParameters(); + String errorMessage = QueryParametersUtil.validateBookmarkQueryParameters(parameters); + if (errorMessage != null) { + return Response.status(Status.BAD_REQUEST).entity(errorMessage).build(); + } + + TmfExperiment experiment = ExperimentManagerService.getExperimentByUUID(expUUID); + if (experiment == null) { + return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); + } + + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + String name = Objects.requireNonNull((String) parameters.get(BOOKMARK_NAME)); + long start = Objects.requireNonNull((Number) parameters.get(BOOKMARK_START)).longValue(); + long end = Objects.requireNonNull((Number) parameters.get(BOOKMARK_END)).longValue(); + + Bookmark bookmark = new Bookmark(bookmarkUUID, name, start, end); + + try { + IMarker[] markers = findBookmarkMarkers(editorFile); + IMarker marker = findMarkerByUUID(markers, bookmarkUUID); + if (marker == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + setMarkerAttributes(marker, bookmark); + return Response.ok(bookmark).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to update bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Delete a bookmark from an experiment + * + * @param expUUID + * UUID of the experiment + * @param bookmarkUUID + * UUID of the bookmark to delete + * @return Response containing the deleted bookmark + */ + @DELETE + @Path("/{bookmarkUUID}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Delete a bookmark from an experiment", responses = { + @ApiResponse(responseCode = "200", description = "Bookmark deleted successfully", content = @Content(schema = @Schema(implementation = org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.Bookmark.class))), + @ApiResponse(responseCode = "404", description = "Experiment or bookmark not found", content = @Content(schema = @Schema(implementation = String.class))) + }) + public Response deleteBookmark( + @Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID, + @Parameter(description = "Bookmark UUID") @PathParam("bookmarkUUID") UUID bookmarkUUID) { + + TmfExperiment experiment = ExperimentManagerService.getExperimentByUUID(expUUID); + if (experiment == null) { + return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); + } + + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + try { + IMarker[] markers = findBookmarkMarkers(editorFile); + IMarker marker = findMarkerByUUID(markers, bookmarkUUID); + if (marker == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + Bookmark bookmark = markerToBookmark(marker); + if (bookmark == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + marker.delete(); + return Response.ok(bookmark).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to delete bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Generate a random UUID for a new bookmark + */ + private static UUID generateUUID(IFile editorFile) throws CoreException { + IMarker[] markers = findBookmarkMarkers(editorFile); + while (true) { + UUID uuid = UUID.randomUUID(); + + // Check if the UUID hasn't been used yet + if (findMarkerByUUID(markers, uuid) == null) { + return uuid; + } + } + } + + /** + * Find all bookmark markers in the editor file + */ + private static IMarker[] findBookmarkMarkers(IFile editorFile) throws CoreException { + return editorFile.findMarkers(IMarker.BOOKMARK, false, IResource.DEPTH_ZERO); + } + + /** + * Convert a marker to a Bookmark object + */ + private static Bookmark markerToBookmark(IMarker marker) { + String uuid = marker.getAttribute(BOOKMARK_UUID, (String) null); + if (uuid == null) { + return null; + } + + String name = marker.getAttribute(IMarker.MESSAGE, (String) null); + if (name == null) { + return null; + } + + String startStr = marker.getAttribute(ITmfMarker.MARKER_TIME, (String) null); + if (startStr == null) { + return null; + } + long start; + try { + start = Long.parseLong(startStr); + } catch (NumberFormatException e) { + return null; + } + + long duration = 0; + String durationStr = marker.getAttribute(ITmfMarker.MARKER_DURATION, (String) null); + if (durationStr != null) { + try { + duration = Long.parseLong(durationStr); + } catch (NumberFormatException e) { + return null; + } + } + long end = start + duration; + + return new Bookmark(UUID.fromString(uuid), name, start, end); + } + + /** + * Find a specific bookmark marker by UUID + */ + private static IMarker findMarkerByUUID(IMarker[] markers, UUID bookmarkUUID) { + for (IMarker marker : markers) { + String uuid = marker.getAttribute(BOOKMARK_UUID, (String) null); + if (uuid != null && UUID.fromString(uuid).equals(bookmarkUUID)) { + return marker; + } + } + return null; + } + + /** + * Create a new marker with bookmark attributes + */ + private static void setMarkerAttributes(IMarker marker, Bookmark bookmark) throws CoreException { + Long duration = bookmark.getEnd() - bookmark.getStart(); + + marker.setAttribute(BOOKMARK_UUID, bookmark.getUUID().toString()); + marker.setAttribute(IMarker.MESSAGE, bookmark.getName()); + marker.setAttribute(ITmfMarker.MARKER_TIME, Long.toString(bookmark.getStart())); + + if (duration > 0) { + marker.setAttribute(ITmfMarker.MARKER_DURATION, Long.toString(duration)); + marker.setAttribute(IMarker.LOCATION, + NLS.bind("timestamp [{0}, {1}]", //$NON-NLS-1$ + TmfTimestamp.fromNanos(bookmark.getStart()), + TmfTimestamp.fromNanos(bookmark.getEnd()))); + } else { + marker.setAttribute(IMarker.LOCATION, + NLS.bind("timestamp [{0}]", //$NON-NLS-1$ + TmfTimestamp.fromNanos(bookmark.getStart()))); + } + + marker.setAttribute(ITmfMarker.MARKER_COLOR, BOOKMARK_DEFAULT_COLOR); + } + + /** + * Convert list of markers to list of bookmarks + */ + private static List markersToBookmarks(IMarker[] markers) { + List bookmarks = new ArrayList<>(); + for (IMarker marker : markers) { + Bookmark bookmark = markerToBookmark(marker); + if (bookmark != null) { + bookmarks.add(bookmark); + } + } + return bookmarks; + } +} \ No newline at end of file diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/EndpointConstants.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/EndpointConstants.java index c883d1511..b56ab6eeb 100644 --- a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/EndpointConstants.java +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/EndpointConstants.java @@ -84,7 +84,7 @@ public final class EndpointConstants { static final String LICENSE = "Apache 2"; //$NON-NLS-1$ static final String LICENSE_URL = "http://www.apache.org/licenses/"; //$NON-NLS-1$ /** The TSP version */ - public static final String VERSION = "0.2.0"; //$NON-NLS-1$ + public static final String VERSION = "0.3.0"; //$NON-NLS-1$ static final String SERVER = "https://localhost:8080/tsp/api"; //$NON-NLS-1$ /** @@ -92,6 +92,7 @@ public final class EndpointConstants { * 3-letters so they align in {@link DataProviderService}; readability. */ static final String ANN = "Annotations"; //$NON-NLS-1$ + static final String BKM = "Bookmarks"; //$NON-NLS-1$ static final String CFG = "Configurations"; //$NON-NLS-1$ static final String DIA = "Diagnostic"; //$NON-NLS-1$ static final String DT = "Data Tree"; //$NON-NLS-1$ @@ -184,6 +185,7 @@ public final class EndpointConstants { static final String TREE_ENTRIES = "Unique entry point for output providers, to get the tree of visible entries"; //$NON-NLS-1$ static final String NO_SUCH_CONFIGURATION = "No such configuration source type or configuration instance"; //$NON-NLS-1$ static final String PROVIDER_CONFIG_NOT_FOUND = "Experiment, output provider or configuration type not found"; //$NON-NLS-1$ + static final String BOOKMARK_NOT_FOUND = "Bookmark not found"; //$NON-NLS-1$ private EndpointConstants() { // private constructor diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/QueryParametersUtil.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/QueryParametersUtil.java index 09b172fce..e13f7f8fc 100644 --- a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/QueryParametersUtil.java +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/QueryParametersUtil.java @@ -440,4 +440,46 @@ private static String validateRequestedElement(Map params) { } return null; } + + /** + * Validate bookmark query parameters. + * + * @param params + * the map of query parameters + * @return an error message if validation fails, or null otherwise + */ + public static String validateBookmarkQueryParameters(Map params) { + String errorMessage; + // Validate name parameter + if ((errorMessage = validateString(NAME, params)) != null) { + return errorMessage; + } + + // Validate start and end times + Object startObj = params.get(START); + Object endObj = params.get(END); + + if (startObj == null) { + return MISSING_PARAMETERS + SEP + START; + } + if (endObj == null) { + return MISSING_PARAMETERS + SEP + END; + } + + if (!(startObj instanceof Number)) { + return INVALID_PARAMETERS + SEP + START; + } + if (!(endObj instanceof Number)) { + return INVALID_PARAMETERS + SEP + END; + } + + long start = ((Number) startObj).longValue(); + long end = ((Number) endObj).longValue(); + + if (start > end) { + return INVALID_PARAMETERS + SEP + "Start time cannot be after end time"; //$NON-NLS-1$ + } + + return null; + } } diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/TraceServerOpenApiResource.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/TraceServerOpenApiResource.java index e01bada96..2b27b5248 100644 --- a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/TraceServerOpenApiResource.java +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/services/TraceServerOpenApiResource.java @@ -12,6 +12,7 @@ package org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services; import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.ANN; +import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.BKM; import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.CFG; import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.DESC; import static org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.EndpointConstants.DIA; @@ -48,6 +49,7 @@ @Server(url = SERVER) }, tags = { @Tag(name = ANN, description = "Retrieve annotations for different outputs."), + @Tag(name = BKM, description = "Bookmark areas of interest in the experiment."), @Tag(name = CFG, description = "Manage configuration source types and configurations."), @Tag(name = DIA, description = "Retrieve the server's status."), @Tag(name = DT, description = "Query data tree models (e.g. for statistics)."), diff --git a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/webapp/WebApplication.java b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/webapp/WebApplication.java index 7a901855e..3b2db4334 100644 --- a/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/webapp/WebApplication.java +++ b/trace-server/org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core/src/org/eclipse/tracecompass/incubator/internal/trace/server/jersey/rest/core/webapp/WebApplication.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.BookmarkManagerService; import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.ConfigurationManagerService; import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.DataProviderService; import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.services.ExperimentManagerService; @@ -143,6 +144,7 @@ protected void registerResourcesAndMappers(ResourceConfig rc) { rc.register(JacksonObjectMapperProvider.class); EncodingFilter.enableFor(rc, GZipEncoder.class); rc.register(TraceServerOpenApiResource.class); + rc.register(BookmarkManagerService.class); } /**