blob: a0974cf59ffe08b08bcd79cca0ecbf02bd5805c7 [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.DataTransferObject;
import com.google.gwt.requestfactory.shared.RequestData;
import com.google.gwt.valuestore.shared.Property;
import com.google.gwt.valuestore.shared.PropertyReference;
import com.google.gwt.valuestore.shared.Record;
import com.google.gwt.valuestore.shared.WriteOperation;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
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 java.util.logging.Logger;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* An implementation of RequestProcessor for JSON encoded payloads.
*/
public class JsonRequestProcessor implements RequestProcessor<String> {
// TODO should we consume String, InputStream, or JSONObject?
private static class DvsData {
private final JSONObject jsonObject;
private final WriteOperation writeOperation;
DvsData(JSONObject jsonObject, WriteOperation writeOperation) {
this.jsonObject = jsonObject;
this.writeOperation = writeOperation;
}
}
private static class EntityData {
private final Object entityInstance;
// TODO: violations should have more structure than JSONObject
private final JSONObject violations;
EntityData(Object entityInstance, JSONObject violations) {
this.entityInstance = entityInstance;
this.violations = violations;
}
}
private static class EntityKey {
private final boolean isFuture;
// TODO: update for non-long id?
private final long id;
private final Class<? extends Record> record;
EntityKey(long id, boolean isFuture, Class<? extends Record> record) {
this.id = id;
this.isFuture = isFuture;
assert record != null;
this.record = record;
}
@Override
public boolean equals(Object ob) {
if (!(ob instanceof EntityKey)) {
return false;
}
EntityKey other = (EntityKey) ob;
return (id == other.id) && (isFuture == other.isFuture)
&& (record.equals(other.record));
}
@Override
public int hashCode() {
return (int) (31 * this.record.hashCode() + (31 * this.id + (isFuture ? 1
: 0)));
}
}
private static class SerializedEntity {
// the field value of the entityInstance might change from under us.
private final Object entityInstance;
private final JSONObject serializedEntity;
SerializedEntity(Object entityInstance, JSONObject serializedEntity) {
this.entityInstance = entityInstance;
this.serializedEntity = serializedEntity;
}
}
public static final String RELATED = "related";
public static final Set<String> BLACK_LIST = initBlackList();
private static final Logger log = Logger.getLogger(JsonRequestProcessor.class.getName());
public static Set<String> initBlackList() {
Set<String> blackList = new HashSet<String>();
for (String str : new String[] {"password"}) {
blackList.add(str);
}
return Collections.unmodifiableSet(blackList);
}
private RequestProperty propertyRefs;
private Map<String, JSONObject> relatedObjects = new HashMap<String, JSONObject>();
private OperationRegistry operationRegistry;
/*
* <li>Request comes in. Construct the involvedKeys, dvsDataMap and
* beforeDataMap, using DVS and parameters.
*
* <li>Apply the DVS and construct the afterDvsDataMqp.
*
* <li>Invoke the method noted in the operation.
*
* <li>Find the changes that need to be sent back.
*/
private Set<EntityKey> involvedKeys = new HashSet<EntityKey>();
private Map<EntityKey, DvsData> dvsDataMap = new HashMap<EntityKey, DvsData>();
private Map<EntityKey, SerializedEntity> beforeDataMap = new HashMap<EntityKey, SerializedEntity>();
private Map<EntityKey, EntityData> afterDvsDataMap = new HashMap<EntityKey, EntityData>();
public 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;
}
public String decodeAndInvokeRequest(String encodedRequest) throws Exception {
try {
Logger.getLogger(this.getClass().getName()).finest("Incoming request "
+ encodedRequest);
String response = processJsonRequest(encodedRequest).toString();
Logger.getLogger(this.getClass().getName()).finest("Outgoing response "
+ response);
return response;
} 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);
}
}
/**
* Encodes parameter value.
*/
public Object decodeParameterValue(Type genericParameterType,
String parameterValue) {
Class<?>parameterType = null;
if (genericParameterType instanceof Class<?>) {
parameterType = (Class<?>) genericParameterType;
}
if (genericParameterType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) genericParameterType;
if (PropertyReference.class == pType.getRawType()) {
parameterType = (Class<?>) pType.getActualTypeArguments()[0];
}
}
if (String.class == parameterType) {
return parameterValue;
}
if (Boolean.class == parameterType || boolean.class == parameterType) {
return Boolean.valueOf(parameterValue);
}
if (Integer.class == parameterType || int.class == parameterType) {
return new Integer(parameterValue);
}
if (Byte.class == parameterType || byte.class == parameterType) {
return new Byte(parameterValue);
}
if (Short.class == parameterType || short.class == parameterType) {
return new Short(parameterValue);
}
if (Float.class == parameterType || float.class == parameterType) {
return new Float(parameterValue);
}
if (Double.class == parameterType || double.class == parameterType) {
return new Double(parameterValue);
}
if (Long.class == parameterType || long.class == parameterType) {
return new Long(parameterValue);
}
if (Character.class == parameterType || char.class == parameterType) {
return parameterValue.charAt(0);
}
if (BigInteger.class == parameterType) {
return new BigInteger(parameterValue);
}
if (BigDecimal.class == parameterType) {
return new BigDecimal(parameterValue);
}
if (parameterType.isEnum()) {
try {
int ordinal = Integer.parseInt(parameterValue);
Method valuesMethod = parameterType.getDeclaredMethod("values",
new Class[0]);
if (valuesMethod != null) {
valuesMethod.setAccessible(true);
Enum<?>[] values = (Enum<?>[]) valuesMethod.invoke(null);
// we use ordinal serialization instead of name since future compiler
// opts may remove names
for (Enum<?> e : values) {
if (ordinal == e.ordinal()) {
return e;
}
}
}
throw new IllegalArgumentException(
"Can't decode enum " + parameterType + " no matching ordinal "
+ ordinal);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
"Can't decode enum " + parameterType);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException(
"Can't decode enum " + parameterType);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"Can't decode enum " + parameterType);
}
}
if (Date.class == parameterType) {
return new Date(Long.parseLong(parameterValue));
}
if (Record.class.isAssignableFrom(parameterType)) {
/* TODO: 1. Don't resolve in this step, just get EntityKey. May need to
* use DVS.
*
* 2. Merge the following and the object resolution code in getEntityKey.
* 3. Update the involvedKeys set.
*/
DataTransferObject service = parameterType.getAnnotation(DataTransferObject.class);
if (service != null) {
Class<?> sClass = service.value();
EntityKey entityKey = getEntityKey(parameterValue.toString());
DvsData dvsData = dvsDataMap.get(entityKey);
try {
if (dvsData != null) {
EntityData entityData = getEntityDataForRecord(entityKey,
dvsData.jsonObject, dvsData.writeOperation);
return entityData.entityInstance;
} else {
Method findMeth = sClass.getMethod(getMethodNameFromPropertyName(sClass.getSimpleName(), "find"), Long.class);
return findMeth.invoke(null, entityKey.id);
}
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
"No such method " + getMethodNameFromPropertyName(
sClass.getSimpleName(), "find"), e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Can't invoke method", e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Can't invoke method", e);
}
}
}
throw new IllegalArgumentException(
"Unknown parameter type: " + parameterType);
}
public Object encodePropertyValue(Object value) {
if (value == null) {
return value;
}
Class<?> type = value.getClass();
if (Boolean.class == type) {
return value;
}
if (Date.class.isAssignableFrom(type)) {
return String.valueOf(((Date) value).getTime());
}
if (Enum.class.isAssignableFrom(type)) {
return Double.valueOf(((Enum<?>) value).ordinal());
}
if (BigDecimal.class == type || BigInteger.class == type
|| Long.class == type) {
return String.valueOf(value);
}
if (Number.class.isAssignableFrom(type)) {
return ((Number) value).doubleValue();
}
return String.valueOf(value);
}
/**
* Returns the propertyValue in the right type, from the DataStore. The value
* is sent into the response.
*/
public Object encodePropertyValueFromDataStore(Object entityElement,
Class<?> propertyType, String propertyName,
RequestProperty propertyContext)
throws SecurityException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException, JSONException {
String methodName = getMethodNameFromPropertyName(propertyName, "get");
Method method = entityElement.getClass().getMethod(methodName);
Object returnValue = method.invoke(entityElement);
if (returnValue != null && Record.class.isAssignableFrom(propertyType)) {
Method idMethod = returnValue.getClass().getMethod("getId");
Long id = (Long) idMethod.invoke(returnValue);
String keyRef = operationRegistry.getSecurityProvider().encodeClassType(
propertyType)
+ "-" + id;
addRelatedObject(keyRef, returnValue,
castToRecordClass(propertyType),
propertyContext.getProperty(propertyName));
// replace value with id reference
return keyRef;
}
return encodePropertyValue(returnValue);
}
/**
* Generate an ID for a new record. The default behavior is to return null and
* let the data store generate the ID automatically.
*
* @param key the key of the record field
* @return the ID of the new record, or null to auto generate
*/
public Long generateIdForCreate(@SuppressWarnings("unused") String key) {
// ignored. id is assigned by default.
return null;
}
/**
* Returns the entityData for a record in the DeltaValueStore.
* <p>
* A <i>set</i> might have side-effects, but we don't handle that.
*/
public EntityData getEntityDataForRecord(EntityKey entityKey,
JSONObject recordObject, WriteOperation writeOperation) {
try {
Class<?> entity = getEntityFromRecordAnnotation(entityKey.record);
Map<String, Class<?>> propertiesInRecord = getPropertiesFromRecord(entityKey.record);
Map<String, Class<?>> propertiesToDTO = new HashMap<String, Class<?>>(propertiesInRecord);
validateKeys(recordObject, propertiesInRecord.keySet());
updatePropertyTypes(propertiesInRecord, entity);
// get entityInstance
Object entityInstance = getEntityInstance(writeOperation, entity,
recordObject.get("id"), propertiesInRecord.get("id"));
Set<ConstraintViolation<Object>> violations = Collections.emptySet();
Iterator<?> keys = recordObject.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
Class<?> propertyType = propertiesInRecord.get(key);
if (writeOperation == WriteOperation.CREATE && ("id".equals(key))) {
Long id = generateIdForCreate(key);
if (id != null) {
entity.getMethod(getMethodNameFromPropertyName(key, "set"),
propertyType).invoke(entityInstance, id);
}
} else {
Object propertyValue = getPropertyValueFromRequest(recordObject, key,
propertiesToDTO.get(key));
entity.getMethod(getMethodNameFromPropertyName(key, "set"),
propertyType).invoke(entityInstance, propertyValue);
}
}
// validations check..
Validator validator = null;
try {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
} catch (Exception e) {
/*
* This is JBoss's clumsy way of telling us that the system has not been
* configured.
*/
log.info(String.format(
"Ignoring exception caught initializing bean validation framework. "
+ "It is probably unconfigured or misconfigured. [%s] %s ",
e.getClass().getName(), e.getLocalizedMessage()));
}
if (validator != null) {
violations = validator.validate(entityInstance);
}
return new EntityData(entityInstance, (violations.isEmpty() ? null
: getViolationsAsJson(violations)));
} catch (Exception ex) {
log.severe(String.format("Caught exception [%s] %s",
ex.getClass().getName(), ex.getLocalizedMessage()));
return getEntityDataForException(ex);
}
}
@SuppressWarnings("unchecked")
public Class<Object> getEntityFromRecordAnnotation(
Class<? extends Record> record) {
DataTransferObject dtoAnn = record.getAnnotation(DataTransferObject.class);
if (dtoAnn != null) {
return (Class<Object>) dtoAnn.value();
}
throw new IllegalArgumentException("Record class " + record.getName()
+ " missing DataTransferObject annotation");
}
public 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, decodeParameterValue(idType, idValue.toString()));
}
/**
* Converts the returnValue of a 'get' method to a JSONArray.
*
* @param resultList object returned by a 'get' method, must be of type
* List<?>
* @param entityKeyClass the class type of the contained value
* @return the JSONArray
*/
public 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, propertyRefs));
}
return jsonArray;
}
public JSONObject getJsonObject(Object entityElement,
Class<? extends Record> entityKeyClass, RequestProperty propertyContext)
throws JSONException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
JSONObject jsonObject = new JSONObject();
for (Property<?> p : allProperties(entityKeyClass)) {
if (requestedProperty(p, propertyContext)) {
String propertyName = p.getName();
jsonObject.put(propertyName, encodePropertyValueFromDataStore(
entityElement, p.getType(), propertyName, propertyContext));
}
}
return jsonObject;
}
/**
* Returns methodName corresponding to the propertyName that can be invoked on
* an entity.
*
* Example: "userName" returns prefix + "UserName". "version" returns prefix +
* "Version"
*/
public 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();
}
/**
* Returns Object[0][0] as the entityKey corresponding to the object instance
* or null if it is a static method. Returns Object[1] as the params array.
*/
public Object[][] getObjectsFromParameterMap(boolean isInstanceMethod,
Map<String, String> parameterMap, Type parameterClasses[]) {
// TODO: create an EntityMethodCall (instance, args) instead.
assert parameterClasses != null;
Object args[][] = new Object[2][];
args[0] = new Object[1];
if (isInstanceMethod) {
EntityKey entityKey = getEntityKey(parameterMap.get(RequestData.PARAM_TOKEN + "0"));
involvedKeys.add(entityKey);
args[0][0] = entityKey;
} else {
args[0][0] = null;
}
// TODO: update the involvedKeys for other params
int offset = (isInstanceMethod ? 1 : 0);
args[1] = new Object[parameterClasses.length - offset];
for (int i = 0; i < parameterClasses.length - offset; i++) {
args[1][i] = decodeParameterValue(parameterClasses[i + offset],
parameterMap.get(RequestData.PARAM_TOKEN + (i + offset)));
}
return args;
}
public RequestDefinition getOperation(String operationName) {
RequestDefinition operation;
operation = operationRegistry.getOperation(operationName);
if (null == operation) {
throw new IllegalArgumentException("Unknown operation " + operationName);
}
return operation;
}
/**
* @param jsonObject
* @return
* @throws org.json.JSONException
*/
public 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(RequestData.PARAM_TOKEN)) {
parameterMap.put(key, jsonObject.getString(key));
}
}
return parameterMap;
}
/**
* Returns the property fields (name => type) for a record.
*/
public 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 property value, in the specified type, from the request object.
* The value is put in the DataStore.
*/
public Object getPropertyValueFromRequest(JSONObject recordObject, String key,
Class<?> propertyType) throws JSONException {
return decodeParameterValue(propertyType, recordObject.get(key).toString());
}
@SuppressWarnings("unchecked")
public Class<Record> getRecordFromClassToken(String recordToken) {
try {
Class<?> clazz = Class.forName(recordToken, false,
getClass().getClassLoader());
if (Record.class.isAssignableFrom(clazz)) {
return (Class<Record>) clazz;
}
throw new SecurityException(
"Attempt to access non-record class " + recordToken);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
"Non-existent record class " + recordToken);
}
}
public 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;
}
public Object invokeDomainMethod(Object domainObject, Method domainMethod, Object args[])
throws IllegalAccessException, InvocationTargetException {
return domainMethod.invoke(domainObject, args);
}
public Object processJsonRequest(String jsonRequestString)
throws JSONException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException, ClassNotFoundException {
RequestDefinition operation;
JSONObject topLevelJsonObject = new JSONObject(jsonRequestString);
String operationName = topLevelJsonObject.getString(RequestData.OPERATION_TOKEN);
String propertyRefsString = topLevelJsonObject.has(RequestData.PROPERTY_REF_TOKEN)
? topLevelJsonObject.getString(RequestData.PROPERTY_REF_TOKEN) : "";
propertyRefs = RequestProperty.parse(propertyRefsString);
operation = getOperation(operationName);
Class<?> domainClass = Class.forName(operation.getDomainClassName());
Method domainMethod = domainClass.getMethod(
operation.getDomainMethodName(), operation.getParameterTypes());
if (Modifier.isStatic(domainMethod.getModifiers()) == operation.isInstance()) {
throw new IllegalArgumentException("the " + domainMethod.getName()
+ " should " + (operation.isInstance() ? "not " : "") + "be static");
}
if (topLevelJsonObject.has(RequestData.CONTENT_TOKEN)) {
// updates involvedKeys and dvsDataMap.
decodeDVS(topLevelJsonObject.getString(RequestData.CONTENT_TOKEN));
}
// get the domain object (for instance methods) and args.
Object args[][] = getObjectsFromParameterMap(operation.isInstance(),
getParameterMap(topLevelJsonObject), operation.getRequestParameterTypes());
// Construct beforeDataMap
constructBeforeDataMap();
// Construct afterDvsDataMap.
constructAfterDvsDataMap();
// resolve parameters that are so far just EntityKeys.
// TODO: resolve paramters other than the domainInstance
EntityKey domainEntityKey = null;
if (args[0][0] != null) {
domainEntityKey = (EntityKey) args[0][0];
EntityData domainEntityData = afterDvsDataMap.get(domainEntityKey);
assert domainEntityData != null;
args[0][0] = domainEntityData.entityInstance;
assert args[0][0] != null;
}
Object result = invokeDomainMethod(args[0][0], domainMethod, args[1]);
JSONObject sideEffects = getSideEffects();
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()));
}
JSONObject envelop = new JSONObject();
if (result instanceof List<?>) {
envelop.put(RequestData.RESULT_TOKEN, toJsonArray(operation, result));
} else if (result instanceof Number || result instanceof Enum<?>
|| result instanceof String || result instanceof Date
|| result instanceof Character || result instanceof Boolean) {
envelop.put(RequestData.RESULT_TOKEN, result);
} else {
JSONObject jsonObject = toJsonObject(operation, result);
envelop.put(RequestData.RESULT_TOKEN, jsonObject);
}
envelop.put(RequestData.SIDE_EFFECTS_TOKEN, sideEffects);
envelop.put(RequestData.RELATED_TOKEN, encodeRelatedObjectsToJson());
return envelop;
}
public void setOperationRegistry(OperationRegistry operationRegistry) {
this.operationRegistry = operationRegistry;
}
public 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");
}
}
}
/**
* Returns true iff the after and before JSONObjects are different.
*
* @throws JSONException
*/
boolean hasChanged(JSONObject before, JSONObject after) {
if (before == null) {
return after != null;
}
// before != null
if (after == null) {
return true;
}
try {
// before != null && after != null
Iterator<?> keyIterator = before.keys();
while (keyIterator.hasNext()) {
String key = keyIterator.next().toString();
Object beforeValue = before.get(key);
Object afterValue = after.get(key);
if (beforeValue == null) {
if (afterValue == null) {
continue;
}
return true;
}
if (afterValue == null) {
return true;
}
if (!beforeValue.equals(afterValue)) {
return true;
}
}
} catch (JSONException ex) {
log.warning("Encountered a JSONException " + ex.getMessage()
+ " in getChanged");
return false;
}
return false;
}
private void addRelatedObject(String keyRef, Object returnValue,
Class<? extends Record> propertyType, RequestProperty propertyContext)
throws JSONException, IllegalAccessException, NoSuchMethodException,
InvocationTargetException {
relatedObjects.put(keyRef, getJsonObject(returnValue, propertyType,
propertyContext));
}
@SuppressWarnings("unchecked")
private Class<? extends Record> castToRecordClass(Class<?> propertyType) {
return (Class<? extends Record>) propertyType;
}
/**
* @throws JSONException
*
*/
private void constructAfterDvsDataMap() {
afterDvsDataMap = new HashMap<EntityKey, EntityData>();
for (EntityKey entityKey : involvedKeys) {
// use the beforeDataMap and dvsDataMap
DvsData dvsData = dvsDataMap.get(entityKey);
if (dvsData != null) {
EntityData entityData = getEntityDataForRecord(entityKey,
dvsData.jsonObject, dvsData.writeOperation);
if (entityKey.isFuture) {
// TODO: assert that the id is null for entityData.entityInstance
}
afterDvsDataMap.put(entityKey, entityData);
} else {
assert !entityKey.isFuture;
SerializedEntity serializedEntity = beforeDataMap.get(entityKey);
assert serializedEntity.entityInstance != null;
afterDvsDataMap.put(entityKey, new EntityData(
serializedEntity.entityInstance, null));
}
}
}
/**
* Constructs the beforeDataMap.
*
* <p>
* Algorithm: go through the involvedKeys, and find the entityData
* corresponding to each.
*
*/
private void constructBeforeDataMap() throws IllegalArgumentException,
SecurityException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException, JSONException {
for (EntityKey entityKey : involvedKeys) {
if (entityKey.isFuture) {
// the "before" is empty.
continue;
}
beforeDataMap.put(entityKey, getSerializedEntity(entityKey));
}
}
/**
* Decode deltaValueStore to populate involvedKeys and dvsDataMap.
*/
private void decodeDVS(String content) throws SecurityException {
try {
JSONObject jsonObject = new JSONObject(content);
for (WriteOperation writeOperation : WriteOperation.values()) {
if (!jsonObject.has(writeOperation.name())) {
continue;
}
JSONArray reportArray = new JSONArray(
jsonObject.getString(writeOperation.name()));
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);
Class<? extends Record> record = getRecordFromClassToken(recordToken);
EntityKey entityKey = new EntityKey(recordObject.getLong("id"),
(writeOperation == WriteOperation.CREATE), record);
involvedKeys.add(entityKey);
dvsDataMap.put(entityKey, new DvsData(recordObject, writeOperation));
}
}
} catch (JSONException e) {
throw new IllegalArgumentException("sync failed: ", e);
}
}
private WriteOperation detectDeleteOrUpdate(EntityKey entityKey,
EntityData entityData) throws IllegalArgumentException,
SecurityException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException, JSONException {
if (entityData.entityInstance == null) {
return null;
}
Class<?> entityClass = getEntityFromRecordAnnotation(entityKey.record);
// TODO: merge this lookup code with other uses.
Object entityInstance = entityClass.getMethod(
"find" + entityClass.getSimpleName(), Long.class).invoke(null,
new Long(entityKey.id));
if (entityInstance == null) {
return WriteOperation.DELETE;
}
if (hasChanged(beforeDataMap.get(entityKey).serializedEntity,
serializeEntity(entityInstance, entityKey.record))) {
return WriteOperation.UPDATE;
}
return null;
}
private JSONObject encodeRelatedObjectsToJson() throws JSONException {
JSONObject array = new JSONObject();
for (Map.Entry<String, JSONObject> entry : relatedObjects.entrySet()) {
array.put(entry.getKey(), entry.getValue());
}
return array;
}
private JSONObject getCreateReturnRecord(EntityKey originalEntityKey,
EntityData entityData) throws SecurityException, JSONException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
// id/futureId, the identifying field is sent back from the incoming record.
assert originalEntityKey.isFuture;
Object entityInstance = entityData.entityInstance;
assert entityInstance != null;
JSONObject returnObject = new JSONObject();
returnObject.put("futureId", originalEntityKey.id + "");
boolean hasViolations = entityData.violations != null
&& entityData.violations.length() > 0;
if (hasViolations) {
returnObject.put("violations", entityData.violations);
} else {
Object newId = encodePropertyValueFromDataStore(entityInstance,
Long.class, "id", propertyRefs);
if (newId == null) {
log.warning("Record with futureId " + originalEntityKey.id
+ " not persisted");
return null; // no changeRecord for this CREATE.
}
returnObject.put("id", getSchemaAndId(originalEntityKey.record, newId));
returnObject.put("version", encodePropertyValueFromDataStore(
entityInstance, Integer.class, "version", propertyRefs));
}
return returnObject;
}
private EntityData getEntityDataForException(Exception ex) {
JSONObject violations = null;
try {
// expecting violations to be a JSON object.
violations = new JSONObject();
if (ex instanceof NumberFormatException) {
violations.put("Expected a number instead of String", ex.getMessage());
} else {
violations.put("", "unexpected server error");
}
} catch (JSONException e) {
// ignore.
e.printStackTrace();
}
return new EntityData(null, violations);
}
/**
* Given param0, return the EntityKey. String is of the form
* "239-NO-com....EmployeeRecord" or "239-IS-com...EmployeeRecord".
*/
private EntityKey getEntityKey(String string) {
String parts[] = string.split("-");
assert parts.length == 3;
Long id = Long.parseLong(parts[0]);
return new EntityKey(id, "IS".equals(parts[1]),
getRecordFromClassToken(parts[2]));
}
private String getSchemaAndId(Class<? extends Record> record, Object newId) {
return record.getName() + "-" + newId;
}
private SerializedEntity getSerializedEntity(EntityKey entityKey)
throws IllegalArgumentException, SecurityException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException,
JSONException {
Class<?> entityClass = getEntityFromRecordAnnotation(entityKey.record);
// TODO: merge this lookup code with other uses.
Object entityInstance = entityClass.getMethod(
"find" + entityClass.getSimpleName(), Long.class).invoke(null,
new Long(entityKey.id));
JSONObject serializedEntity = serializeEntity(entityInstance,
entityKey.record);
return new SerializedEntity(entityInstance, serializedEntity);
}
/**
* Returns a JSONObject with at most three keys: CREATE, UPDATE, DELETE. Each
* value is a JSONArray of JSONObjects.
*/
private JSONObject getSideEffects() throws SecurityException, JSONException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
JSONObject sideEffects = new JSONObject();
JSONArray createArray = new JSONArray();
JSONArray deleteArray = new JSONArray();
JSONArray updateArray = new JSONArray();
for (EntityKey entityKey : involvedKeys) {
EntityData entityData = afterDvsDataMap.get(entityKey);
assert entityData != null;
if (entityKey.isFuture) {
JSONObject createRecord = getCreateReturnRecord(entityKey, entityData);
if (createRecord != null) {
createArray.put(createRecord);
}
continue;
}
WriteOperation writeOperation = detectDeleteOrUpdate(entityKey,
entityData);
if (writeOperation == WriteOperation.DELETE) {
JSONObject deleteRecord = new JSONObject();
deleteRecord.put("id", getSchemaAndId(entityKey.record, entityKey.id));
deleteArray.put(deleteRecord);
}
if (writeOperation == WriteOperation.UPDATE) {
JSONObject updateRecord = new JSONObject();
updateRecord.put("id", getSchemaAndId(entityKey.record, entityKey.id));
updateArray.put(updateRecord);
}
}
if (createArray.length() > 0) {
sideEffects.put(WriteOperation.CREATE.name(), createArray);
}
if (deleteArray.length() > 0) {
sideEffects.put(WriteOperation.DELETE.name(), deleteArray);
}
if (updateArray.length() > 0) {
sideEffects.put(WriteOperation.UPDATE.name(), updateArray);
}
return sideEffects;
}
/**
* 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
* @param propertyContext the root of the current dotted property reference
* @return has the property value been requested
*/
private boolean requestedProperty(Property<?> p,
RequestProperty propertyContext) {
if (Record.class.isAssignableFrom(p.getType())) {
return propertyContext.hasProperty(p.getName());
} else {
return !BLACK_LIST.contains(p.getName());
}
}
/**
* Return the properties of an entityInstance, visible on the client, as a
* JSONObject.
*<p>
* TODO: clean up the copy-paste from getJSONObject.
*/
private JSONObject serializeEntity(Object entityInstance,
Class<? extends Record> recordClass) throws SecurityException,
NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
InvocationTargetException, JSONException {
if (entityInstance == null) {
return null;
}
JSONObject jsonObject = new JSONObject();
for (Property<?> p : allProperties(recordClass)) {
String propertyName = p.getName();
String methodName = getMethodNameFromPropertyName(propertyName, "get");
Method method = entityInstance.getClass().getMethod(methodName);
Object returnValue = method.invoke(entityInstance);
Object propertyValue;
if (returnValue != null && Record.class.isAssignableFrom(p.getType())) {
Method idMethod = returnValue.getClass().getMethod("getId");
Long id = (Long) idMethod.invoke(returnValue);
propertyValue = id + "-NO-" + operationRegistry.getSecurityProvider().encodeClassType(
p.getType());
} else {
propertyValue = encodePropertyValue(returnValue);
}
jsonObject.put(propertyName, propertyValue);
}
return jsonObject;
}
@SuppressWarnings("unchecked")
private Object toJsonArray(RequestDefinition operation, Object result)
throws IllegalAccessException, JSONException, NoSuchMethodException,
InvocationTargetException {
JSONArray jsonArray = getJsonArray((List<?>) result,
(Class<? extends Record>) operation.getReturnType());
return jsonArray;
}
@SuppressWarnings("unchecked")
private JSONObject toJsonObject(RequestDefinition operation, Object result)
throws JSONException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
JSONObject jsonObject = getJsonObject(result,
(Class<? extends Record>) operation.getReturnType(), propertyRefs);
return jsonObject;
}
/**
* 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());
}
}
}
}