Generic service invocation API #PL-3952

This commit is contained in:
Maxim Gorbunkov 2015-02-11 13:50:50 +00:00
parent 19abddf16a
commit c26be59981
9 changed files with 402 additions and 12 deletions

View File

@ -10,6 +10,7 @@ import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.View;
import javax.activation.MimeType;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@ -33,6 +34,9 @@ public interface Convertor {
Object process(Set<Entity> entities, String requestURI)
throws InvocationTargetException, NoSuchMethodException, IllegalAccessException;
Object processServiceMethodResult(Object result, String requestURI, @Nullable String viewName)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException;
CommitRequest parseCommitRequest(String content);
void write(HttpServletResponse response, Object o) throws IOException;

View File

@ -11,7 +11,6 @@ import com.haulmont.chile.core.datatypes.impl.*;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.cuba.core.app.DataService;
import com.haulmont.cuba.core.app.DomainDescriptionService;
import com.haulmont.cuba.core.entity.BaseGenericIdEntity;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.sys.AbstractViewRepository;
@ -22,15 +21,18 @@ import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.*;
import javax.activation.MimeType;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.*;
@ -67,6 +69,9 @@ public class DataServiceController {
@Inject
protected Authentication authentication;
@Inject
protected RestServicePermissions restServicePermissions;
@RequestMapping(value = "/api/find.{type}", method = RequestMethod.GET)
public void find(@PathVariable String type,
@RequestParam(value = "e") String entityRef,
@ -306,6 +311,115 @@ public class DataServiceController {
}
}
@RequestMapping(value = "/api/service.{type}", method = RequestMethod.GET)
public void serviceByGet(@PathVariable(value = "type") String type,
@RequestParam(value = "s") String sessionId,
@RequestParam(value = "service") String serviceName,
@RequestParam(value = "method") String methodName,
@RequestParam(value = "view", required = false) String view,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
if (!authentication.begin(sessionId)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
if (!restServicePermissions.isPermitted(serviceName, methodName)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
try {
response.addHeader("Access-Control-Allow-Origin", "*");
Map<String, String[]> parameterMap = request.getParameterMap();
List<String> paramValuesString = new ArrayList<>();
List<Class<?>> paramTypes = new ArrayList<>();
int idx = 0;
while (true) {
String[] _values = parameterMap.get("param" + idx);
if (_values == null) break;
if (_values.length > 1) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Multiple values for param" + idx);
return;
}
paramValuesString.add(_values[0]);
String[] _types = parameterMap.get("param" + idx + "_type");
if (_types != null) {
if (_types.length > 1) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Multiple values for param" + idx + "_type");
return;
}
paramTypes.add(idx, ClassUtils.forName(_types[0], null));
} else if (!paramTypes.isEmpty()) {
//types should be defined for all parameters or for none of them
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Parameter type for param" + idx +" is not defined");
return;
}
idx++;
}
Object service = AppBeans.get(serviceName);
Method serviceMethod;
if (paramTypes.isEmpty()) {
//trying to guess which method to invoke
Method[] methods = service.getClass().getMethods();
List<Method> appropriateMethods = new ArrayList<>();
for (Method method : methods) {
if (methodName.equals(method.getName()) && method.getParameterTypes().length == paramValuesString.size()) {
appropriateMethods.add(method);
}
}
if (appropriateMethods.size() == 1) {
serviceMethod = appropriateMethods.get(0);
} else if (appropriateMethods.size() > 1) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "There are multiple methods with given argument numbers. Please define parameter types in request");
return;
} else {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Method not found");
return;
}
} else {
try {
serviceMethod = service.getClass().getMethod(methodName, paramTypes.toArray(new Class[paramTypes.size()]));
} catch (NoSuchMethodException e) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Method not found");
return;
}
}
List<Object> paramValues = new ArrayList<>();
Class<?>[] types = serviceMethod.getParameterTypes();
for (int i = 0; i < types.length; i++) {
Class<?> aClass = types[i];
paramValues.add(toObject(aClass, paramValuesString.get(i)));
}
Object result = serviceMethod.invoke(service, paramValues.toArray());
Convertor convertor = conversionFactory.getConvertor(type);
Object converted = convertor.processServiceMethodResult(result, request.getRequestURI(), view);
convertor.write(response, converted);
} catch (Throwable e) {
sendError(request, response, e);
} finally {
authentication.end();
}
}
private Object toObject(Class clazz, String value) {
if (Boolean.class == clazz || Boolean.TYPE == clazz) return Boolean.parseBoolean(value);
if (Byte.class == clazz || Byte.TYPE == clazz) return Byte.parseByte(value);
if (Short.class == clazz || Short.TYPE == clazz) return Short.parseShort(value);
if (Integer.class == clazz || Integer.TYPE == clazz) return Integer.parseInt(value);
if (Long.class == clazz || Long.TYPE == clazz) return Long.parseLong(value);
if (Float.class == clazz || Float.TYPE == clazz) return Float.parseFloat(value);
if (Double.class == clazz || Double.TYPE == clazz) return Double.parseDouble(value);
if (UUID.class == clazz) return UUID.fromString(value);
if (String.class == clazz) return value;
throw new IllegalArgumentException("Parameters of type " + clazz.getName() + " are not supported");
}
private void sendError(HttpServletRequest request, HttpServletResponse response, Throwable e) throws IOException {
log.error("Error processing request: " + request.getRequestURI() + "?" + request.getQueryString(), e);

View File

@ -21,6 +21,7 @@
package com.haulmont.cuba.portal.restapi;
import com.google.common.base.Strings;
import com.haulmont.chile.core.datatypes.impl.StringDatatype;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
@ -36,6 +37,8 @@ import org.json.JSONObject;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.annotation.Nullable;
import javax.persistence.Id;
import javax.servlet.http.HttpServletResponse;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
@ -116,6 +119,48 @@ public class JSONConvertor implements Convertor {
return result;
}
@Override
public Object processServiceMethodResult(Object result, String requestURI, @Nullable String viewName)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
MyJSONObject root = new MyJSONObject();
if (result instanceof Entity) {
Entity entity = (Entity) result;
ViewRepository viewRepository = AppBeans.get(ViewRepository.class);
if (Strings.isNullOrEmpty(viewName)) viewName = View.LOCAL;
View view = viewRepository.getView(entity.getMetaClass(), viewName);
MyJSONObject entityObject = process(entity, entity.getMetaClass(), "", view);
root.set("result", entityObject);
} else if (result instanceof Collection) {
if (!checkCollectionItemTypes((Collection) result, Entity.class))
throw new IllegalArgumentException("Items that are not instances of Entity found in service method result");
ArrayList list = new ArrayList((Collection) result);
MetaClass metaClass;
if (!list.isEmpty())
metaClass = ((Entity) list.get(0)).getMetaClass();
else
metaClass = AppBeans.get(Metadata.class).getClasses().iterator().next();
ViewRepository viewRepository = AppBeans.get(ViewRepository.class);
if (Strings.isNullOrEmpty(viewName)) viewName = View.LOCAL;
View view = viewRepository.getView(metaClass, viewName);
MyJSONObject.Array processed = process(list, metaClass, requestURI, view);
root.set("result", processed);
} else {
root.set("result", result);
}
return root;
}
/**
* Checks that all collection items are instances of given class
*/
protected boolean checkCollectionItemTypes(Collection collection, Class<?> itemClass) {
for (Object collectionItem : collection) {
if (!itemClass.isAssignableFrom(collectionItem.getClass()))
return false;
}
return true;
}
private MetaClass getMetaClass(Entity entity) {
return metadata.getSession().getClassNN(entity.getClass());
}

