/* * 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 com.salesforce.cantor.functions.Functions; import com.salesforce.cantor.functions.FunctionsOnCantor; 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.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.math.BigDecimal; import java.util.*; @Component @Path("/functions") @Tag(name = "Functions Resource", description = "Api for handling Cantor functions") public class FunctionsResource { private static final Logger logger = LoggerFactory.getLogger(FunctionsResource.class); private static final String serverErrorMessage = "Internal server error occurred"; private static final Gson parser = new Gson(); private final Cantor cantor; private final Functions functions; @Autowired public FunctionsResource(final Cantor cantor) { this.cantor = cantor; this.functions = new FunctionsOnCantor(cantor); } @PUT @Path("/{namespace}") @Operation(summary = "Create a new function namespace") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Function namespace was created or already exists"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response createNamespace(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace) throws IOException { logger.info("received request to drop namespace {}", namespace); this.functions.create(namespace); return Response.ok().build(); } @DELETE @Path("/{namespace}") @Operation(summary = "Drop a function namespace") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Function namespace was dropped or didn't exist"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response dropNamespace(@Parameter(description = "Namespace identifier") @PathParam("namespace") final String namespace) throws IOException { logger.info("received request to drop namespace {}", namespace); this.functions.drop(namespace); return Response.ok().build(); } @GET @Path("/{namespace}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get list of all functions in the given namespace") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides the list of all functions in the namespace", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response getFunctions(@PathParam("namespace") final String namespace) throws IOException { logger.info("received request for all objects namespaces"); return Response.ok(parser.toJson(this.functions.list(namespace))).build(); } @GET @Path("/{namespace}/{function}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get a function") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Provides the function with the given name", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response getFunction(@PathParam("namespace") final String namespace, @PathParam("function") final String functionName) throws IOException { final byte[] bytes = this.functions.get(namespace, functionName); if (bytes == null) { return Response.status(Response.Status.NOT_FOUND).build(); } return Response.ok(bytes).build(); } @GET @Path("/run/{namespace}/{function}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Execute 'get' method on a function") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Process and execute the function", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response getExecuteFunction(@PathParam("namespace") final String namespace, @PathParam("function") final String function, @Context final HttpServletRequest request, @Context final HttpServletResponse response) { logger.info("executing '{}/{}' with get method", namespace, function); return doExecute(namespace, function, request, response); } @PUT @Path("/run/{namespace}/{function}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Execute put method on function query") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Process and execute the function", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response putExecuteFunction(@PathParam("namespace") final String namespace, @PathParam("function") final String function, @Context final HttpServletRequest request, @Context final HttpServletResponse response) { logger.info("executing '{}/{}' with put method", namespace, function); return doExecute(namespace, function, request, response); } @POST @Path("/run/{namespace}/{function}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Execute post method on function query") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Process and execute the function", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response postExecuteFunction(@PathParam("namespace") final String namespace, @PathParam("function") final String function, @Context final HttpServletRequest request, @Context final HttpServletResponse response) { logger.info("executing '{}/{}' with post method", namespace, function); return doExecute(namespace, function, request, response); } @DELETE @Path("/run/{namespace}/{function}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Execute delete method on function query") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Process and execute the function", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response deleteExecuteFunction(@PathParam("namespace") final String namespace, @PathParam("function") final String function, @Context final HttpServletRequest request, @Context final HttpServletResponse response) { logger.info("executing '{}/{}' with delete method", namespace, function); return doExecute(namespace, function, request, response); } @PUT @Path("/{namespace}/{function}") @Operation(summary = "Store a function") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Function stored"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response create(@Parameter(description = "Namespace") @PathParam("namespace") final String namespace, @Parameter(description = "Function identifier") @PathParam("function") final String functionName, final String body) { try { this.functions.store(namespace, functionName, body); return Response.status(Response.Status.CREATED).build(); } catch (IOException e) { return Response.status(Response.Status.BAD_REQUEST) .entity(toString(e)) .build(); } } @DELETE @Path("/{namespace}/{function}") @Operation(summary = "Remove a function") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Function removed"), @ApiResponse(responseCode = "500", description = serverErrorMessage) }) public Response drop(@Parameter(description = "Namespace") @PathParam("namespace") final String namespace, @Parameter(description = "Namespace identifier") @PathParam("function") final String function) { try { this.functions.delete(namespace, function); return Response.ok().build(); } catch (IOException e) { return Response.status(Response.Status.BAD_REQUEST) .entity(toString(e)) .build(); } } private Response doExecute(final String namespace, final String function, final HttpServletRequest request, final HttpServletResponse response) { try { final com.salesforce.cantor.functions.Context context = new com.salesforce.cantor.functions.Context(this.cantor, this.functions); // special parameters, http.request and http.response are passed to functions context.set("http.request", request); context.set("http.response", response); this.functions.run(namespace, function, context, getParams(request)); // retrieve special parameter http.status from context final Object statusObject = context.get("http.status"); final int status; if (statusObject instanceof String) { status = Integer.parseInt((String) statusObject); } else if (statusObject instanceof BigDecimal) { status = ((BigDecimal) statusObject).intValue(); } else if (statusObject instanceof Long) { status = ((Long) statusObject).intValue(); } else if (statusObject instanceof Integer) { status = (int) statusObject; } else { status = Response.Status.OK.getStatusCode(); } final Response.ResponseBuilder builder = Response.status(status); // retrieve special parameter .out from context if (context.get(".out") != null) { builder.entity(context.get(".out")); } // retrieve special parameter http.headers from context if (context.get("http.headers") != null) { for (final Map.Entry<String, Object> header : ((Map<String, Object>) context.get("http.headers")).entrySet()) { builder.header(header.getKey(), header.getValue()); } } return builder.build(); } catch (Exception e) { return Response.serverError() .header("Content-Type", "text/plain") .entity(toString(e)) .build(); } } // convert http request query string parameters to a map of string to string private Map<String, String> getParams(final HttpServletRequest request) { final Map<String, String> params = new HashMap<>(); for (final Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) { params.put(entry.getKey(), entry.getValue()[0]); } return params; } // convert throwable object stack trace to string private String toString(final Throwable throwable) { final StringWriter writer = new StringWriter(); final PrintWriter printer = new PrintWriter(writer); throwable.printStackTrace(printer); return writer.toString(); } }