| /* |
| * 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()); |
| } |
| } |
| } |
| } |