blob: 3c06c36db2e484bd4b1ea3aef8b4f0150efc60b0 [file] [log] [blame]
/*
* 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.logging.internal.log4j.api.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);
}
}