| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license |
| * agreements. See the NOTICE file distributed with this work for additional information regarding |
| * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. You may obtain a |
| * copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package org.apache.geode.rest.internal.web.controllers; |
| |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import io.swagger.annotations.Api; |
| import io.swagger.annotations.ApiOperation; |
| import io.swagger.annotations.ApiResponse; |
| import io.swagger.annotations.ApiResponses; |
| import org.apache.logging.log4j.Logger; |
| import org.springframework.http.HttpHeaders; |
| import org.springframework.http.HttpStatus; |
| import org.springframework.http.MediaType; |
| import org.springframework.http.ResponseEntity; |
| import org.springframework.security.access.prepost.PreAuthorize; |
| import org.springframework.stereotype.Controller; |
| import org.springframework.web.bind.annotation.PathVariable; |
| import org.springframework.web.bind.annotation.RequestBody; |
| import org.springframework.web.bind.annotation.RequestMapping; |
| import org.springframework.web.bind.annotation.RequestMethod; |
| import org.springframework.web.bind.annotation.RequestParam; |
| import org.springframework.web.bind.annotation.ResponseBody; |
| import org.springframework.web.bind.annotation.ResponseStatus; |
| |
| import org.apache.geode.cache.Region; |
| import org.apache.geode.cache.query.FunctionDomainException; |
| import org.apache.geode.cache.query.NameResolutionException; |
| import org.apache.geode.cache.query.Query; |
| import org.apache.geode.cache.query.QueryExecutionLowMemoryException; |
| import org.apache.geode.cache.query.QueryExecutionTimeoutException; |
| import org.apache.geode.cache.query.QueryInvalidException; |
| import org.apache.geode.cache.query.QueryInvocationTargetException; |
| import org.apache.geode.cache.query.TypeMismatchException; |
| import org.apache.geode.cache.query.internal.DefaultQuery; |
| import org.apache.geode.internal.logging.LogService; |
| import org.apache.geode.rest.internal.web.exception.GemfireRestException; |
| import org.apache.geode.rest.internal.web.exception.ResourceNotFoundException; |
| import org.apache.geode.rest.internal.web.util.JSONUtils; |
| import org.apache.geode.rest.internal.web.util.ValidationUtils; |
| |
| /** |
| * The QueryingController class serves all HTTP REST requests related to the gemfire querying |
| * |
| * @see org.springframework.stereotype.Controller |
| * @since GemFire 8.0 |
| */ |
| @Controller("queryController") |
| @Api(value = "queries", description = "Rest api for geode query execution", |
| produces = MediaType.APPLICATION_JSON_VALUE, tags = "queries") |
| @RequestMapping(QueryAccessController.REST_API_VERSION + "/queries") |
| @SuppressWarnings("unused") |
| public class QueryAccessController extends AbstractBaseController { |
| |
| private static final Logger logger = LogService.getLogger(); |
| |
| static final String PARAMETERIZED_QUERIES_REGION = "__ParameterizedQueries__"; |
| |
| private final ConcurrentHashMap<String, DefaultQuery> compiledQueries = new ConcurrentHashMap<>(); |
| |
| // Constant String value indicating the version of the REST API. |
| static final String REST_API_VERSION = "/v1"; |
| |
| /** |
| * Gets the version of the REST API implemented by this @Controller. |
| * |
| * @return a String indicating the REST API version. |
| */ |
| @Override |
| protected String getRestApiVersion() { |
| return REST_API_VERSION; |
| } |
| |
| /** |
| * list all parametrized Queries created in a Gemfire data node |
| * |
| * @return result as a JSON document. |
| */ |
| @RequestMapping(method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) |
| @ApiOperation(value = "list all parametrized queries", |
| notes = "List all parametrized queries by id/name") |
| @ApiResponses({@ApiResponse(code = 200, message = "OK."), |
| @ApiResponse(code = 401, message = "Invalid Username or Password."), |
| @ApiResponse(code = 403, message = "Insufficient privileges for operation."), |
| @ApiResponse(code = 500, message = "if GemFire throws an error or exception")}) |
| @ResponseBody |
| @ResponseStatus(HttpStatus.OK) |
| @PreAuthorize("@securityService.authorize('DATA', 'READ')") |
| public ResponseEntity<?> list() { |
| logger.debug("Listing all parametrized Queries in GemFire..."); |
| |
| final Region<String, String> parametrizedQueryRegion = |
| getQueryStore(PARAMETERIZED_QUERIES_REGION); |
| |
| String queryListAsJson = JSONUtils.formulateJsonForListQueriesCall(parametrizedQueryRegion); |
| final HttpHeaders headers = new HttpHeaders(); |
| headers.setLocation(toUri("queries")); |
| return new ResponseEntity<>(queryListAsJson, headers, HttpStatus.OK); |
| } |
| |
| /** |
| * Create a named, parametrized Query |
| * |
| * @param queryId uniquely identify the query |
| * @param oqlInUrl OQL query string specified in a request URL |
| * @param oqlInBody OQL query string specified in a request body |
| * @return result as a JSON document. |
| */ |
| @RequestMapping(method = RequestMethod.POST) |
| @ApiOperation(value = "create a parametrized Query", |
| notes = "Prepare the specified parametrized query and assign the corresponding ID for lookup") |
| @ApiResponses({@ApiResponse(code = 201, message = "Successfully created."), |
| @ApiResponse(code = 401, message = "Invalid Username or Password."), |
| @ApiResponse(code = 403, message = "Insufficient privileges for operation."), |
| @ApiResponse(code = 409, message = "QueryId already assigned to other query."), |
| @ApiResponse(code = 500, message = "GemFire throws an error or exception.")}) |
| @PreAuthorize("@securityService.authorize('DATA', 'READ')") |
| public ResponseEntity<?> create(@RequestParam("id") final String queryId, |
| @RequestParam(value = "q", required = false) String oqlInUrl, |
| @RequestBody(required = false) final String oqlInBody) { |
| final String oqlStatement = validateQuery(oqlInUrl, oqlInBody); |
| logger.debug("Creating a named, parametrized Query ({}) with ID ({})...", oqlStatement, |
| queryId); |
| |
| |
| // store the compiled OQL statement with 'queryId' as the Key into the hidden, |
| // Parametrized Queries Region... |
| final String existingOql = |
| createNamedQuery(PARAMETERIZED_QUERIES_REGION, queryId, oqlStatement); |
| |
| final HttpHeaders headers = new HttpHeaders(); |
| headers.setLocation(toUri("queries", queryId)); |
| |
| if (existingOql != null) { |
| headers.setContentType(MediaType.APPLICATION_JSON); |
| return new ResponseEntity<>(JSONUtils.formulateJsonForExistingQuery(queryId, existingOql), |
| headers, HttpStatus.CONFLICT); |
| } else { |
| return new ResponseEntity<String>(headers, HttpStatus.CREATED); |
| } |
| } |
| |
| /** |
| * Run an adhoc Query specified in a query string |
| * |
| * @param oql OQL query string to be executed |
| * @return query result as a JSON document |
| */ |
| @RequestMapping(method = RequestMethod.GET, value = "/adhoc", |
| produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) |
| @ApiOperation(value = "run an adhoc query", |
| notes = "Run an unnamed (unidentified), ad-hoc query passed as a URL parameter") |
| @ApiResponses({@ApiResponse(code = 200, message = "OK."), |
| @ApiResponse(code = 401, message = "Invalid Username or Password."), |
| @ApiResponse(code = 403, message = "Insufficient privileges for operation."), |
| @ApiResponse(code = 500, message = "GemFire throws an error or exception")}) |
| @ResponseBody |
| @ResponseStatus(HttpStatus.OK) |
| @PreAuthorize("@securityService.authorize('DATA', 'READ')") |
| public ResponseEntity<String> runAdhocQuery(@RequestParam("q") String oql) { |
| logger.debug("Running an adhoc Query ({})...", oql); |
| |
| oql = decode(oql); |
| final Query query = getQueryService().newQuery(oql); |
| |
| // NOTE Query.execute throws many checked Exceptions; let the BaseControllerAdvice Exception |
| // handlers catch |
| // and handle the Exceptions appropriately (500 Server Error)! |
| try { |
| Object queryResult = query.execute(); |
| return processQueryResponse(query, null, queryResult); |
| } catch (FunctionDomainException fde) { |
| throw new GemfireRestException( |
| "A function was applied to a parameter that is improper for that function!", fde); |
| } catch (TypeMismatchException tme) { |
| throw new GemfireRestException("Bind parameter is not of the expected type!", tme); |
| } catch (NameResolutionException nre) { |
| throw new GemfireRestException("Name in the query cannot be resolved!", nre); |
| } catch (IllegalArgumentException iae) { |
| throw new GemfireRestException( |
| " The number of bound parameters does not match the number of placeholders!", iae); |
| } catch (IllegalStateException ise) { |
| throw new GemfireRestException("Query is not permitted on this type of region!", ise); |
| } catch (QueryExecutionTimeoutException qete) { |
| throw new GemfireRestException( |
| "Query execution time is exceeded max query execution time (gemfire.Cache.MAX_QUERY_EXECUTION_TIME) configured! ", |
| qete); |
| } catch (QueryInvocationTargetException qite) { |
| throw new GemfireRestException( |
| "Data referenced in from clause is not available for querying!", qite); |
| } catch (QueryExecutionLowMemoryException qelme) { |
| throw new GemfireRestException( |
| "Query execution gets canceled due to low memory conditions and the resource manager critical heap percentage has been set!", |
| qelme); |
| } catch (Exception ex) { |
| throw new GemfireRestException(ex.getMessage(), ex); |
| } |
| } |
| |
| /** |
| * Run named parametrized Query with ID |
| * |
| * @param queryId id of the OQL string |
| * @param arguments query bind params required while executing query |
| * @return query result as a JSON document |
| */ |
| @RequestMapping(method = RequestMethod.POST, value = "/{query}", |
| produces = {MediaType.APPLICATION_JSON_VALUE}) |
| @ApiOperation(value = "run parametrized query", |
| notes = "run the specified named query passing in scalar values for query parameters in the GemFire cluster") |
| @ApiResponses({@ApiResponse(code = 200, message = "Query successfully executed."), |
| @ApiResponse(code = 401, message = "Invalid Username or Password."), |
| @ApiResponse(code = 403, message = "Insufficient privileges for operation."), |
| @ApiResponse(code = 400, |
| message = "Query bind params specified as JSON document in the request body is invalid"), |
| @ApiResponse(code = 500, message = "GemFire throws an error or exception")}) |
| @ResponseBody |
| @ResponseStatus(HttpStatus.OK) |
| @PreAuthorize("@securityService.authorize('DATA', 'READ')") |
| public ResponseEntity<String> runNamedQuery(@PathVariable("query") String queryId, |
| @RequestBody String arguments) { |
| logger.debug("Running named Query with ID ({})...", queryId); |
| |
| queryId = decode(queryId); |
| |
| if (arguments != null) { |
| // Its a compiled query. |
| |
| // Convert arguments into Object[] |
| Object args[] = jsonToObjectArray(arguments); |
| |
| Query compiledQuery = compiledQueries.get(queryId); |
| if (compiledQuery == null) { |
| // This is first time the query is seen by this server. |
| final String oql = getQueryStore(PARAMETERIZED_QUERIES_REGION).get(queryId); |
| |
| ValidationUtils.returnValueThrowOnNull(oql, new ResourceNotFoundException( |
| String.format("No Query with ID (%1$s) was found!", queryId))); |
| try { |
| compiledQuery = getQueryService().newQuery(oql); |
| } catch (QueryInvalidException qie) { |
| throw new GemfireRestException("Syntax of the OQL queryString is invalid!", qie); |
| } |
| compiledQueries.putIfAbsent(queryId, (DefaultQuery) compiledQuery); |
| } |
| // NOTE Query.execute throws many checked Exceptions; let the BaseControllerAdvice Exception |
| // handlers catch |
| // and handle the Exceptions appropriately (500 Server Error)! |
| try { |
| Object queryResult = compiledQuery.execute(args); |
| return processQueryResponse(compiledQuery, args, queryResult); |
| } catch (FunctionDomainException fde) { |
| throw new GemfireRestException( |
| "A function was applied to a parameter that is improper for that function!", fde); |
| } catch (TypeMismatchException tme) { |
| throw new GemfireRestException("Bind parameter is not of the expected type!", tme); |
| } catch (NameResolutionException nre) { |
| throw new GemfireRestException("Name in the query cannot be resolved!", nre); |
| } catch (IllegalArgumentException iae) { |
| throw new GemfireRestException( |
| " The number of bound parameters does not match the number of placeholders!", iae); |
| } catch (IllegalStateException ise) { |
| throw new GemfireRestException("Query is not permitted on this type of region!", ise); |
| } catch (QueryExecutionTimeoutException qete) { |
| throw new GemfireRestException( |
| "Query execution time is exceeded max query execution time (gemfire.Cache.MAX_QUERY_EXECUTION_TIME) configured!", |
| qete); |
| } catch (QueryInvocationTargetException qite) { |
| throw new GemfireRestException( |
| "Data referenced in from clause is not available for querying!", qite); |
| } catch (QueryExecutionLowMemoryException qelme) { |
| throw new GemfireRestException( |
| "Query gets canceled due to low memory conditions and the resource manager critical heap percentage has been set!", |
| qelme); |
| } catch (Exception e) { |
| throw new GemfireRestException("Error encountered while executing named query!", e); |
| } |
| } else { |
| throw new GemfireRestException( |
| " Bind params either not specified or not processed properly by the server!"); |
| } |
| } |
| |
| /** |
| * Update named, parametrized Query |
| * |
| * @param queryId uniquely identify the query |
| * @param oqlInUrl OQL query string specified in a request URL |
| * @param oqlInBody OQL query string specified in a request body |
| */ |
| @RequestMapping(method = RequestMethod.PUT, value = "/{query}") |
| @ApiOperation(value = "update parametrized query", |
| notes = "Update named, parametrized query by ID") |
| @ApiResponses({@ApiResponse(code = 200, message = "Updated successfully."), |
| @ApiResponse(code = 401, message = "Invalid Username or Password."), |
| @ApiResponse(code = 403, message = "Insufficient privileges for operation."), |
| @ApiResponse(code = 404, message = "queryId does not exist."), |
| @ApiResponse(code = 500, message = "GemFire throws an error or exception.")}) |
| @PreAuthorize("@securityService.authorize('DATA', 'READ')") |
| public ResponseEntity<?> update(@PathVariable("query") final String queryId, |
| @RequestParam(value = "q", required = false) String oqlInUrl, |
| @RequestBody(required = false) final String oqlInBody) { |
| final String oqlStatement = validateQuery(oqlInUrl, oqlInBody); |
| |
| logger.debug("Updating a named, parametrized Query ({}) with ID ({})...", oqlStatement, |
| queryId); |
| |
| // update the OQL statement with 'queryId' as the Key into the hidden, Parametrized Queries |
| // Region... |
| checkForQueryIdExist(PARAMETERIZED_QUERIES_REGION, queryId); |
| updateNamedQuery(PARAMETERIZED_QUERIES_REGION, queryId, oqlStatement); |
| compiledQueries.remove(queryId); |
| |
| return new ResponseEntity<>(HttpStatus.OK); |
| } |
| |
| /** |
| * Delete named, parametrized Query |
| * |
| * @param queryId uniquely identify the query to be deleted |
| */ |
| @RequestMapping(method = RequestMethod.DELETE, value = "/{query}") |
| @ApiOperation(value = "delete parametrized query", |
| notes = "delete named, parametrized query by ID") |
| @ApiResponses({@ApiResponse(code = 200, message = "Deleted successfully."), |
| @ApiResponse(code = 401, message = "Invalid Username or Password."), |
| @ApiResponse(code = 403, message = "Insufficient privileges for operation."), |
| @ApiResponse(code = 404, message = "queryId does not exist."), |
| @ApiResponse(code = 500, message = "GemFire throws an error or exception")}) |
| @PreAuthorize("@securityService.authorize('DATA', 'WRITE')") |
| public ResponseEntity<?> delete(@PathVariable("query") final String queryId) { |
| logger.debug("Deleting a named, parametrized Query with ID ({}).", queryId); |
| |
| // delete the OQL statement with 'queryId' as the Key into the hidden, |
| // ParameterizedQueries Region... |
| deleteNamedQuery(PARAMETERIZED_QUERIES_REGION, queryId); |
| compiledQueries.remove(queryId); |
| return new ResponseEntity<>(HttpStatus.OK); |
| } |
| |
| } |