/* * Copyright (c) 2020, Salesforce.com, Inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ package com.salesforce.cantor.http.resources; import com.google.gson.Gson; import com.salesforce.cantor.Cantor; 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.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.ws.rs.BeanParam; 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.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @Component @Path("/sets") @Tag(name = "Sets Resource", description = "Api for handling Cantor Sets") public class SetsResource { private static final Logger logger = LoggerFactory.getLogger(SetsResource.class); private static final String serverErrorMessage = "Internal server error occurred"; private static final Gson parser = new Gson(); private static final String jsonFieldResults = "results"; private static final String jsonFieldSize = "size"; private static final String jsonFieldWeight = "weight"; private final Cantor cantor; @Autowired public SetsResource(final Cantor cantor) { this.cantor = cantor; } @GET @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get all sets namespaces") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides the list of all namespaces", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response getNamespaces() throws IOException { logger.info("received request for all sets namespaces"); return Response.ok(parser.toJson(this.cantor.sets().namespaces())).build(); } @PUT @Path("/{namespace}") @Operation(summary = "Create a new set namespace") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Set namespace was created or already existed"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response create(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace) throws IOException { logger.info("received request for creation of namespace {}", namespace); this.cantor.sets().create(namespace); return Response.ok().build(); } @DELETE @Path("/{namespace}") @Operation(summary = "Drop a set namespace") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Set namespace was dropped or didn't exist"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response drop(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace) throws IOException { logger.info("received request to drop namespace {}", namespace); this.cantor.sets().drop(namespace); return Response.ok().build(); } @PUT @Path("/{namespace}/{set}/{entry}/{weight}") @Operation(summary = "Add or overwrite an entry in a set") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Entry was successfully added or its weight was updated"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response add(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @Parameter(description = "Name of the entry") @PathParam("entry") final String entry, @Parameter(description = "Weight of the entry") @PathParam("weight") final long weight) throws IOException { logger.info("received request to add entry with weight {}:{} in set/namespace {}/{}", entry, weight, set, namespace); this.cantor.sets().add(namespace, set, entry, weight); return Response.ok().build(); } @GET @Path("/entries/{namespace}/{set}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get entry names from a set") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides entry names matching query parameters as a list", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "400", description = "One of the query parameters has a bad value"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response entries(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @BeanParam final SetsDataSourceBean bean) throws IOException { logger.info("received request for entries in set/namespace {}/{}", set, namespace); logger.debug("request parameters: {}", bean); final Collection<String> entries = this.cantor.sets().entries( namespace, set, bean.getMin(), bean.getMax(), bean.getStart(), bean.getCount(), bean.isAscending()); return Response.ok(parser.toJson(entries)).build(); } @GET @Path("/{namespace}/{set}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get entries from a set") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides entry names and weights matching query parameters as properties in a json", content = @Content(schema = @Schema(implementation = Map.class))), @ApiResponse(responseCode = "400", description = "One of the query parameters has a bad value"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response get(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @BeanParam final SetsDataSourceBean bean) throws IOException { logger.info("received request for values in set/namespace {}/{}", set, namespace); logger.debug("request parameters: {}", bean); final Map<String, Long> entries = this.cantor.sets().get( namespace, set, bean.getMin(), bean.getMax(), bean.getStart(), bean.getCount(), bean.isAscending()); return Response.ok(parser.toJson(entries)).build(); } @DELETE @Path("/{namespace}/{set}") @Operation(summary = "Delete entries in a set between provided weights") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully deleted entries between and including provided weights"), @ApiResponse(responseCode = "400", description = "One of the query parameters has a bad value"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response delete(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @Parameter(description = "Minimum weight for an entry", example = "0") @QueryParam("min") final long min, @Parameter(description = "Maximum weight for an entry", example = "0") @QueryParam("max") final long max) throws IOException { logger.info("received request to delete entries in set/namespace {}/{} between weights {}-{}", set, namespace, min, max); this.cantor.sets().delete(namespace, set, min, max); return Response.ok().build(); } @DELETE @Path("/{namespace}/{set}/{entry}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Delete a specific entry by name") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides single property json with a boolean which is only true if the key was found and the entry was deleted", content = @Content(schema = @Schema(implementation = HttpModels.DeleteResponse.class))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response delete(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @Parameter(description = "Name of the entry") @PathParam("entry") final String entry) throws IOException { logger.info("received request to delete entry {} in set/namespace {}/{}", entry, set, namespace); final Map<String, Boolean> completed = new HashMap<>(); completed.put(jsonFieldResults, this.cantor.sets().delete(namespace, set, entry)); return Response.ok(parser.toJson(completed)).build(); } @GET @Path("/union/{namespace}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Perform a union of all provided sets") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides the union of all entries filtered by query parameters as properties in a json", content = @Content(schema = @Schema(implementation = Map.class))), @ApiResponse(responseCode = "400", description = "One of the query parameters has a bad value"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response union(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @QueryParam("set") final List<String> sets, @BeanParam final SetsDataSourceBean bean) throws IOException { logger.info("received request for union of sets {} in namespace {}", sets, namespace); logger.debug("request parameters: {}", bean); final Map<String, Long> union = this.cantor.sets().union( namespace, sets, bean.getMin(), bean.getMax(), bean.getStart(), bean.getCount(), bean.isAscending()); return Response.ok(parser.toJson(union)).build(); } @GET @Path("/intersect/{namespace}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Perform an intersection of all provided sets") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides an intersection of all entries filtered by query parameters as properties in a json", content = @Content(schema = @Schema(implementation = Map.class))), @ApiResponse(responseCode = "400", description = "One of the query parameters has a bad value"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response intersect(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "List of sets") @QueryParam("set") final List<String> sets, @BeanParam final SetsDataSourceBean bean) throws IOException { logger.info("received request for intersection of sets {} in namespace {}", sets, namespace); logger.debug("request parameters: {}", bean); final Map<String, Long> intersection = this.cantor.sets().intersect( namespace, sets, bean.getMin(), bean.getMax(), bean.getStart(), bean.getCount(), bean.isAscending()); return Response.ok(parser.toJson(intersection)).build(); } @DELETE @Path("/pop/{namespace}/{set}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Pop entries from a set") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Entries and weights of elements popped matching query parameters", content = @Content(schema = @Schema(implementation = Map.class))), @ApiResponse(responseCode = "400", description = "One of the query parameters has a bad value"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response pop(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @BeanParam final SetsDataSourceBean bean) throws IOException { logger.info("received request to pop off set/namespace {}/{}", set, namespace); logger.debug("request parameters: {}", bean); final Map<String, Long> entries = this.cantor.sets().pop( namespace, set, bean.getMin(), bean.getMax(), bean.getStart(), bean.getCount(), bean.isAscending()); return Response.ok(parser.toJson(entries)).build(); } @GET @Path("/{namespace}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get list of all sets in a namespace") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides a list of all sets in namespace", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response sets(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace) throws IOException { logger.info("received request for all sets in namespace {}", namespace); return Response.ok(parser.toJson(this.cantor.sets().sets(namespace))).build(); } @GET @Path("/size/{namespace}/{set}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get number of entries in a set") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides single property json with the size of the set", content = @Content(schema = @Schema(implementation = HttpModels.SizeResponse.class))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response size(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set) throws IOException { logger.info("received request for size of set/namespace {}/{}", set, namespace); final Map<String, Integer> size = new HashMap<>(); size.put(jsonFieldSize, this.cantor.sets().size(namespace, set)); return Response.ok(parser.toJson(size)).build(); } @GET @Path("/weight/{namespace}/{set}/{entry}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get weight of a specific entry in a set") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides single property json with the weight of the entry if it exists", content = @Content(schema = @Schema(implementation = HttpModels.WeightResponse.class))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response weight(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @Parameter(description = "Name of the entry") @PathParam("entry") final String entry) throws IOException { logger.info("received request for weight of entry {} in set/namespace {}/{}", entry, set, namespace); final Map<String, Long> weight = new HashMap<>(); weight.put(jsonFieldWeight, this.cantor.sets().weight(namespace, set, entry)); return Response.ok(parser.toJson(weight)).build(); } @POST @Path("/{namespace}/{set}/{entry}/{count}") @Operation(summary = "Atomically increment the weight of an entry and return the value after increment") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully incremented entry by count"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response inc(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace, @Parameter(description = "Name of the set") @PathParam("set") final String set, @Parameter(description = "Name of the entry") @PathParam("entry") final String entry, @Parameter(description = "Amount to increment", example = "10") @PathParam("count") final long count) throws IOException { logger.info("received request to increment entry {} in set/namespace {}/{} by {}", entry, set, namespace, count); final Map<String, Long> results = new HashMap<>(); results.put(jsonFieldResults, this.cantor.sets().inc(namespace, set, entry, count)); return Response.ok(results).build(); } protected static class SetsDataSourceBean { @Parameter(description = "Minimum weight for an entry", example = "0") @QueryParam("min") private long min; @Parameter(description = "Maximum weight for an entry", example = "-1") @QueryParam("max") private long max; @Parameter(description = "Index from which to start counting", example = "0") @QueryParam("start") private int start; @Parameter(description = "Number of entries allowed in response", example = "10") @QueryParam("count") private int count; @Parameter(description = "Return in ascending or descending format", example = "false") @QueryParam("asc") private boolean ascending; /* * Getters and setter are required for Swagger to process the Jersey bean * Swagger does not currently support the BeanParam annotation with a constructor */ public long getMin() { return min; } public long getMax() { if (this.max == -1) { this.max = Long.MAX_VALUE; logger.info("setting bean max to Long.MAX_VALUE"); } return max; } public int getStart() { return start; } public int getCount() { return count; } public boolean isAscending() { return ascending; } public void setMin(final long min) { this.min = min; } public void setMax(final long max) { this.max = max; } public void setStart(final int start) { this.start = start; } public void setCount(final int count) { this.count = count; } public void setAscending(final boolean ascending) { this.ascending = ascending; } } }