From a4e77bf951e8cbd4048af119c4ea30d86c0b1141 Mon Sep 17 00:00:00 2001 From: Kaveh Shahedi Date: Fri, 25 Oct 2024 00:08:44 -0400 Subject: [PATCH 1/4] server: Add bookmarks endpoint support Using bookmarks, user can save custom data points where each corresponds to a specific time range. Each bookmark contains "name", "start", "end", and an optional "payload". The following endpoints are introduced: - [GET] Get all the bookmarks of an experiment - [GET] Get a specific bookmark of an experiment - [POST] Create a new bookmark for an experiment - [PUT] Update an old bookmark of an experiment - [DELETE] Delete a bookmark of an experiment [Added] Corresponding endpoints for bookmarks in trace server Signed-off-by: Kaveh Shahedi --- .../jersey/rest/core/model/Bookmark.java | 63 +++ .../core/model/BookmarkQueryParameters.java | 56 +++ .../jersey/rest/core/services/Bookmark.java | 136 +++++ .../core/services/BookmarkManagerService.java | 474 ++++++++++++++++++ .../rest/core/services/EndpointConstants.java | 2 + .../core/services/QueryParametersUtil.java | 64 +++ .../rest/core/webapp/WebApplication.java | 2 + 7 files changed, 797 insertions(+) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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..5a6da9fea --- /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,63 @@ +/******************************************************************************* + * 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 + * @since 10.1 + */ +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 experiment ID. + */ + @NonNull + @Schema(description = "The experiment's unique identifier this bookmark belongs to") + String getExperimentId(); + + /** + * @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..05e2e83a4 --- /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,56 @@ +/******************************************************************************* + * 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 + * @since 10.1 + */ +public interface BookmarkQueryParameters { + + /** + * @return The bookmark parameters + */ + @JsonProperty("parameters") + @Schema(description = "The bookmark parameters", requiredMode = RequiredMode.REQUIRED) + BookmarkParameters getParameters(); + + + 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..0bafdb8ce --- /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,136 @@ +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; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Bookmark model for TSP + * + * @author Kaveh Shahedi + * @since 10.1 + */ +public class Bookmark implements Serializable { + private static final long serialVersionUID = -3626414315455912960L; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final UUID fUUID; + private final String fName; + private final String fExperimentId; + private final long fStart; + private final long fEnd; + private final JsonNode fPayload; + + /** + * {@link JsonCreator} Constructor for final fields + * + * @param uuid + * the stub's UUID + * @param name + * bookmark name + * @param experimentId + * experiment id + * @param start + * start time + * @param end + * end time + * @param payload + * additional JSON data associated with the bookmark (optional) + */ + @JsonCreator + public Bookmark( + @JsonProperty("UUID") UUID uuid, + @JsonProperty("name") String name, + @JsonProperty("experimentId") String experimentId, + @JsonProperty("start") long start, + @JsonProperty("end") long end, + @JsonProperty(value = "payload", required = false) JsonNode payload) { + fUUID = uuid; + fName = name; + fExperimentId = experimentId; + fStart = start; + fEnd = end; + fPayload = (payload != null) ? payload : MAPPER.createObjectNode(); + } + + /** + * Constructor without payload + * + * @param uuid + * the stub's UUID + * @param name + * bookmark name + * @param experimentId + * experiment id + * @param start + * start time + * @param end + * end time + */ + public Bookmark(UUID uuid, String name, String experimentId, long start, long end) { + this(uuid, name, experimentId, start, end, MAPPER.createObjectNode()); + } + + /** + * 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 experiment id + * + * @return the experiment id + */ + public String getExperimentId() { + return fExperimentId; + } + + /** + * 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; + } + + /** + * Get the payload + * + * @return the JSON payload, empty JSON object if no payload was set + */ + public JsonNode getPayload() { + return fPayload; + } + + @Override + public String toString() { + return "Bookmark [fUUID=" + fUUID + ", fName=" + fName + ", fExperimentId=" + fExperimentId //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + ", fStart=" + fStart + ", fEnd=" + fEnd + ", fPayload=" + fPayload + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } +} \ 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..c307dcdae --- /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,474 @@ +/******************************************************************************* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +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.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceVisitor; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.Activator; +import org.eclipse.tracecompass.incubator.internal.trace.server.jersey.rest.core.model.views.QueryParameters; +import org.eclipse.tracecompass.tmf.core.TmfCommonConstants; +import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment; + +import com.google.common.collect.Lists; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +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 + * @since 10.1 + */ +@Path("/experiments/{expUUID}/bookmarks") +@Tag(name = BKM) +public class BookmarkManagerService { + + private static final Map> EXPERIMENT_BOOKMARKS = Collections.synchronizedMap(initBookmarkResources()); + private static final String BOOKMARKS_FOLDER = "Bookmarks"; //$NON-NLS-1$ + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static Map> initBookmarkResources() { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IProject project = root.getProject(TmfCommonConstants.DEFAULT_TRACE_PROJECT_NAME); + Map> experimentBookmarks = new HashMap<>(); + try { + project.refreshLocal(IResource.DEPTH_INFINITE, null); + IFolder bookmarksFolder = project.getFolder(BOOKMARKS_FOLDER); + bookmarksFolder.accept((IResourceVisitor) resource -> { + if (resource.equals(bookmarksFolder)) { + return true; + } + if (resource instanceof IFolder) { + UUID expUUID = UUID.fromString(Objects.requireNonNull(resource.getName())); + Map bookmarks = loadBookmarks((IFolder) resource); + if (!bookmarks.isEmpty()) { + experimentBookmarks.put(expUUID, bookmarks); + } + } + return false; + }, IResource.DEPTH_ONE, IResource.NONE); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to load bookmarks", e); //$NON-NLS-1$ + } + return experimentBookmarks; + } + + private static Map loadBookmarks(IFolder experimentFolder) throws CoreException { + Map bookmarks = new HashMap<>(); + experimentFolder.accept(resource -> { + if (resource instanceof IFile && resource.getName().endsWith(".bookmark")) { //$NON-NLS-1$ + try (ObjectInputStream ois = new ObjectInputStream(((IFile) resource).getContents(true))) { + Bookmark bookmark = (Bookmark) ois.readObject(); + bookmarks.put(bookmark.getUUID(), bookmark); + } catch (Exception e) { + Activator.getInstance().logError("Failed to load bookmark", e); //$NON-NLS-1$ + } + } + return true; + }); + return bookmarks; + } + + /** + * 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 = 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(); + } + + synchronized (EXPERIMENT_BOOKMARKS) { + List bookmarks = Lists.transform(new ArrayList<>(EXPERIMENT_BOOKMARKS.getOrDefault(expUUID, Collections.emptyMap()).values()), bookmark -> bookmark); + return Response.ok(bookmarks).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 = 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(); + } + + Map bookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); + if (bookmarks == null || !bookmarks.containsKey(bookmarkUUID)) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + return Response.ok(bookmarks.get(bookmarkUUID)).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 = 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(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(); + } + + String name = Objects.requireNonNull((String) parameters.get("name")); //$NON-NLS-1$ + long start = Objects.requireNonNull((Number) parameters.get("start")).longValue(); //$NON-NLS-1$ + long end = Objects.requireNonNull((Number) parameters.get("end")).longValue(); //$NON-NLS-1$ + + // Handle payload, defaulting to empty JSON object if not provided + JsonNode payload = null; + Object rawPayload = parameters.get("payload"); + if (rawPayload != null) { + try { + if (rawPayload instanceof String) { + payload = MAPPER.readTree((String) rawPayload); + } else { + payload = MAPPER.valueToTree(rawPayload); + } + } catch (IOException e) { + return Response.status(Status.BAD_REQUEST).entity("Invalid payload format").build(); + } + } + + try { + IFolder bookmarkFolder = getBookmarkFolder(expUUID); + UUID bookmarkUUID = UUID.nameUUIDFromBytes(Objects.requireNonNull(name.getBytes(Charset.defaultCharset()))); + + // Check if bookmark already exists + Map existingBookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); + if (existingBookmarks != null && existingBookmarks.containsKey(bookmarkUUID)) { + Bookmark existingBookmark = Objects.requireNonNull(existingBookmarks.get(bookmarkUUID)); + // Check if it's the same bookmark (same start and end times) + if (existingBookmark.getStart() != start || existingBookmark.getEnd() != end) { + // It's a different bookmark with the same name, return conflict + return Response.status(Status.CONFLICT).entity(existingBookmark).build(); + } + // It's the same bookmark, return it + return Response.ok(existingBookmark).build(); + } + + createFolder(bookmarkFolder); + + Bookmark bookmark = new Bookmark(bookmarkUUID, name, expUUID.toString(), start, end, payload); + + // Save to file system + IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + + oos.writeObject(bookmark); + oos.flush(); + + if (bookmarkFile.exists()) { + bookmarkFile.setContents(new ByteArrayInputStream(baos.toByteArray()), IResource.FORCE, null); + } else { + bookmarkFile.create(new ByteArrayInputStream(baos.toByteArray()), true, null); + } + } catch (IOException e) { + Activator.getInstance().logError("Failed to create bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + + // Add to memory + Map bookmarks = EXPERIMENT_BOOKMARKS.computeIfAbsent(expUUID, k -> new HashMap<>()); + bookmarks.put(bookmarkUUID, 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 = 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(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(); + } + + Map bookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); + if (bookmarks == null || !bookmarks.containsKey(bookmarkUUID)) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + String name = Objects.requireNonNull((String) parameters.get("name")); //$NON-NLS-1$ + long start = Objects.requireNonNull((Number) parameters.get("start")).longValue(); //$NON-NLS-1$ + long end = Objects.requireNonNull((Number) parameters.get("end")).longValue(); //$NON-NLS-1$ + + // Handle payload, defaulting to empty JSON object if not provided + JsonNode payload = null; + Object rawPayload = parameters.get("payload"); + if (rawPayload != null) { + try { + if (rawPayload instanceof String) { + payload = MAPPER.readTree((String) rawPayload); + } else { + payload = MAPPER.valueToTree(rawPayload); + } + } catch (IOException e) { + return Response.status(Status.BAD_REQUEST).entity("Invalid payload format").build(); + } + } + + try { + IFolder bookmarkFolder = getBookmarkFolder(expUUID); + Bookmark updatedBookmark = new Bookmark(bookmarkUUID, name, expUUID.toString(), start, end, payload); + + // Update file system + IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ + if (bookmarkFile.exists()) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + + oos.writeObject(updatedBookmark); + oos.flush(); + + bookmarkFile.setContents(new ByteArrayInputStream(baos.toByteArray()), IResource.FORCE, null); + // Update memory + bookmarks.put(bookmarkUUID, updatedBookmark); + return Response.ok(updatedBookmark).build(); + } + } + + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to update bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } catch (IOException 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 = 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(); + } + + Map bookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); + if (bookmarks == null || !bookmarks.containsKey(bookmarkUUID)) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } + + try { + IFolder bookmarkFolder = getBookmarkFolder(expUUID); + IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ + Bookmark deletedBookmark = bookmarks.remove(bookmarkUUID); + + if (bookmarkFile.exists()) { + bookmarkFile.delete(true, null); + } + + if (bookmarks.isEmpty()) { + EXPERIMENT_BOOKMARKS.remove(expUUID); + if (bookmarkFolder.exists()) { + bookmarkFolder.delete(true, null); + } + } + + return Response.ok(deletedBookmark).build(); + } catch (CoreException e) { + Activator.getInstance().logError("Failed to delete bookmark", e); //$NON-NLS-1$ + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Gets the Eclipse resource folder for the bookmark. + * + * @param expUUID + * UUID of the experiment + * @return The Eclipse resource folder + * + * @throws CoreException + * if an error occurs + */ + private static @NonNull IFolder getBookmarkFolder(UUID expUUID) throws CoreException { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IProject project = root.getProject(TmfCommonConstants.DEFAULT_TRACE_PROJECT_NAME); + project.refreshLocal(IResource.DEPTH_INFINITE, null); + IFolder bookmarksFolder = project.getFolder(BOOKMARKS_FOLDER); + return Objects.requireNonNull(bookmarksFolder.getFolder(expUUID.toString())); + } + + private static void createFolder(IFolder folder) throws CoreException { + if (!folder.exists()) { + if (folder.getParent() instanceof IFolder) { + createFolder((IFolder) folder.getParent()); + } + folder.create(true, true, null); + } + } + + /** + * Dispose method to be only called at server shutdown. + */ + public static void dispose() { + EXPERIMENT_BOOKMARKS.clear(); + } +} \ 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..c0456b9a5 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 @@ -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..cb3f38e18 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 @@ -33,6 +33,10 @@ import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphArrow; import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphState; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Utility methods to validate and convert query parameters from the input Trace * Server Protocol to the output data provider interfaces. @@ -61,6 +65,7 @@ private interface ElementType { private static final String TIME = "time"; //$NON-NLS-1$ private static final String TRACES = "traces"; //$NON-NLS-1$ private static final String URI = "uri"; //$NON-NLS-1$ + private static final String PAYLOAD = "payload"; //$NON-NLS-1$ private static final long MAX_NBTIMES = 1 << 16; private static final @NonNull OutputElementStyle EMPTY_STYLE = new OutputElementStyle(null, Collections.emptyMap()); @@ -440,4 +445,63 @@ 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$ + } + + // Validate payload + Object payload = params.get(PAYLOAD); + if (payload != null) { + // Check if it is a JSON parseable object + try { + if (payload instanceof String) { + // Try parsing string as JSON + new ObjectMapper().readTree((String) payload); + } else if (!(payload instanceof Map) && !(payload instanceof List) && !(payload instanceof JsonNode)) { + // If not a string, should be a Map, List, or already a JsonNode + return INVALID_PARAMETERS + SEP + "Payload must be a valid JSON structure"; //$NON-NLS-1$ + } + } catch (JsonProcessingException e) { + return INVALID_PARAMETERS + SEP + "Invalid JSON payload format"; //$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/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); } /** From 7bc9b76dd7cd3812e69738423f5db4ef8be0887f Mon Sep 17 00:00:00 2001 From: Kaveh Shahedi Date: Tue, 5 Nov 2024 17:34:34 -0500 Subject: [PATCH 2/4] server: Add unit tests for BookmarkManagerService Required unit tests have been implemented for BookmarkServiceManager class. In the tests, various aspects of bookmarking functionality (e.g., creating, updating, deleting, etc.) are checked. [Added] Required unit tests for bookmarking functionality Signed-off-by: Kaveh Shahedi --- .../services/BookmarkManagerServiceTest.java | 352 ++++++++++++++++++ .../core/tests/stubs/BookmarkModelStub.java | 162 ++++++++ .../stubs/webapp/TestWebApplication.java | 2 + .../rest/core/tests/utils/RestServerTest.java | 4 + .../jersey/rest/core/model/Bookmark.java | 9 +- .../jersey/rest/core/services/Bookmark.java | 70 +--- .../core/services/BookmarkManagerService.java | 52 +-- .../core/services/QueryParametersUtil.java | 22 -- 8 files changed, 550 insertions(+), 123 deletions(-) create mode 100644 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 create mode 100644 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 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..2b5c5a2f3 --- /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,352 @@ +/******************************************************************************* + * 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.Map; + +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.Before; +import org.junit.Test; + +/** + * Test class for BookmarkManagerService + * + * @author Kaveh Shahedi + * @since 10.1 + */ +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; + + /** + * Setup method to run before each test. Creates a clean experiment and removes all + * existing bookmarks. + */ + @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("Experiment UUID should not be null", experiment.getUUID()); + + // Get all existing bookmarks and delete them + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + Response response = bookmarkTarget.request().get(); + assertEquals("GET request for bookmarks should return 200", 200, response.getStatus()); + + if (response.getStatus() == 200) { + BookmarkModelStub[] existingBookmarks = response.readEntity(BookmarkModelStub[].class); + assertNotNull("Bookmark array should not be null", existingBookmarks); + + for (BookmarkModelStub bookmark : existingBookmarks) { + Response deleteResponse = bookmarkTarget.path(bookmark.getUUID().toString()) + .request() + .delete(); + assertEquals("DELETE request should return 200", 200, deleteResponse.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); + + 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 and end times + parameters.put(NAME, BOOKMARK_NAME); + parameters.put("start", "not a number"); + parameters.put("end", "not a number"); + + response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + assertEquals("Should return 400 for non-numeric times", 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); + + 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); + + Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + assertEquals("Response status should be 200", 200, response.getStatus()); + + BookmarkModelStub expStub = response.readEntity(BookmarkModelStub.class); + assertNotNull("Response body should not be null", expStub); + assertEquals("Bookmark name should match", BOOKMARK.getName(), expStub.getName()); + assertEquals("Start time should match", BOOKMARK.getStart(), expStub.getStart()); + assertEquals("End time should match", BOOKMARK.getEnd(), expStub.getEnd()); + assertNotNull("UUID should not be null", expStub.getUUID()); + } + + /** + * Test the creation of a bookmark with a repetitive name. + */ + @Test + public void testCreateBookmarkRepetitiveName() { + WebTarget application = getApplicationEndpoint(); + WebTarget bookmarkTarget = application.path(EXPERIMENTS) + .path(experiment.getUUID().toString()) + .path(BOOKMARKS); + + // Create first bookmark + Map parameters = new HashMap<>(); + parameters.put(NAME, BOOKMARK.getName()); + parameters.put("start", START_TIME); + parameters.put("end", END_TIME); + + Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + BookmarkModelStub firstBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals("First bookmark creation should succeed", 200, response.getStatus()); + assertNotNull("First bookmark should not be null", firstBookmark); + + // Try to create second bookmark with same name but different times + parameters.replace("start", START_TIME + 1); + parameters.replace("end", END_TIME + 1); + + response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + assertEquals("Should return conflict for duplicate name", 409, response.getStatus()); + + // Verify the original bookmark wasn't modified + Response getResponse = bookmarkTarget.path(firstBookmark.getUUID().toString()).request().get(); + BookmarkModelStub retrievedBookmark = getResponse.readEntity(BookmarkModelStub.class); + assertEquals("Original bookmark should remain unchanged", firstBookmark, retrievedBookmark); + } + + /** + * 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 + 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"); + 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"); + response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + assertEquals("Second bookmark creation should succeed", 200, response.getStatus()); + + // Get all bookmarks + 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("Start time should match", START_TIME, bookmark.getStart()); + assertEquals("End time should 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); + + Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + assertEquals("Bookmark creation should succeed", 200, response.getStatus()); + BookmarkModelStub createdBookmark = response.readEntity(BookmarkModelStub.class); + + // Test getting non-existent bookmark + Response nonExistentResponse = bookmarkTarget.path("non-existent-uuid").request().get(); + assertEquals("Should return 404 for non-existent bookmark", 404, nonExistentResponse.getStatus()); + + // Test getting existing bookmark + 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); + + Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + BookmarkModelStub originalBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals("Initial bookmark creation should succeed", 200, response.getStatus()); + + // Test updating non-existent bookmark + WebTarget nonExistentTarget = bookmarkTarget.path("non-existent-uuid"); + Response nonExistentResponse = nonExistentTarget.request() + .put(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + assertEquals("Should return 404 for non-existent bookmark", 404, nonExistentResponse.getStatus()); + + // Test updating with invalid parameters + parameters.put("start", END_TIME); + parameters.put("end", START_TIME); + 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); + + 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("Updated bookmark should not be null", updatedBookmark); + assertEquals("UUID should remain 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 + Response nonExistentResponse = bookmarkTarget.path("non-existent-uuid") + .request() + .delete(); + assertEquals("Should return 404 for non-existent bookmark", 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); + + Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); + BookmarkModelStub createdBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals("Bookmark creation should succeed", 200, response.getStatus()); + + // Delete the bookmark + 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 + 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 + Response getAllResponse = bookmarkTarget.request().get(); + BookmarkModelStub[] allBookmarks = getAllResponse.readEntity(BookmarkModelStub[].class); + assertEquals("Should have no bookmarks after deletion", 0, allBookmarks.length); + } +} \ 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..e3f8e3c5c --- /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,162 @@ +/******************************************************************************* + * 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.nio.charset.Charset; +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 + * @since 10.1 + */ +@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(getUUID(name), name, start, end); + } + + private static UUID getUUID(String name) { + return UUID.nameUUIDFromBytes(Objects.requireNonNull(name.getBytes(Charset.defaultCharset()))); + } + + /** + * 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 index 5a6da9fea..61e66cbd6 100644 --- 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 @@ -30,7 +30,7 @@ public interface Bookmark { /** * @return The bookmark UUID. */ - @JsonProperty("UUID") + @JsonProperty("uuid") @Schema(description = "The bookmark's unique identifier") UUID getUUID(); @@ -41,13 +41,6 @@ public interface Bookmark { @Schema(description = "User defined name for the bookmark") String getName(); - /** - * @return The experiment ID. - */ - @NonNull - @Schema(description = "The experiment's unique identifier this bookmark belongs to") - String getExperimentId(); - /** * @return The start time. */ 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 index 0bafdb8ce..d787a3acb 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/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 @@ -1,11 +1,19 @@ +/******************************************************************************* + * 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; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; /** * Bookmark model for TSP @@ -14,15 +22,13 @@ * @since 10.1 */ public class Bookmark implements Serializable { - private static final long serialVersionUID = -3626414315455912960L; - private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final long serialVersionUID = 6126770413230064175L; private final UUID fUUID; private final String fName; - private final String fExperimentId; private final long fStart; private final long fEnd; - private final JsonNode fPayload; /** * {@link JsonCreator} Constructor for final fields @@ -31,47 +37,21 @@ public class Bookmark implements Serializable { * the stub's UUID * @param name * bookmark name - * @param experimentId - * experiment id * @param start * start time * @param end * end time - * @param payload - * additional JSON data associated with the bookmark (optional) */ @JsonCreator public Bookmark( - @JsonProperty("UUID") UUID uuid, + @JsonProperty("uuid") UUID uuid, @JsonProperty("name") String name, - @JsonProperty("experimentId") String experimentId, @JsonProperty("start") long start, - @JsonProperty("end") long end, - @JsonProperty(value = "payload", required = false) JsonNode payload) { + @JsonProperty("end") long end) { fUUID = uuid; fName = name; - fExperimentId = experimentId; fStart = start; fEnd = end; - fPayload = (payload != null) ? payload : MAPPER.createObjectNode(); - } - - /** - * Constructor without payload - * - * @param uuid - * the stub's UUID - * @param name - * bookmark name - * @param experimentId - * experiment id - * @param start - * start time - * @param end - * end time - */ - public Bookmark(UUID uuid, String name, String experimentId, long start, long end) { - this(uuid, name, experimentId, start, end, MAPPER.createObjectNode()); } /** @@ -92,15 +72,6 @@ public String getName() { return fName; } - /** - * Get the experiment id - * - * @return the experiment id - */ - public String getExperimentId() { - return fExperimentId; - } - /** * Get the start time * @@ -119,18 +90,9 @@ public long getEnd() { return fEnd; } - /** - * Get the payload - * - * @return the JSON payload, empty JSON object if no payload was set - */ - public JsonNode getPayload() { - return fPayload; - } - @Override public String toString() { - return "Bookmark [fUUID=" + fUUID + ", fName=" + fName + ", fExperimentId=" + fExperimentId //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - + ", fStart=" + fStart + ", fEnd=" + fEnd + ", fPayload=" + fPayload + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + 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 index c307dcdae..8d3dbc721 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/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 @@ -53,15 +53,13 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.annotation.NonNull; 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.TmfCommonConstants; import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment; import com.google.common.collect.Lists; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -84,8 +82,6 @@ public class BookmarkManagerService { private static final Map> EXPERIMENT_BOOKMARKS = Collections.synchronizedMap(initBookmarkResources()); private static final String BOOKMARKS_FOLDER = "Bookmarks"; //$NON-NLS-1$ - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static Map> initBookmarkResources() { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IProject project = root.getProject(TmfCommonConstants.DEFAULT_TRACE_PROJECT_NAME); @@ -93,6 +89,10 @@ private static Map> initBookmarkResources() { try { project.refreshLocal(IResource.DEPTH_INFINITE, null); IFolder bookmarksFolder = project.getFolder(BOOKMARKS_FOLDER); + // Check if the folder exists. If not, create it + if (!bookmarksFolder.exists()) { + bookmarksFolder.create(true, true, null); + } bookmarksFolder.accept((IResourceVisitor) resource -> { if (resource.equals(bookmarksFolder)) { return true; @@ -205,7 +205,9 @@ public Response getBookmark( }) public Response createBookmark( @Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID, - @RequestBody(required = true) QueryParameters queryParameters) { + @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(); @@ -226,21 +228,6 @@ public Response createBookmark( long start = Objects.requireNonNull((Number) parameters.get("start")).longValue(); //$NON-NLS-1$ long end = Objects.requireNonNull((Number) parameters.get("end")).longValue(); //$NON-NLS-1$ - // Handle payload, defaulting to empty JSON object if not provided - JsonNode payload = null; - Object rawPayload = parameters.get("payload"); - if (rawPayload != null) { - try { - if (rawPayload instanceof String) { - payload = MAPPER.readTree((String) rawPayload); - } else { - payload = MAPPER.valueToTree(rawPayload); - } - } catch (IOException e) { - return Response.status(Status.BAD_REQUEST).entity("Invalid payload format").build(); - } - } - try { IFolder bookmarkFolder = getBookmarkFolder(expUUID); UUID bookmarkUUID = UUID.nameUUIDFromBytes(Objects.requireNonNull(name.getBytes(Charset.defaultCharset()))); @@ -260,7 +247,7 @@ public Response createBookmark( createFolder(bookmarkFolder); - Bookmark bookmark = new Bookmark(bookmarkUUID, name, expUUID.toString(), start, end, payload); + Bookmark bookmark = new Bookmark(bookmarkUUID, name, start, end); // Save to file system IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ @@ -314,7 +301,9 @@ public Response createBookmark( public Response updateBookmark( @Parameter(description = EXP_UUID) @PathParam("expUUID") UUID expUUID, @Parameter(description = "Bookmark UUID") @PathParam("bookmarkUUID") UUID bookmarkUUID, - @RequestBody(required = true) QueryParameters queryParameters) { + @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(); @@ -340,24 +329,9 @@ public Response updateBookmark( long start = Objects.requireNonNull((Number) parameters.get("start")).longValue(); //$NON-NLS-1$ long end = Objects.requireNonNull((Number) parameters.get("end")).longValue(); //$NON-NLS-1$ - // Handle payload, defaulting to empty JSON object if not provided - JsonNode payload = null; - Object rawPayload = parameters.get("payload"); - if (rawPayload != null) { - try { - if (rawPayload instanceof String) { - payload = MAPPER.readTree((String) rawPayload); - } else { - payload = MAPPER.valueToTree(rawPayload); - } - } catch (IOException e) { - return Response.status(Status.BAD_REQUEST).entity("Invalid payload format").build(); - } - } - try { IFolder bookmarkFolder = getBookmarkFolder(expUUID); - Bookmark updatedBookmark = new Bookmark(bookmarkUUID, name, expUUID.toString(), start, end, payload); + Bookmark updatedBookmark = new Bookmark(bookmarkUUID, name, start, end); // Update file system IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ 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 cb3f38e18..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 @@ -33,10 +33,6 @@ import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphArrow; import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphState; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - /** * Utility methods to validate and convert query parameters from the input Trace * Server Protocol to the output data provider interfaces. @@ -65,7 +61,6 @@ private interface ElementType { private static final String TIME = "time"; //$NON-NLS-1$ private static final String TRACES = "traces"; //$NON-NLS-1$ private static final String URI = "uri"; //$NON-NLS-1$ - private static final String PAYLOAD = "payload"; //$NON-NLS-1$ private static final long MAX_NBTIMES = 1 << 16; private static final @NonNull OutputElementStyle EMPTY_STYLE = new OutputElementStyle(null, Collections.emptyMap()); @@ -485,23 +480,6 @@ public static String validateBookmarkQueryParameters(Map params) return INVALID_PARAMETERS + SEP + "Start time cannot be after end time"; //$NON-NLS-1$ } - // Validate payload - Object payload = params.get(PAYLOAD); - if (payload != null) { - // Check if it is a JSON parseable object - try { - if (payload instanceof String) { - // Try parsing string as JSON - new ObjectMapper().readTree((String) payload); - } else if (!(payload instanceof Map) && !(payload instanceof List) && !(payload instanceof JsonNode)) { - // If not a string, should be a Map, List, or already a JsonNode - return INVALID_PARAMETERS + SEP + "Payload must be a valid JSON structure"; //$NON-NLS-1$ - } - } catch (JsonProcessingException e) { - return INVALID_PARAMETERS + SEP + "Invalid JSON payload format"; //$NON-NLS-1$ - } - } - return null; } } From 0e3bab16dbe16424137f64506497110dea51dbfa Mon Sep 17 00:00:00 2001 From: Kaveh Shahedi Date: Thu, 14 Nov 2024 16:12:23 -0500 Subject: [PATCH 3/4] server: Use standard IMarker structure for storing bookmarks The new changes aim to re-structure the algorithm of storing bookmarks. Previously, we stored each bookmark (of an experiment) under a specific directory inside the .webapp directory of Trace Compass. Right now, we are using Eclipse's markers system to store and fetch the benchmarks. [Changed] Bookmark storing/fetching system is changed to IMarker Signed-off-by: Kaveh Shahedi --- .../services/BookmarkManagerServiceTest.java | 399 +++++++++++------- .../core/tests/stubs/BookmarkModelStub.java | 8 +- .../jersey/rest/core/model/Bookmark.java | 1 - .../core/model/BookmarkQueryParameters.java | 4 +- .../jersey/rest/core/services/Bookmark.java | 1 - .../core/services/BookmarkManagerService.java | 361 ++++++++-------- 6 files changed, 439 insertions(+), 335 deletions(-) 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 index 2b5c5a2f3..fabc5bef6 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/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 @@ -15,7 +15,10 @@ 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; @@ -26,6 +29,7 @@ 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; @@ -33,8 +37,8 @@ * Test class for BookmarkManagerService * * @author Kaveh Shahedi - * @since 10.1 */ +@SuppressWarnings("null") public class BookmarkManagerServiceTest extends RestServerTest { private static final String BOOKMARK_NAME = "TEST"; @@ -43,40 +47,101 @@ public class BookmarkManagerServiceTest extends RestServerTest { 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. Creates a clean experiment and removes all - * existing bookmarks. + * 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); + CONTEXT_SWITCHES_UST_NOT_INITIALIZED_STUB); assertNotNull("Experiment should not be null", experiment); - assertNotNull("Experiment UUID should not be null", experiment.getUUID()); + assertNotNull(NON_NULL_UUID, experiment.getUUID()); + } - // Get all existing bookmarks and delete them + /** + * 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); - Response response = bookmarkTarget.request().get(); - assertEquals("GET request for bookmarks should return 200", 200, response.getStatus()); + try (Response response = bookmarkTarget.request().get()) { + assertEquals("GET request for bookmarks should return 200", 200, response.getStatus()); - if (response.getStatus() == 200) { BookmarkModelStub[] existingBookmarks = response.readEntity(BookmarkModelStub[].class); assertNotNull("Bookmark array should not be null", existingBookmarks); for (BookmarkModelStub bookmark : existingBookmarks) { - Response deleteResponse = bookmarkTarget.path(bookmark.getUUID().toString()) - .request() - .delete(); - assertEquals("DELETE request should return 200", 200, deleteResponse.getStatus()); + 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. */ @@ -90,27 +155,35 @@ public void testCreateBookmarkInvalidParams() { // Test with null name Map parameters = new HashMap<>(); parameters.put(NAME, null); - parameters.put("start", START_TIME); - parameters.put("end", END_TIME); + 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()); - 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 and end times + // Test with non-numeric start parameters.put(NAME, BOOKMARK_NAME); - parameters.put("start", "not a number"); - parameters.put("end", "not a number"); + 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()); + } - response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Should return 400 for non-numeric times", 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); + 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()); + } - response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Should return 400 for invalid time range", 400, response.getStatus()); } /** @@ -125,52 +198,81 @@ public void testCreateBookmark() { Map parameters = new HashMap<>(); parameters.put(NAME, BOOKMARK.getName()); - parameters.put("start", START_TIME); - parameters.put("end", END_TIME); - - Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Response status should be 200", 200, response.getStatus()); - - BookmarkModelStub expStub = response.readEntity(BookmarkModelStub.class); - assertNotNull("Response body should not be null", expStub); - assertEquals("Bookmark name should match", BOOKMARK.getName(), expStub.getName()); - assertEquals("Start time should match", BOOKMARK.getStart(), expStub.getStart()); - assertEquals("End time should match", BOOKMARK.getEnd(), expStub.getEnd()); - assertNotNull("UUID should not be null", expStub.getUUID()); + 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 a repetitive name. + * Test the creation of a bookmark with no end time (i.e., just start time). */ @Test - public void testCreateBookmarkRepetitiveName() { + public void testCreateBookmarkNoEndTime() { WebTarget application = getApplicationEndpoint(); WebTarget bookmarkTarget = application.path(EXPERIMENTS) .path(experiment.getUUID().toString()) .path(BOOKMARKS); - // Create first bookmark Map parameters = new HashMap<>(); parameters.put(NAME, BOOKMARK.getName()); - parameters.put("start", START_TIME); - parameters.put("end", END_TIME); - - Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - BookmarkModelStub firstBookmark = response.readEntity(BookmarkModelStub.class); - assertEquals("First bookmark creation should succeed", 200, response.getStatus()); - assertNotNull("First bookmark should not be null", firstBookmark); + 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()); + } + } - // Try to create second bookmark with same name but different times - parameters.replace("start", START_TIME + 1); - parameters.replace("end", END_TIME + 1); + /** + * 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); - response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Should return conflict for duplicate name", 409, response.getStatus()); + 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()); + } + } - // Verify the original bookmark wasn't modified - Response getResponse = bookmarkTarget.path(firstBookmark.getUUID().toString()).request().get(); - BookmarkModelStub retrievedBookmark = getResponse.readEntity(BookmarkModelStub.class); - assertEquals("Original bookmark should remain unchanged", firstBookmark, retrievedBookmark); } /** @@ -184,38 +286,42 @@ public void testGetAllBookmarks() { .path(BOOKMARKS); // Initially there should be no bookmarks - Response response = bookmarkTarget.request().get(); - BookmarkModelStub[] initialBookmarks = response.readEntity(BookmarkModelStub[].class); - assertEquals("Should start with no bookmarks", 0, initialBookmarks.length); + 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); + parameters.put(START, START_TIME); + parameters.put(END, END_TIME); // Create first bookmark parameters.put(NAME, "Bookmark1"); - response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("First bookmark creation should succeed", 200, response.getStatus()); + 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"); - response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Second bookmark creation should succeed", 200, response.getStatus()); + 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 - 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("Start time should match", START_TIME, bookmark.getStart()); - assertEquals("End time should match", END_TIME, bookmark.getEnd()); - assertTrue("Name should be either Bookmark1 or Bookmark2", - bookmark.getName().equals("Bookmark1") || bookmark.getName().equals("Bookmark2")); + 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")); + } } } @@ -232,23 +338,28 @@ public void testGetSpecificBookmark() { // Create a bookmark Map parameters = new HashMap<>(); parameters.put(NAME, BOOKMARK.getName()); - parameters.put("start", START_TIME); - parameters.put("end", END_TIME); - - Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Bookmark creation should succeed", 200, response.getStatus()); - BookmarkModelStub createdBookmark = response.readEntity(BookmarkModelStub.class); + 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 - Response nonExistentResponse = bookmarkTarget.path("non-existent-uuid").request().get(); - assertEquals("Should return 404 for non-existent bookmark", 404, nonExistentResponse.getStatus()); + try (Response nonExistentResponse = bookmarkTarget.path(experiment.getUUID().toString()).request().get()) { + assertEquals(NON_EXISTENT_BOOKMARK_STATUS_CODE, 404, nonExistentResponse.getStatus()); + } // Test getting existing bookmark - response = bookmarkTarget.path(createdBookmark.getUUID().toString()).request().get(); - assertEquals("Should successfully get existing bookmark", 200, response.getStatus()); + 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); + BookmarkModelStub retrievedBookmark = response.readEntity(BookmarkModelStub.class); + assertEquals("Retrieved bookmark should match created bookmark", createdBookmark, retrievedBookmark); + } } /** @@ -264,44 +375,37 @@ public void testUpdateBookmark() { // Create initial bookmark Map parameters = new HashMap<>(); parameters.put(NAME, BOOKMARK.getName()); - parameters.put("start", START_TIME); - parameters.put("end", END_TIME); - - Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - BookmarkModelStub originalBookmark = response.readEntity(BookmarkModelStub.class); - assertEquals("Initial bookmark creation should succeed", 200, response.getStatus()); - - // Test updating non-existent bookmark - WebTarget nonExistentTarget = bookmarkTarget.path("non-existent-uuid"); - Response nonExistentResponse = nonExistentTarget.request() - .put(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - assertEquals("Should return 404 for non-existent bookmark", 404, nonExistentResponse.getStatus()); + 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); - 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()); + 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); - - 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("Updated bookmark should not be null", updatedBookmark); - assertEquals("UUID should remain 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()); + 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()); + } } /** @@ -315,38 +419,41 @@ public void testDeleteBookmark() { .path(BOOKMARKS); // Try deleting non-existent bookmark - Response nonExistentResponse = bookmarkTarget.path("non-existent-uuid") - .request() - .delete(); - assertEquals("Should return 404 for non-existent bookmark", 404, nonExistentResponse.getStatus()); + 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); - - Response response = bookmarkTarget.request().post(Entity.json(new QueryParameters(parameters, Collections.emptyList()))); - BookmarkModelStub createdBookmark = response.readEntity(BookmarkModelStub.class); - assertEquals("Bookmark creation should succeed", 200, response.getStatus()); + 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 - 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); + 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 - Response getResponse = bookmarkTarget.path(createdBookmark.getUUID().toString()) - .request() - .get(); - assertEquals("Should return 404 for deleted bookmark", 404, getResponse.getStatus()); + 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 - Response getAllResponse = bookmarkTarget.request().get(); - BookmarkModelStub[] allBookmarks = getAllResponse.readEntity(BookmarkModelStub[].class); - assertEquals("Should have no bookmarks after deletion", 0, allBookmarks.length); + 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 index e3f8e3c5c..ad3c45c20 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/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 @@ -12,7 +12,6 @@ package org.eclipse.tracecompass.incubator.trace.server.jersey.rest.core.tests.stubs; import java.io.Serializable; -import java.nio.charset.Charset; import java.util.Objects; import java.util.UUID; @@ -25,7 +24,6 @@ * BookmarkModel schema * * @author Kaveh Shahedi - * @since 10.1 */ @JsonIgnoreProperties(ignoreUnknown = true) public class BookmarkModelStub implements Serializable { @@ -71,11 +69,7 @@ public BookmarkModelStub( * end time */ public BookmarkModelStub(String name, long start, long end) { - this(getUUID(name), name, start, end); - } - - private static UUID getUUID(String name) { - return UUID.nameUUIDFromBytes(Objects.requireNonNull(name.getBytes(Charset.defaultCharset()))); + this(UUID.randomUUID(), name, start, end); } /** 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 index 61e66cbd6..b85b33bab 100644 --- 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 @@ -23,7 +23,6 @@ * Contributes to the model used for TSP swagger-core annotations. * * @author Kaveh Shahedi - * @since 10.1 */ public interface Bookmark { 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 index 05e2e83a4..e948710ca 100644 --- 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 @@ -19,7 +19,6 @@ * Parameters for bookmark creation and update operations * * @author Kaveh Shahedi - * @since 10.1 */ public interface BookmarkQueryParameters { @@ -31,6 +30,9 @@ public interface BookmarkQueryParameters { BookmarkParameters getParameters(); + /** + * Bookmark parameters + */ interface BookmarkParameters { /** * @return The bookmark name 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 index d787a3acb..83a451fb6 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/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 @@ -19,7 +19,6 @@ * Bookmark model for TSP * * @author Kaveh Shahedi - * @since 10.1 */ public class Bookmark implements Serializable { 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 index 8d3dbc721..020dc2a95 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/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 @@ -17,15 +17,8 @@ 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.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -44,22 +37,18 @@ import javax.ws.rs.core.Response.Status; import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IResourceVisitor; -import org.eclipse.core.resources.IWorkspaceRoot; -import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; -import org.eclipse.jdt.annotation.NonNull; +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.TmfCommonConstants; +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 com.google.common.collect.Lists; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -73,60 +62,18 @@ * Service to manage bookmarks for experiments * * @author Kaveh Shahedi - * @since 10.1 */ @Path("/experiments/{expUUID}/bookmarks") @Tag(name = BKM) public class BookmarkManagerService { - private static final Map> EXPERIMENT_BOOKMARKS = Collections.synchronizedMap(initBookmarkResources()); - private static final String BOOKMARKS_FOLDER = "Bookmarks"; //$NON-NLS-1$ - - private static Map> initBookmarkResources() { - IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); - IProject project = root.getProject(TmfCommonConstants.DEFAULT_TRACE_PROJECT_NAME); - Map> experimentBookmarks = new HashMap<>(); - try { - project.refreshLocal(IResource.DEPTH_INFINITE, null); - IFolder bookmarksFolder = project.getFolder(BOOKMARKS_FOLDER); - // Check if the folder exists. If not, create it - if (!bookmarksFolder.exists()) { - bookmarksFolder.create(true, true, null); - } - bookmarksFolder.accept((IResourceVisitor) resource -> { - if (resource.equals(bookmarksFolder)) { - return true; - } - if (resource instanceof IFolder) { - UUID expUUID = UUID.fromString(Objects.requireNonNull(resource.getName())); - Map bookmarks = loadBookmarks((IFolder) resource); - if (!bookmarks.isEmpty()) { - experimentBookmarks.put(expUUID, bookmarks); - } - } - return false; - }, IResource.DEPTH_ONE, IResource.NONE); - } catch (CoreException e) { - Activator.getInstance().logError("Failed to load bookmarks", e); //$NON-NLS-1$ - } - return experimentBookmarks; - } + // 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 Map loadBookmarks(IFolder experimentFolder) throws CoreException { - Map bookmarks = new HashMap<>(); - experimentFolder.accept(resource -> { - if (resource instanceof IFile && resource.getName().endsWith(".bookmark")) { //$NON-NLS-1$ - try (ObjectInputStream ois = new ObjectInputStream(((IFile) resource).getContents(true))) { - Bookmark bookmark = (Bookmark) ois.readObject(); - bookmarks.put(bookmark.getUUID(), bookmark); - } catch (Exception e) { - Activator.getInstance().logError("Failed to load bookmark", e); //$NON-NLS-1$ - } - } - return true; - }); - return bookmarks; - } + private static final String BOOKMARK_DEFAULT_COLOR = "RGBA {255, 0, 0, 128}"; //$NON-NLS-1$ /** * Retrieve all bookmarks for a specific experiment @@ -138,7 +85,7 @@ private static Map loadBookmarks(IFolder experimentFolder) throw @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 = Bookmark.class)))), + @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) { @@ -147,9 +94,18 @@ public Response getBookmarks(@Parameter(description = EXP_UUID) @PathParam("expU return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); } - synchronized (EXPERIMENT_BOOKMARKS) { - List bookmarks = Lists.transform(new ArrayList<>(EXPERIMENT_BOOKMARKS.getOrDefault(expUUID, Collections.emptyMap()).values()), bookmark -> bookmark); + 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(); } } @@ -166,7 +122,7 @@ public Response getBookmarks(@Parameter(description = EXP_UUID) @PathParam("expU @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 = Bookmark.class))), + @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( @@ -178,12 +134,28 @@ public Response getBookmark( return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); } - Map bookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); - if (bookmarks == null || !bookmarks.containsKey(bookmarkUUID)) { + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); } - return Response.ok(bookmarks.get(bookmarkUUID)).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(); + } } /** @@ -199,7 +171,7 @@ public Response getBookmark( @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 = Bookmark.class))), + @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))) }) @@ -224,53 +196,21 @@ public Response createBookmark( return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); } - String name = Objects.requireNonNull((String) parameters.get("name")); //$NON-NLS-1$ - long start = Objects.requireNonNull((Number) parameters.get("start")).longValue(); //$NON-NLS-1$ - long end = Objects.requireNonNull((Number) parameters.get("end")).longValue(); //$NON-NLS-1$ + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); + } try { - IFolder bookmarkFolder = getBookmarkFolder(expUUID); - UUID bookmarkUUID = UUID.nameUUIDFromBytes(Objects.requireNonNull(name.getBytes(Charset.defaultCharset()))); - - // Check if bookmark already exists - Map existingBookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); - if (existingBookmarks != null && existingBookmarks.containsKey(bookmarkUUID)) { - Bookmark existingBookmark = Objects.requireNonNull(existingBookmarks.get(bookmarkUUID)); - // Check if it's the same bookmark (same start and end times) - if (existingBookmark.getStart() != start || existingBookmark.getEnd() != end) { - // It's a different bookmark with the same name, return conflict - return Response.status(Status.CONFLICT).entity(existingBookmark).build(); - } - // It's the same bookmark, return it - return Response.ok(existingBookmark).build(); - } - - createFolder(bookmarkFolder); - - Bookmark bookmark = new Bookmark(bookmarkUUID, name, start, end); + 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); - // Save to file system - IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) { - - oos.writeObject(bookmark); - oos.flush(); - - if (bookmarkFile.exists()) { - bookmarkFile.setContents(new ByteArrayInputStream(baos.toByteArray()), IResource.FORCE, null); - } else { - bookmarkFile.create(new ByteArrayInputStream(baos.toByteArray()), true, null); - } - } catch (IOException e) { - Activator.getInstance().logError("Failed to create bookmark", e); //$NON-NLS-1$ - return Response.status(Status.INTERNAL_SERVER_ERROR).build(); - } - - // Add to memory - Map bookmarks = EXPERIMENT_BOOKMARKS.computeIfAbsent(expUUID, k -> new HashMap<>()); - bookmarks.put(bookmarkUUID, bookmark); + 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$ @@ -294,7 +234,7 @@ public Response createBookmark( @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 = Bookmark.class))), + @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))) }) @@ -320,42 +260,29 @@ public Response updateBookmark( return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); } - Map bookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); - if (bookmarks == null || !bookmarks.containsKey(bookmarkUUID)) { + 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("name")); //$NON-NLS-1$ - long start = Objects.requireNonNull((Number) parameters.get("start")).longValue(); //$NON-NLS-1$ - long end = Objects.requireNonNull((Number) parameters.get("end")).longValue(); //$NON-NLS-1$ + 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 { - IFolder bookmarkFolder = getBookmarkFolder(expUUID); - Bookmark updatedBookmark = new Bookmark(bookmarkUUID, name, start, end); - - // Update file system - IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ - if (bookmarkFile.exists()) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) { - - oos.writeObject(updatedBookmark); - oos.flush(); - - bookmarkFile.setContents(new ByteArrayInputStream(baos.toByteArray()), IResource.FORCE, null); - // Update memory - bookmarks.put(bookmarkUUID, updatedBookmark); - return Response.ok(updatedBookmark).build(); - } + IMarker[] markers = findBookmarkMarkers(editorFile); + IMarker marker = findMarkerByUUID(markers, bookmarkUUID); + if (marker == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); } - 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(); - } catch (IOException e) { - Activator.getInstance().logError("Failed to update bookmark", e); //$NON-NLS-1$ - return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } @@ -372,7 +299,7 @@ public Response updateBookmark( @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 = Bookmark.class))), + @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( @@ -384,28 +311,25 @@ public Response deleteBookmark( return Response.status(Status.NOT_FOUND).entity(NO_SUCH_EXPERIMENT).build(); } - Map bookmarks = EXPERIMENT_BOOKMARKS.get(expUUID); - if (bookmarks == null || !bookmarks.containsKey(bookmarkUUID)) { + IFile editorFile = TmfTraceManager.getInstance().getTraceEditorFile(experiment); + if (editorFile == null) { return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); } try { - IFolder bookmarkFolder = getBookmarkFolder(expUUID); - IFile bookmarkFile = bookmarkFolder.getFile(bookmarkUUID.toString() + ".bookmark"); //$NON-NLS-1$ - Bookmark deletedBookmark = bookmarks.remove(bookmarkUUID); - - if (bookmarkFile.exists()) { - bookmarkFile.delete(true, null); + IMarker[] markers = findBookmarkMarkers(editorFile); + IMarker marker = findMarkerByUUID(markers, bookmarkUUID); + if (marker == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); } - if (bookmarks.isEmpty()) { - EXPERIMENT_BOOKMARKS.remove(expUUID); - if (bookmarkFolder.exists()) { - bookmarkFolder.delete(true, null); - } + Bookmark bookmark = markerToBookmark(marker); + if (bookmark == null) { + return Response.status(Status.NOT_FOUND).entity(EndpointConstants.BOOKMARK_NOT_FOUND).build(); } - return Response.ok(deletedBookmark).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(); @@ -413,36 +337,115 @@ public Response deleteBookmark( } /** - * Gets the Eclipse resource folder for the bookmark. - * - * @param expUUID - * UUID of the experiment - * @return The Eclipse resource folder - * - * @throws CoreException - * if an error occurs + * Generate a random UUID for a new bookmark */ - private static @NonNull IFolder getBookmarkFolder(UUID expUUID) throws CoreException { - IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); - IProject project = root.getProject(TmfCommonConstants.DEFAULT_TRACE_PROJECT_NAME); - project.refreshLocal(IResource.DEPTH_INFINITE, null); - IFolder bookmarksFolder = project.getFolder(BOOKMARKS_FOLDER); - return Objects.requireNonNull(bookmarksFolder.getFolder(expUUID.toString())); + 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); } - private static void createFolder(IFolder folder) throws CoreException { - if (!folder.exists()) { - if (folder.getParent() instanceof IFolder) { - createFolder((IFolder) folder.getParent()); + /** + * 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; } - folder.create(true, true, null); } + return null; } /** - * Dispose method to be only called at server shutdown. + * Create a new marker with bookmark attributes */ - public static void dispose() { - EXPERIMENT_BOOKMARKS.clear(); + 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 From 1c09447a7fd5ac8066dda62f9ef8a93071a1cae9 Mon Sep 17 00:00:00 2001 From: Kaveh Shahedi Date: Fri, 22 Nov 2024 12:58:33 -0500 Subject: [PATCH 4/4] server: Update the trace server version to 0.3.0 The version is now updated to 0.3.0 from 0.2.0. Most of the changes are related to managing experiments' bookmarks. Also, the bookmarks tag (BKM) is added to TraceServerOpenApiResource class. [Changed] Trace server version is changed to 0.3.0 Signed-off-by: Kaveh Shahedi --- .../server/jersey/rest/core/services/EndpointConstants.java | 2 +- .../jersey/rest/core/services/TraceServerOpenApiResource.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 c0456b9a5..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$ /** 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)."),