/*
* Copyright 2011 GigaSpaces Technologies Ltd
*
* Licensed 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.openspaces.rest.space;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletResponse;
import net.jini.core.lease.Lease;
import org.openspaces.core.GigaSpace;
import org.openspaces.core.space.UrlSpaceConfigurer;
import org.openspaces.rest.exceptions.ObjectNotFoundException;
import org.openspaces.rest.exceptions.TypeNotFoundException;
import org.openspaces.rest.utils.ControllerUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
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.springframework.web.servlet.ModelAndView;
import com.gigaspaces.document.SpaceDocument;
import com.gigaspaces.metadata.SpacePropertyDescriptor;
import com.gigaspaces.metadata.SpaceTypeDescriptor;
import com.gigaspaces.query.IdQuery;
import com.gigaspaces.query.QueryResultType;
import com.j_spaces.core.UnknownTypeException;
import com.j_spaces.core.client.SQLQuery;
import com.j_spaces.core.client.UpdateModifiers;
/**
* Spring MVC controller for the RESTful Space API
*
* usage examples:
* GET:
* http://localhost:8080/rest/data/Item/1
* http://192.168.9.47:8080/rest/data/Item/_criteria?q=data2='common'
*
* Limit result size:
* http://192.168.9.47:8080/rest/data/Item/_criteria?q=data2='common'&s=10
*
* DELETE:
* curl -XDELETE http://localhost:8080/rest/data/Item/1
* curl -XDELETE http://localhost:8080/rest/data/Item/_criteria?q=id=1
*
* Limit result size:
* curl -XDELETE http://localhost:8080/rest/data/Item/_criteria?q=data2='common'&s=5
*
* POST:
* curl -XPOST -d '[{"id":"1", "data":"testdata", "data2":"common", "nestedData" : {"nestedKey1":"nestedValue1"}}, {"id":"2", "data":"testdata2", "data2":"common", "nestedData" : {"nestedKey2":"nestedValue2"}}, {"id":"3", "data":"testdata3", "data2":"common", "nestedData" : {"nestedKey3":"nestedValue3"}}]' http://localhost:8080/rest/data/Item
*
* PUT:
* curl -XPUT -d '{"id":"1", "data":"testdata", "data2":"commonUpdated", "nestedData" : {"nestedKey1":"nestedValue1Updated"}}' http://192.168.9.47:8080/rest/data/Item
*
* @author rafi
* @since 8.0
*/
@Controller
@RequestMapping(value = "/rest/data/*")
public class SpaceAPIController {
private static final String CRITERIA_KEYWORD = "_criteria";
private static final String QUERY_PARAM = "q";
private static final String SIZE_PARAM = "s";
private static final String SPACE_PARAM="space";
private static final String LOCATORS_PARAM="locators";
private int maxReturnValues = Integer.MAX_VALUE;
private final Map<String,GigaSpace> connectionCache=new HashMap<String,GigaSpace>();
private UrlSpaceConfigurer scfg=null;
private static final Logger logger = Logger.getLogger(SpaceAPIController.class.getName());
/**
* redirects to index view
* @return
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView redirectToIndex(){
return new ModelAndView("index");
}
/**
* REST GET by query request handler
*
* @param type
* @param query
* @return
* @throws ObjectNotFoundException
*/
@RequestMapping(value = "/{type}/" + CRITERIA_KEYWORD, method = RequestMethod.GET, params=QUERY_PARAM)
public @ResponseBody Map<String, Object>[] getByQuery(
@PathVariable String type,
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@RequestParam(value=QUERY_PARAM) String query,
@RequestParam(value=SIZE_PARAM, required=false) Integer size) throws ObjectNotFoundException{
if(logger.isLoggable(Level.FINE))
logger.fine("creating read query with type: " + type + " and query: " + query);
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
SQLQuery<SpaceDocument> sqlQuery = new SQLQuery<SpaceDocument>(type, query, QueryResultType.DOCUMENT);
int maxSize = (size==null ? maxReturnValues : size.intValue());
SpaceDocument[] docs;
try {
docs = gigaSpace.readMultiple(sqlQuery, maxSize);
} catch (DataAccessException e) {
throw translateDataAccessException(gigaSpace,e, type);
}
Map<String, Object>[] result;
if (docs == null || docs.length == 0){
throw new ObjectNotFoundException("no objects matched the criteria");
}
result = ControllerUtils.createPropertiesResult(docs);
return result;
}
/**
* REST GET by ID request handler
*
* @param type
* @param id
* @return
* @throws ObjectNotFoundException
* @throws UnknownTypeException
*/
@RequestMapping(value = "/{type}/{id}", method = RequestMethod.GET)
public @ResponseBody Map<String, Object> getById(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
@PathVariable String id) throws ObjectNotFoundException{
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
//read by id request
Object typedBasedId = getTypeBasedIdObject(gigaSpace,type, id);
if(logger.isLoggable(Level.FINE))
logger.fine("creating readbyid query with type: " + type + " and id: " + id);
IdQuery<SpaceDocument> idQuery = new IdQuery<SpaceDocument>(type, typedBasedId, QueryResultType.DOCUMENT);
SpaceDocument doc = gigaSpace.readById(idQuery);
if (doc == null){
throw new ObjectNotFoundException("no object matched the criteria");
}
return doc.getProperties();
}
/**
* REST COUNT request handler
*
*/
@RequestMapping(value = "/{type}/count", method = RequestMethod.GET)
public ModelAndView count(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
HttpServletResponse response) throws ObjectNotFoundException{
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
//read by id request
Integer cnt = gigaSpace.count(new SpaceDocument(type));
if (cnt == null){
throw new ObjectNotFoundException("no object matched the criteria");
}
ModelAndView mv=new ModelAndView("jsonView");
mv.addObject("count",cnt);
response.setHeader("Access-Control-Allow-Origin","*");
return mv;
}
/**
* REST GET by type request handler
*
* @param type
* @return
* @throws ObjectNotFoundException
* @throws UnknownTypeException
*/
@RequestMapping(value = "/{type}", method = RequestMethod.GET)
public @ResponseBody Map<String, Object>[] getByType(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
@RequestParam(value=SIZE_PARAM, required=false) Integer size)throws ObjectNotFoundException{
return getByQuery(type,space,locators, "", size);
}
private Object getTypeBasedIdObject(GigaSpace gigaSpace,String type, String id) {
SpaceTypeDescriptor typeDescriptor = gigaSpace.getTypeManager().getTypeDescriptor(type);
if (typeDescriptor == null){
throw new TypeNotFoundException(type);
}
//Investigate id type
String idPropertyName = typeDescriptor.getIdPropertyName();
SpacePropertyDescriptor idProperty = typeDescriptor.getFixedProperty(idPropertyName);
try {
return ControllerUtils.convertPropertyToPrimitiveType(id, idProperty.getType(), idPropertyName);
} catch (UnknownTypeException e) {
throw new DataAccessException("Only primitive SpaceId is currently supported by RestData") {};
}
}
/**
* REST DELETE by id request handler
*
* @param type
* @param id
* @return
* @throws ObjectNotFoundException
*/
@RequestMapping(value = "/{type}/{id}", method = RequestMethod.DELETE)
public @ResponseBody Map<String, Object> deleteById(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
@PathVariable String id) throws ObjectNotFoundException {
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
//take by id
Object typedBasedId = getTypeBasedIdObject(gigaSpace,type, id);
if(logger.isLoggable(Level.FINE))
logger.fine("creating takebyid query with type: " + type + " and id: " + id);
SpaceDocument doc;
doc = gigaSpace.takeById(new IdQuery<SpaceDocument>(type, typedBasedId, QueryResultType.DOCUMENT));
if (doc == null){
throw new ObjectNotFoundException("no object matched the criteria");
}
return doc.getProperties();
}
/**
* REST DELETE by query request handler
* @param type
* @param query
* @return
*/
@RequestMapping(value = "/{type}/" + CRITERIA_KEYWORD, method = RequestMethod.DELETE, params=QUERY_PARAM)
public @ResponseBody Map<String, Object>[] deleteByQuery(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
@RequestParam(value=QUERY_PARAM) String query,
@RequestParam(value=SIZE_PARAM, required=false) Integer size){
if(logger.isLoggable(Level.FINE))
logger.fine("creating take query with type: " + type + " and query: " + query);
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
SQLQuery<SpaceDocument> sqlQuery = new SQLQuery<SpaceDocument>(type, query, QueryResultType.DOCUMENT);
int maxSize = (size==null ? maxReturnValues : size.intValue());
SpaceDocument[] docs;
try {
docs = gigaSpace.takeMultiple(sqlQuery, maxSize);
} catch (DataAccessException e) {
throw translateDataAccessException(gigaSpace, e, type);
}
return ControllerUtils.createPropertiesResult(docs);
}
/**
* REST DELETE by type request handler
* @param type
* @param query
* @return
*/
@RequestMapping(value = "/{space}/{locators}/{type}", method = RequestMethod.DELETE)
public @ResponseBody Map<String, Object>[] deleteByType(
@PathVariable String space,
@PathVariable String locators,
@PathVariable String type,
@RequestParam(value=SIZE_PARAM, required=false) Integer size){
return deleteByQuery(space,locators,type, "", size);
}
/**
* REST POST request handler
*
* @param type
* @param reader
* @return
* @throws TypeNotFoundException
*/
@RequestMapping(value = "/{type}", method = RequestMethod.POST)
public @ResponseBody String post(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
BufferedReader reader)
throws TypeNotFoundException{
if(logger.isLoggable(Level.FINE))
logger.fine("performing post, type: " + type);
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
createAndWriteDocuments(gigaSpace, type, reader, UpdateModifiers.WRITE_ONLY);
return "success";
}
/**
* REST PUT request handler
*
* @param type
* @param reader
* @return
* @throws TypeNotFoundException
*/
@RequestMapping(value = "/{type}", method = RequestMethod.PUT)
public @ResponseBody String put(
@RequestParam(value=SPACE_PARAM) String space,
@RequestParam(value=LOCATORS_PARAM,defaultValue="localhost") String locators,
@PathVariable String type,
BufferedReader reader)
throws TypeNotFoundException{
if(logger.isLoggable(Level.FINE))
logger.fine("performing put, type: " + type);
GigaSpace gigaSpace=ControllerUtils.xapCache.get(space,locators);
createAndWriteDocuments(gigaSpace, type, reader, UpdateModifiers.UPDATE_OR_WRITE);
return "success";
}
private RuntimeException translateDataAccessException(GigaSpace gigaSpace,DataAccessException e, String type) {
if (gigaSpace.getTypeManager().getTypeDescriptor(type) == null) {
return new TypeNotFoundException(type);
} else {
return e;
}
}
/**
* TypeNotFoundException Handler, returns an error response to the client
*
* @param writer
* @throws IOException
*/
@ExceptionHandler(TypeNotFoundException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
public void resolveTypeDescriptorNotFoundException(TypeNotFoundException e, Writer writer) throws IOException {
if(logger.isLoggable(Level.FINE))
logger.fine("type descriptor for typeName: " + e.getTypeName() + " not found, returning error response");
writer.write("{\"error\":\"Type: " + e.getTypeName() + " is not registered in space.\"}");
}
/**
* ObjectNotFoundException Handler, returns an error response to the client
*
* @param writer
* @throws IOException
*/
@ExceptionHandler(ObjectNotFoundException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
public void resolveDocumentNotFoundException(Writer writer) throws IOException {
if(logger.isLoggable(Level.FINE))
logger.fine("space id query has no results, returning error response");
writer.write("{\"error\":\"Object not found\"}");
}
/**
* DataAcessException Handler, returns an error response to the client
* @param e
* @param writer
* @throws IOException
*/
@ExceptionHandler(DataAccessException.class)
@ResponseStatus(value=HttpStatus.INTERNAL_SERVER_ERROR)
public void resolveDataAccessException(Exception e, Writer writer) throws IOException {
if(logger.isLoggable(Level.WARNING))
logger.log(Level.WARNING, "received DataAccessException exception", e);
writer.write(String.format( "{\"error\":{\"java.class\":\"%s\", \"message\":\"%s\"}}",
e.getClass(), e.getMessage()));
}
/**
* helper method that creates space documents from the httpRequest payload and writes them to space.
*
* @param type
* @param reader
* @param updateModifiers
* @throws TypeNotFoundException
*/
private void createAndWriteDocuments(GigaSpace gigaSpace, String type, BufferedReader reader, int updateModifiers)
throws TypeNotFoundException{
logger.info("creating space Documents from payload");
SpaceDocument[] spaceDocuments = ControllerUtils.createSpaceDocuments(type, reader, gigaSpace);
if (spaceDocuments != null && spaceDocuments.length > 0){
gigaSpace.writeMultiple(spaceDocuments, Lease.FOREVER, updateModifiers);
if(logger.isLoggable(Level.FINE))
logger.fine("wrote space documents to space");
}else{
if(logger.isLoggable(Level.FINE))
logger.fine("did not write anything to space");
}
}
public void setMaxReturnValues(int maxReturnValues) {
this.maxReturnValues = maxReturnValues;
}
}