View File

@ -21,6 +21,8 @@
package com.haulmont.cuba.portal.restapi;
import com.google.common.base.Strings;
import java.io.PrintWriter;
import java.util.*;
@ -33,8 +35,12 @@ public class MyJSONObject implements MyJSON {
private final boolean _ref;
private final Map<String, Object> _values;
public MyJSONObject() {
this(null, false);
}
public MyJSONObject(Object id, boolean ref) {
_id = id.toString();
_id = id != null ? id.toString() : null;
_ref = ref;
_values = new LinkedHashMap<String, Object>();
}
@ -52,14 +58,19 @@ public class MyJSONObject implements MyJSON {
public StringBuilder asString(int indent) {
StringBuilder buf = new StringBuilder().append(OBJECT_START);
if (!Strings.isNullOrEmpty(_id)) {
buf.append(encodeField(_ref ? REF_MARKER : ID_MARKER, ior(), 0));
buf.append(FIELD_SEPARATOR).append(NEWLINE);
}
if (_ref) {
return buf.append(OBJECT_END);
}
StringBuilder tab = newIndent(indent+1);
int i = 0;
for (Map.Entry<String, Object> e : _values.entrySet()) {
buf.append(FIELD_SEPARATOR).append(NEWLINE);
buf.append(tab).append(encodeField(e.getKey(), e.getValue(), indent+1));
if (i++ < _values.entrySet().size() - 1)
buf.append(FIELD_SEPARATOR).append(NEWLINE);
}
buf.append(NEWLINE)
.append(newIndent(indent))

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2008-2015 Haulmont. All rights reserved.
* Use is subject to license terms, see http://www.cuba-platform.com/license for details.
*/
package com.haulmont.cuba.portal.restapi;
import com.haulmont.bali.util.Dom4j;
import com.haulmont.cuba.core.global.Resources;
import com.haulmont.cuba.core.sys.AppContext;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.text.StrTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Element;
import org.springframework.core.io.Resource;
import javax.annotation.ManagedBean;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Class holds an information about services that allowed to be invoked with REST API.
* Configuration is loaded from {@code *-rest-services.xml} files.
*
* @author gorbunkov
* @version $Id$
*/
@ManagedBean(RestServicePermissions.NAME)
public class RestServicePermissions {
public static final String NAME = "cuba_RestServicePermissions";
public static final String CUBA_REST_SERVICES_CONFIG_PROP_NAME = "cuba.restServicesConfig";
@Inject
protected Resources resources;
protected static Log log = LogFactory.getLog(RestServicePermissions.class);
protected Map<String, Set<String>> serviceMethods = new ConcurrentHashMap<>();
protected volatile boolean initialized;
protected ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* Checks whether method of service is allowed to be invoked with REST API
*/
public boolean isPermitted(String serviceName, String methodName) {
lock.readLock().lock();
try {
checkInitialized();
Set<String> methods = serviceMethods.get(serviceName);
return methods != null && methods.contains(methodName);
} finally {
lock.readLock().unlock();
}
}
protected void checkInitialized() {
if (!initialized) {
lock.readLock().unlock();
lock.writeLock().lock();
try {
if (!initialized) {
init();
initialized = true;
}
} finally {
lock.readLock().lock();
lock.writeLock().unlock();
}
}
}
protected void init() {
String configName = AppContext.getProperty(CUBA_REST_SERVICES_CONFIG_PROP_NAME);
StrTokenizer tokenizer = new StrTokenizer(configName);
for (String location : tokenizer.getTokenArray()) {
Resource resource = resources.getResource(location);
if (resource.exists()) {
InputStream stream = null;
try {
stream = resource.getInputStream();
loadConfig(Dom4j.readDocument(stream).getRootElement());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(stream);
}
} else {
log.warn("Resource " + location + " not found, ignore it");
}
}
}
protected void loadConfig(Element rootElem) {
for (Element serviceElem : Dom4j.elements(rootElem, "service")) {
String serviceName = serviceElem.attributeValue("name");
for (Element methodElem : Dom4j.elements(serviceElem, "method")) {
String methodName = methodElem.attributeValue("name");
addServiceMethod(serviceName, methodName);
}
}
}
protected void addServiceMethod(String serviceName, String methodName) {
Set<String> methods = serviceMethods.get(serviceName);
if (methods == null) {
methods = new HashSet<>();
serviceMethods.put(serviceName, methods);
}
methods.add(methodName);
}
}

View File

@ -21,6 +21,7 @@
package com.haulmont.cuba.portal.restapi;
import com.google.common.base.Strings;
import com.haulmont.chile.core.datatypes.impl.StringDatatype;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
@ -40,6 +41,7 @@ import org.w3c.dom.ls.LSParserFilter;
import org.w3c.dom.traversal.NodeFilter;
import javax.activation.MimeType;
import javax.annotation.Nullable;
import javax.persistence.Embedded;
import javax.persistence.Id;
import javax.persistence.Version;
@ -69,6 +71,7 @@ public class XMLConvertor implements Convertor {
public static final String MIME_STR = "text/xml;charset=UTF-8";
public static final String ELEMENT_INSTANCE = "instance";
public static final String ELEMENT_RESULT = "result";
public static final String ELEMENT_URI = "uri";
public static final String ELEMENT_REF = "ref";
public static final String ELEMENT_NULL_REF = "null";
@ -159,6 +162,48 @@ public class XMLConvertor implements Convertor {
}
}
@Override
public Object processServiceMethodResult(@Nullable Object result, String requestURI, String viewName)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Element root = newDocument(ELEMENT_RESULT);
if (result instanceof Entity) {
Entity entity = (Entity) result;
if (Strings.isNullOrEmpty(viewName)) viewName = View.LOCAL;
ViewRepository viewRepository = AppBeans.get(ViewRepository.class);
View view = viewRepository.getView(entity.getMetaClass(), viewName);
Document convertedEntity = process(entity, entity.getMetaClass(), requestURI, view);
Node importedNode = root.getOwnerDocument().importNode(convertedEntity.getDocumentElement(), true);
root.appendChild(importedNode);
} else if (result instanceof Collection) {
if (!checkCollectionItemTypes((Collection) result, Entity.class))
throw new IllegalArgumentException("Items that are not instances of Entity class found in service method result");
ArrayList list = new ArrayList((Collection) result);
MetaClass metaClass;
if (!list.isEmpty())
metaClass = ((Entity) list.get(0)).getMetaClass();
else
metaClass = AppBeans.get(Metadata.class).getClasses().iterator().next();
ViewRepository viewRepository = AppBeans.get(ViewRepository.class);
if (Strings.isNullOrEmpty(viewName)) viewName = View.LOCAL;
View view = viewRepository.getView(metaClass, viewName);
Document processed = process(list, metaClass, requestURI, view);
Node importedNode = root.getOwnerDocument().importNode(processed.getDocumentElement(), true);
root.appendChild(importedNode);
} else {
root.setTextContent(result != null ? result.toString() : NULL_VALUE);
}
return root.getOwnerDocument();
}
protected boolean checkCollectionItemTypes(Collection collection, Class<?> itemClass) {
for (Object collectionItem : collection) {
if (!itemClass.isAssignableFrom(collectionItem.getClass()))
return false;
}
return true;
}
@Override
public void write(HttpServletResponse response, Object o) throws IOException {
Document doc = (Document) o;
@ -421,14 +466,14 @@ public class XMLConvertor implements Convertor {
Document doc = builder.newDocument();
Element root = doc.createElement(rootTag);
doc.appendChild(root);
String[] nvpairs = new String[]{
"xmlns:xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
// "xsi:noNamespaceSchemaLocation", INSTANCE_XSD,
ATTR_VERSION, "1.0",
};
for (int i = 0; i < nvpairs.length; i += 2) {
root.setAttribute(nvpairs[i], nvpairs[i + 1]);
}
// String[] nvpairs = new String[]{
// "xmlns:xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
//// "xsi:noNamespaceSchemaLocation", INSTANCE_XSD,
// ATTR_VERSION, "1.0",
// };
// for (int i = 0; i < nvpairs.length; i += 2) {
// root.setAttribute(nvpairs[i], nvpairs[i + 1]);
// }
return root;
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2008-2015 Haulmont. All rights reserved.
~ Use is subject to license terms, see http://www.cuba-platform.com/license for details.
-->
<xs:schema
targetNamespace="http://schemas.haulmont.com/cuba/5.3/rest-services.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.haulmont.com/cuba/5.3/rest-services.xsd"
elementFormDefault="qualified"
attributeFormDefault="unqualified"
>
<xs:element name="services" type="servicesType"/>
<xs:complexType name="servicesType">
<xs:sequence>
<xs:element name="service" type="serviceType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="serviceType">
<xs:sequence>
<xs:element name="method" type="methodType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="methodType">
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
</xs:schema>

View File

@ -41,6 +41,7 @@ cuba.dispatcherSpringContextConfig=cuba-portal-dispatcher-spring.xml
cuba.viewsConfig=cuba-views.xml
cuba.persistenceConfig=cuba-persistence.xml
cuba.metadataConfig=cuba-metadata.xml
cuba.restServicesConfig=cuba-rest-services.xml
cuba.mainMessagePack=com.haulmont.cuba.core

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2008-2015 Haulmont. All rights reserved.
~ Use is subject to license terms, see http://www.cuba-platform.com/license for details.
-->
<services xmlns="http://schemas.haulmont.com/cuba/5.3/rest-services.xsd">
<!--<service name="cuba_SampleService">-->
<!--<method name="doSmth"/>-->
<!--<method name="doSmthElse"/>-->
<!--</service>-->
</services>