blob: 7b70890bcd15fe06357f234f13621e33da3d773d [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.ArrayList;
import java.util.List;
import java.util.Map;
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.util.StringUtils;
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.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.rest.internal.web.controllers.support.JSONTypes;
import org.apache.geode.rest.internal.web.controllers.support.RegionData;
import org.apache.geode.rest.internal.web.controllers.support.RegionEntryData;
import org.apache.geode.rest.internal.web.exception.ResourceNotFoundException;
import org.apache.geode.rest.internal.web.util.ArrayUtils;
/**
* The PdxBasedCrudController class serving REST Requests related to the REST CRUD operation on
* region
*
* @see org.springframework.stereotype.Controller
* @since GemFire 8.0
*/
@Controller("pdxCrudController")
@Api(value = "region", description = "region CRUD operations", tags = "region")
@RequestMapping(PdxBasedCrudController.REST_API_VERSION)
@SuppressWarnings("unused")
public class PdxBasedCrudController extends CommonCrudController {
private static final Logger logger = LogService.getLogger();
static final String REST_API_VERSION = "/v1";
private static final String DEFAULT_GETALL_RESULT_LIMIT = "50";
@Override
protected String getRestApiVersion() {
return REST_API_VERSION;
}
/**
* Creating entry into the region
*
* @param region region name where data will be created
* @param key gemfire region key
* @param json JSON document that is stored against the key
* @return JSON document
*/
@RequestMapping(method = RequestMethod.POST, value = "/{region}",
consumes = MediaType.APPLICATION_JSON_VALUE, produces = {MediaType.APPLICATION_JSON_VALUE})
@ApiOperation(value = "create entry", notes = "Create (put-if-absent) data in region")
@ApiResponses({@ApiResponse(code = 201, message = "Created."),
@ApiResponse(code = 400,
message = "Data specified (JSON doc) in the request body is invalid."),
@ApiResponse(code = 401, message = "Invalid Username or Password."),
@ApiResponse(code = 403, message = "Insufficient privileges for operation."),
@ApiResponse(code = 404, message = "Region does not exist."),
@ApiResponse(code = 409, message = "Key already exist in region."),
@ApiResponse(code = 500, message = "GemFire throws an error or exception.")})
@PreAuthorize("@securityService.authorize('DATA', 'WRITE', #region)")
public ResponseEntity<?> create(@PathVariable("region") String region,
@RequestParam(value = "key", required = false) String key, @RequestBody final String json) {
key = generateKey(key);
logger.debug(
"Posting (creating/putIfAbsent) JSON document ({}) to Region ({}) with Key ({})...", json,
region, key);
region = decode(region);
Object existingPdxObj = null;
// Check whether the user has supplied single JSON doc or Array of JSON docs
final JSONTypes jsonType = validateJsonAndFindType(json);
if (JSONTypes.JSON_ARRAY.equals(jsonType)) {
existingPdxObj = postValue(region, key, convertJsonArrayIntoPdxCollection(json));
} else {
existingPdxObj = postValue(region, key, convert(json));
}
final HttpHeaders headers = new HttpHeaders();
headers.setLocation(toUri(region, key));
if (existingPdxObj != null) {
final RegionEntryData<Object> data = new RegionEntryData<>(region);
data.add(existingPdxObj);
headers.setContentType(MediaType.APPLICATION_JSON);
return new ResponseEntity<RegionEntryData<?>>(data, headers, HttpStatus.CONFLICT);
} else {
return new ResponseEntity<String>(headers, HttpStatus.CREATED);
}
}
/**
* Read all or fixed number of data in a given Region
*
* @param region gemfire region name
* @param limit total number of entries requested
* @return JSON document
*/
@RequestMapping(method = RequestMethod.GET, value = "/{region}",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ApiOperation(value = "read all data for region",
notes = "Read all data for region. Use limit param to get fixed or limited number of entries.")
@ApiResponses({@ApiResponse(code = 200, message = "OK."),
@ApiResponse(code = 400, message = "Bad request."),
@ApiResponse(code = 401, message = "Invalid Username or Password."),
@ApiResponse(code = 403, message = "Insufficient privileges for operation."),
@ApiResponse(code = 404, message = "Region does not exist."),
@ApiResponse(code = 500, message = "GemFire throws an error or exception.")})
@PreAuthorize("@securityService.authorize('DATA', 'READ', #region)")
public ResponseEntity<?> read(@PathVariable("region") String region,
@RequestParam(value = "limit",
defaultValue = DEFAULT_GETALL_RESULT_LIMIT) final String limit) {
logger.debug("Reading all data in Region ({})...", region);
region = decode(region);
Map<Object, Object> valueObjs = null;
final RegionData<Object> data = new RegionData<>(region);
final HttpHeaders headers = new HttpHeaders();
String keyList = null;
int regionSize = getRegion(region).size();
List<Object> keys = new ArrayList<>(regionSize);
List<Object> values = new ArrayList<>(regionSize);
for (Map.Entry<Object, Object> entry : getValues(region).entrySet()) {
Object value = entry.getValue();
if (value != null) {
keys.add(entry.getKey());
values.add(value);
}
}
if ("ALL".equalsIgnoreCase(limit)) {
data.add(values);
keyList = StringUtils.collectionToDelimitedString(keys, ",");
} else {
try {
int maxLimit = Integer.valueOf(limit);
if (maxLimit < 0) {
String errorMessage =
String.format("Negative limit param (%1$s) is not valid!", maxLimit);
return new ResponseEntity<>(convertErrorAsJson(errorMessage), HttpStatus.BAD_REQUEST);
}
int mapSize = keys.size();
if (maxLimit > mapSize) {
maxLimit = mapSize;
}
data.add(values.subList(0, maxLimit));
keyList = StringUtils.collectionToDelimitedString(keys.subList(0, maxLimit), ",");
} catch (NumberFormatException e) {
// limit param is not specified in proper format. set the HTTPHeader
// for BAD_REQUEST
String errorMessage = String.format("limit param (%1$s) is not valid!", limit);
return new ResponseEntity<>(convertErrorAsJson(errorMessage), HttpStatus.BAD_REQUEST);
}
}
headers.set("Content-Location", toUri(region, keyList).toASCIIString());
return new ResponseEntity<RegionData<?>>(data, headers, HttpStatus.OK);
}
/**
* Reading data for set of keys
*
* @param region gemfire region name
* @param keys string containing comma separated keys
* @return JSON document
*/
@RequestMapping(method = RequestMethod.GET, value = "/{region}/{keys}",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ApiOperation(value = "read data for specific keys",
notes = "Read data for specific set of keys in region.")
@ApiResponses({@ApiResponse(code = 200, message = "OK."),
@ApiResponse(code = 400, message = "Bad Request."),
@ApiResponse(code = 401, message = "Invalid Username or Password."),
@ApiResponse(code = 403, message = "Insufficient privileges for operation."),
@ApiResponse(code = 404, message = "Region does not exist."),
@ApiResponse(code = 500, message = "GemFire throws an error or exception.")})
@PreAuthorize("@securityService.authorize('READ', #region, #keys)")
public ResponseEntity<?> read(@PathVariable("region") String region,
@PathVariable("keys") final String[] keys,
@RequestParam(value = "ignoreMissingKey", required = false) final String ignoreMissingKey) {
logger.debug("Reading data for keys ({}) in Region ({})", ArrayUtils.toString(keys), region);
final HttpHeaders headers = new HttpHeaders();
region = decode(region);
if (keys.length == 1) {
/* GET op on single key */
Object value = getValue(region, keys[0]);
// if region.get(K) return null (i.e INVLD or TOMBSTONE case) We consider 404, NOT Found case
if (value == null) {
throw new ResourceNotFoundException(String
.format("Key (%1$s) does not exist for region (%2$s) in cache!", keys[0], region));
}
final RegionEntryData<Object> data = new RegionEntryData<>(region);
headers.set("Content-Location", toUri(region, keys[0]).toASCIIString());
data.add(value);
return new ResponseEntity<RegionData<?>>(data, headers, HttpStatus.OK);
} else {
// fail fast for the case where ignoreMissingKey param is not specified correctly.
if (ignoreMissingKey != null && !(ignoreMissingKey.equalsIgnoreCase("true")
|| ignoreMissingKey.equalsIgnoreCase("false"))) {
String errorMessage = String.format(
"ignoreMissingKey param (%1$s) is not valid. valid usage is ignoreMissingKey=true!",
ignoreMissingKey);
return new ResponseEntity<>(convertErrorAsJson(errorMessage), HttpStatus.BAD_REQUEST);
}
if (!("true".equalsIgnoreCase(ignoreMissingKey))) {
List<String> unknownKeys = checkForMultipleKeysExist(region, keys);
if (unknownKeys.size() > 0) {
String unknownKeysAsStr = StringUtils.collectionToDelimitedString(unknownKeys, ",");
String erroString = String.format("Requested keys (%1$s) not exist in region (%2$s)",
StringUtils.collectionToDelimitedString(unknownKeys, ","), region);
return new ResponseEntity<>(convertErrorAsJson(erroString), headers,
HttpStatus.BAD_REQUEST);
}
}
final Map<Object, Object> valueObjs = getValues(region, keys);
// Do we need to remove null values from Map..?
// To Remove null value entries from map.
// valueObjs.values().removeAll(Collections.singleton(null));
// currently we are not removing keys having value null from the result.
String keyList = StringUtils.collectionToDelimitedString(valueObjs.keySet(), ",");
headers.set("Content-Location", toUri(region, keyList).toASCIIString());
final RegionData<Object> data = new RegionData<>(region);
data.add(valueObjs.values());
return new ResponseEntity<RegionData<?>>(data, headers, HttpStatus.OK);
}
}
/**
* Update data for a key or set of keys
*
* @param region gemfire data region
* @param keys keys for which update operation is requested
* @param opValue type of update (put, replace, cas etc)
* @param json new data for the key(s)
* @return JSON document
*/
@RequestMapping(method = RequestMethod.PUT, value = "/{region}/{keys}",
consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
@ApiOperation(value = "update data for key",
notes = "Update or insert (put) data for key in region."
+ "op=REPLACE, update (replace) data with key if and only if the key exists in region"
+ "op=CAS update (compare-and-set) value having key with a new value if and only if the \"@old\" value sent matches the current value for the key in region")
@ApiResponses({@ApiResponse(code = 200, message = "OK."),
@ApiResponse(code = 400, message = "Bad Request."),
@ApiResponse(code = 401, message = "Invalid Username or Password."),
@ApiResponse(code = 403, message = "Insufficient privileges for operation."),
@ApiResponse(code = 404,
message = "Region does not exist or if key is not mapped to some value for REPLACE or CAS."),
@ApiResponse(code = 409,
message = "For CAS, @old value does not match to the current value in region"),
@ApiResponse(code = 500, message = "GemFire throws an error or exception.")})
@PreAuthorize("@securityService.authorize('WRITE', #region, #keys)")
public ResponseEntity<?> update(@PathVariable("region") String region,
@PathVariable("keys") final String[] keys,
@RequestParam(value = "op", defaultValue = "PUT") final String opValue,
@RequestBody final String json) {
logger.debug("updating key(s) for region ({}) ", region);
region = decode(region);
if (keys.length > 1) {
// putAll case
return updateMultipleKeys(region, keys, json);
} else {
// put case
return updateSingleKey(region, keys[0], json, opValue);
}
}
@RequestMapping(method = RequestMethod.HEAD, value = "/{region}",
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Get total number of entries",
notes = "Get total number of entries into the specified region")
@ApiResponses({@ApiResponse(code = 200, message = "OK."),
@ApiResponse(code = 400, message = "Bad request."),
@ApiResponse(code = 401, message = "Invalid Username or Password."),
@ApiResponse(code = 403, message = "Insufficient privileges for operation."),
@ApiResponse(code = 404, message = "Region does not exist."),
@ApiResponse(code = 500, message = "GemFire throws an error or exception.")})
@PreAuthorize("@securityService.authorize('DATA', 'READ', #region)")
public ResponseEntity<?> size(@PathVariable("region") String region) {
logger.debug("Determining the number of entries in Region ({})...", region);
region = decode(region);
final HttpHeaders headers = new HttpHeaders();
headers.set("Resource-Count", String.valueOf(getRegion(region).size()));
return new ResponseEntity<RegionData<?>>(headers, HttpStatus.OK);
}
}