blob: 2b9a4547b1efec3a27e8ef7f22dc60a706742634 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* 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 com.google.gwt.requestfactory.server;
import com.google.gwt.requestfactory.shared.RequestFactory;
import com.google.gwt.requestfactory.shared.ServerType;
import com.google.gwt.requestfactory.shared.RequestFactory.Config;
import com.google.gwt.requestfactory.shared.RequestFactory.RequestDefinition;
import com.google.gwt.requestfactory.shared.RequestFactory.WriteOperation;
import com.google.gwt.requestfactory.shared.impl.RequestDataManager;
import com.google.gwt.valuestore.shared.Property;
import com.google.gwt.valuestore.shared.Record;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* Handles GWT RequestFactory JSON requests. Configured via servlet context
* param <code>servlet.serverOperation</code>, which must be set to the name of
* a default instantiable class implementing
* com.google.gwt.requestfactory.shared.RequestFactory.Config.
* <p>
* e.g.
*
* <pre> &lt;context-param>
&lt;param-name>servlet.serverOperation&lt;/param-name>
&lt;param-value>com.myco.myapp.MyAppServerSideOperations&lt;/param-value>
&lt;/context-param>
* </pre>
*/
@SuppressWarnings("serial")
public class RequestFactoryServlet extends HttpServlet {
/**
* A class representing the pair of a domain entity and its corresponding
* record class on the client side.
*/
protected static class EntityRecordPair {
public final Class<?> entity;
public final Class<? extends Record> record;
EntityRecordPair(Class<?> entity, Class<? extends Record> record) {
this.entity = entity;
this.record = record;
}
}
private static final Set<String> BLACK_LIST = initBlackList();
private static final String SERVER_OPERATION_CONTEXT_PARAM = "servlet.serverOperation";
private static Set<String> initBlackList() {
Set<String> blackList = new HashSet<String>();
for (String str : new String[] {"password"}) {
blackList.add(str);
}
return Collections.unmodifiableSet(blackList);
}
private Config config = null;
protected Map<String, EntityRecordPair> tokenToEntityRecord;
@SuppressWarnings("unchecked")
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
initDb(); // temporary place-holder
ensureConfig();
RequestDefinition operation = null;
try {
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter writer = response.getWriter();
JSONObject topLevelJsonObject = new JSONObject(getContent(request));
String operationName = topLevelJsonObject.getString(RequestDataManager.OPERATION_TOKEN);
if (operationName.equals(RequestFactory.SYNC)) {
sync(topLevelJsonObject.getString(RequestDataManager.CONTENT_TOKEN),
writer);
} else {
operation = getOperation(operationName);
Class<?> domainClass = Class.forName(operation.getDomainClassName());
Method domainMethod = domainClass.getMethod(
operation.getDomainMethodName(), operation.getParameterTypes());
if (!Modifier.isStatic(domainMethod.getModifiers())) {
throw new IllegalArgumentException("the " + domainMethod.getName()
+ " is not static");
}
Object args[] = RequestDataManager.getObjectsFromParameterMap(
getParameterMap(topLevelJsonObject),
domainMethod.getParameterTypes());
Object result = domainMethod.invoke(null, args);
if ((result instanceof List<?>) != operation.isReturnTypeList()) {
throw new IllegalArgumentException(String.format(
"Type mismatch, expected %s%s, but %s returns %s",
operation.isReturnTypeList() ? "list of " : "",
operation.getReturnType(), domainMethod,
domainMethod.getReturnType()));
}
if (result instanceof List<?>) {
JSONArray jsonArray = getJsonArray((List<?>) result,
(Class<? extends Record>) operation.getReturnType());
writer.print(jsonArray.toString());
} else if (result instanceof Number) {
writer.print(result.toString());
} else {
JSONObject jsonObject = getJsonObject(result,
(Class<? extends Record>) operation.getReturnType());
writer.print("(" + jsonObject.toString() + ")");
}
}
writer.flush();
// TODO: clean exception handling code below.
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException(e);
} catch (SecurityException e) {
throw new IllegalArgumentException(e);
} catch (JSONException e) {
throw new IllegalArgumentException(e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
/**
* Allow subclass to initialize database.
*/
protected void initDb() {
}
/**
* Persist a recordObject of token "recordToken" and return useful information
* as a JSONObject to return back.
* <p>
* Example: recordToken = "Employee", entity = Employee.class, record =
* EmployeeRecord.class
*<p>
* Steps:
* <ol>
* <li>assert that each property is present in "EmployeeRecord"
* <li>invoke "findEmployee (id)" OR new Employee()
* <li>set various fields on the attached entity and persist OR remove()
* <li>return data
* </ol>
*/
JSONObject updateRecordInDataStore(String recordToken,
JSONObject recordObject, WriteOperation writeOperation)
throws SecurityException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException, JSONException, InstantiationException {
Class<?> entity = tokenToEntityRecord.get(recordToken).entity;
Class<? extends Record> record = tokenToEntityRecord.get(recordToken).record;
Map<String, Class<?>> propertiesInRecord = getPropertiesFromRecord(record);
validateKeys(recordObject, propertiesInRecord.keySet());
updatePropertyTypes(propertiesInRecord, entity);
// get entityInstance
Object entityInstance = getEntityInstance(writeOperation, entity,
recordObject.get("id"), propertiesInRecord.get("id"));
// persist
Set<ConstraintViolation<Object>> violations = null;
if (writeOperation == WriteOperation.DELETE) {
entity.getMethod("remove").invoke(entityInstance);
} else {
Iterator<?> keys = recordObject.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
Class<?> propertyType = propertiesInRecord.get(key);
if (writeOperation == WriteOperation.CREATE && ("id".equals(key))) {
// ignored. id is assigned by default.
} else {
Object propertyValue = getPropertyValueFromRequest(recordObject, key,
propertyType);
propertyValue = getSwizzledObject(propertyValue, propertyType);
entity.getMethod(getMethodNameFromPropertyName(key, "set"),
propertyType).invoke(entityInstance, propertyValue);
}
}
// validations check..
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
violations = validator.validate(entityInstance);
if (violations.isEmpty()) {
entity.getMethod("persist").invoke(entityInstance);
}
}
// return data back.
return getReturnRecord(writeOperation, entityInstance, recordObject,
violations);
}
private Collection<Property<?>> allProperties(Class<? extends Record> clazz) {
Set<Property<?>> rtn = new HashSet<Property<?>>();
for (Field f : clazz.getFields()) {
if (Modifier.isStatic(f.getModifiers())
&& Property.class.isAssignableFrom(f.getType())) {
try {
rtn.add((Property<?>) f.get(null));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return rtn;
}
@SuppressWarnings("unchecked")
private void ensureConfig() {
if (config == null) {
synchronized (this) {
if (config != null) {
return;
}
try {
final String serverOperation = getServletContext().getInitParameter(
SERVER_OPERATION_CONTEXT_PARAM);
if (null == serverOperation) {
failConfig();
}
Class<?> clazz = Class.forName(serverOperation);
if (Config.class.isAssignableFrom(clazz)) {
config = ((Class<? extends Config>) clazz).newInstance();
// initialize tokenToEntity map
tokenToEntityRecord = new HashMap<String, EntityRecordPair>();
for (Class<? extends Record> recordClass : config.recordTypes()) {
ServerType serverType = recordClass.getAnnotation(ServerType.class);
String token = (String) recordClass.getField("TOKEN").get(null);
if (token == null) {
throw new IllegalStateException("TOKEN field on "
+ recordClass.getName() + " can not be null");
}
EntityRecordPair previousValue = tokenToEntityRecord.get(token);
if (previousValue != null) {
throw new IllegalStateException(
"TOKEN fields have to be unique. TOKEN fields for both "
+ recordClass.getName() + " and "
+ previousValue.record.getName()
+ " have the same value, value = " + token);
}
tokenToEntityRecord.put(token, new EntityRecordPair(
serverType.type(), recordClass));
}
}
} catch (ClassNotFoundException e) {
failConfig(e);
} catch (InstantiationException e) {
failConfig(e);
} catch (IllegalAccessException e) {
failConfig(e);
} catch (SecurityException e) {
failConfig(e);
} catch (ClassCastException e) {
failConfig(e);
} catch (NoSuchFieldException e) {
failConfig(e);
}
}
}
}
private void failConfig() {
failConfig(null);
}
private void failConfig(Throwable e) {
final String message = String.format("Context parameter \"%s\" must name "
+ "a default instantiable configuration class implementing %s",
SERVER_OPERATION_CONTEXT_PARAM, RequestFactory.Config.class.getName());
throw new IllegalStateException(message, e);
}
private String getContent(HttpServletRequest request) throws IOException {
int contentLength = request.getContentLength();
byte contentBytes[] = new byte[contentLength];
BufferedInputStream bis = new BufferedInputStream(request.getInputStream());
try {
int readBytes = 0;
while (bis.read(contentBytes, readBytes, contentLength - readBytes) > 0) {
// read the contents
}
// TODO: encoding issues?
return new String(contentBytes);
} finally {
bis.close();
}
}
private Object getEntityInstance(WriteOperation writeOperation,
Class<?> entity, Object idValue, Class<?> idType)
throws SecurityException, InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
if (writeOperation == WriteOperation.CREATE) {
return entity.getConstructor().newInstance();
}
// TODO: check "version" validity.
return entity.getMethod("find" + entity.getSimpleName(), idType).invoke(
null, getSwizzledObject(idValue, idType));
}
/**
* Converts the returnValue of a 'get' method to a JSONArray.
*
* @param resultObject object returned by a 'get' method, must be of type
* List<?>
* @return the JSONArray
*/
private JSONArray getJsonArray(List<?> resultList,
Class<? extends Record> entityKeyClass) throws IllegalArgumentException,
SecurityException, IllegalAccessException, JSONException,
NoSuchMethodException, InvocationTargetException {
JSONArray jsonArray = new JSONArray();
if (resultList.size() == 0) {
return jsonArray;
}
for (Object entityElement : resultList) {
jsonArray.put(getJsonObject(entityElement, entityKeyClass));
}
return jsonArray;
}
private JSONObject getJsonObject(Object entityElement,
Class<? extends Record> entityKeyClass) throws JSONException,
NoSuchMethodException, IllegalAccessException, InvocationTargetException {
JSONObject jsonObject = new JSONObject();
for (Property<?> p : allProperties(entityKeyClass)) {
if (requestedProperty(p)) {
String propertyName = p.getName();
jsonObject.put(propertyName, getPropertyValueFromDataStore(
entityElement, propertyName));
}
}
return jsonObject;
}
/**
* Returns methodName corresponding to the propertyName that can be invoked on
* an entity.
*
* Example: "userName" returns prefix + "UserName". "version" returns prefix +
* "Version"
*/
private String getMethodNameFromPropertyName(String propertyName,
String prefix) {
if (propertyName == null) {
throw new NullPointerException("propertyName must not be null");
}
StringBuffer methodName = new StringBuffer(prefix);
methodName.append(propertyName.substring(0, 1).toUpperCase());
methodName.append(propertyName.substring(1));
return methodName.toString();
}
private RequestDefinition getOperation(String operationName) {
RequestDefinition operation;
operation = config.requestDefinitions().get(operationName);
if (null == operation) {
throw new IllegalArgumentException("Unknown operation " + operationName);
}
return operation;
}
/**
* @param jsonObject
* @return
* @throws JSONException
*/
private Map<String, String> getParameterMap(JSONObject jsonObject)
throws JSONException {
Map<String, String> parameterMap = new HashMap<String, String>();
Iterator<?> keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next().toString();
if (key.startsWith(RequestDataManager.PARAM_TOKEN)) {
parameterMap.put(key, jsonObject.getString(key));
}
}
return parameterMap;
}
/**
* Returns the property fields (name => type) for a record.
*/
private Map<String, Class<?>> getPropertiesFromRecord(
Class<? extends Record> record) throws SecurityException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Map<String, Class<?>> properties = new HashMap<String, Class<?>>();
for (Field f : record.getFields()) {
if (Property.class.isAssignableFrom(f.getType())) {
Class<?> propertyType = (Class<?>) f.getType().getMethod("getType").invoke(
f.get(null));
properties.put(f.getName(), propertyType);
}
}
return properties;
}
/**
* Returns the propertyValue in the right type, from the DataStore. The value
* is sent into the response.
*/
private Object getPropertyValueFromDataStore(Object entityElement,
String propertyName) throws SecurityException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
String methodName = getMethodNameFromPropertyName(propertyName, "get");
Method method = entityElement.getClass().getMethod(methodName);
Object returnValue = method.invoke(entityElement);
/*
* TODO: make these conventions more prominent. 1. encoding long as String
* 2. encoding Date as Double
*/
if (returnValue instanceof java.lang.Long) {
return returnValue.toString();
}
if (returnValue instanceof java.util.Date) {
return new Double(((java.util.Date) returnValue).getTime());
}
return returnValue;
}
/**
* Returns the property value, in the specified type, from the request object.
* The value is put in the DataStore.
*/
private Object getPropertyValueFromRequest(JSONObject recordObject,
String key, Class<?> propertyType) throws JSONException {
if (propertyType == java.lang.Integer.class) {
return new Integer(recordObject.getInt(key));
}
/*
* 1. decode String to long. 2. decode Double to Date.
*/
if (propertyType == java.lang.Long.class) {
return Long.valueOf(recordObject.getString(key));
}
if (propertyType == java.util.Date.class) {
return new Date((long) recordObject.getDouble(key));
}
return recordObject.get(key);
}
private JSONObject getReturnRecord(WriteOperation writeOperation,
Object entityInstance, JSONObject recordObject,
Set<ConstraintViolation<Object>> violations) throws SecurityException,
JSONException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
JSONObject returnObject = new JSONObject();
if (writeOperation != WriteOperation.CREATE || violations == null) {
// currently sending back only two properties.
for (String propertyName : new String[] {"id", "version"}) {
if ("version".equals(propertyName) && violations != null) {
continue;
}
returnObject.put(propertyName, getPropertyValueFromDataStore(
entityInstance, propertyName));
}
}
if (violations != null) {
returnObject.put("violations", getViolationsAsJson(violations));
}
if (writeOperation == WriteOperation.CREATE) {
returnObject.put("futureId", recordObject.getString("id"));
}
return returnObject;
}
/**
* Swizzle an idValue received from the client to the type expected by the
* server. Return the object of the new type.
*/
private Object getSwizzledObject(Object idValue, Class<?> idType) {
if (idValue.getClass() == idType) {
return idValue;
}
// swizzle from String to Long
if (idValue.getClass() == String.class && idType == Long.class) {
return new Long((String) idValue);
}
throw new IllegalArgumentException("id is of type: " + idValue.getClass()
+ ", expected type: " + idType);
}
private JSONObject getViolationsAsJson(
Set<ConstraintViolation<Object>> violations) throws JSONException {
JSONObject violationsAsJson = new JSONObject();
for (ConstraintViolation<Object> violation : violations) {
violationsAsJson.put(violation.getPropertyPath().toString(),
violation.getMessage());
}
return violationsAsJson;
}
/**
* returns true if the property has been requested. TODO: use the properties
* that should be coming with the request.
*
* @param p the field of entity ref
* @return has the property value been requested
*/
private boolean requestedProperty(Property<?> p) {
return !BLACK_LIST.contains(p.getName());
}
private void sync(String content, PrintWriter writer)
throws SecurityException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException, InstantiationException {
try {
JSONObject jsonObject = new JSONObject(content);
JSONObject returnJsonObject = new JSONObject();
for (WriteOperation writeOperation : WriteOperation.values()) {
if (!jsonObject.has(writeOperation.name())) {
continue;
}
JSONArray reportArray = new JSONArray(
jsonObject.getString(writeOperation.name()));
JSONArray returnArray = new JSONArray();
int length = reportArray.length();
if (length == 0) {
throw new IllegalArgumentException("No json array for "
+ writeOperation.name() + " should have been sent");
}
for (int i = 0; i < length; i++) {
JSONObject recordWithSchema = reportArray.getJSONObject(i);
Iterator<?> iterator = recordWithSchema.keys();
String recordToken = (String) iterator.next();
if (iterator.hasNext()) {
throw new IllegalArgumentException(
"There cannot be more than one record token");
}
JSONObject recordObject = recordWithSchema.getJSONObject(recordToken);
JSONObject returnObject = updateRecordInDataStore(recordToken,
recordObject, writeOperation);
returnArray.put(returnObject);
}
returnJsonObject.put(writeOperation.name(), returnArray);
}
writer.print(returnJsonObject.toString());
} catch (JSONException e) {
throw new IllegalArgumentException("sync failed: ", e);
}
}
/**
* Update propertiesInRecord based on the types of entity.
*/
private void updatePropertyTypes(Map<String, Class<?>> propertiesInRecord,
Class<?> entity) {
for (Field field : entity.getDeclaredFields()) {
Class<?> fieldType = propertiesInRecord.get(field.getName());
if (fieldType != null) {
propertiesInRecord.put(field.getName(), field.getType());
}
}
}
private void validateKeys(JSONObject recordObject,
Set<String> declaredProperties) {
Iterator<?> keys = recordObject.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
if (!declaredProperties.contains(key)) {
throw new IllegalArgumentException("key " + key
+ " is not permitted to be set");
}
}
}
}