/* Cryson Copyright 2011-2012 Björn Sperber (cryson@sperber.se) 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 se.sperber.cryson.serialization; import com.google.common.collect.Sets; import com.google.gson.*; import org.hibernate.proxy.HibernateProxy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import se.sperber.cryson.security.Restrictable; import javax.annotation.PostConstruct; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; @Component public class CrysonSerializer { private Gson gson; private Gson gsonAllInclusive; private JsonParser jsonParser; private final Map<Class<?>, Set<Field>> lazyFieldsCache = new ConcurrentHashMap<Class<?>, Set<Field>>(); private final Map<Class<?>, Set<Field>> userTypeFieldsCache = new ConcurrentHashMap<Class<?>, Set<Field>>(); private final Set<String> allowedUnauthorizedAttributeNames = Sets.newHashSet("id", "crysonEntityClass"); @Autowired private ReflectionHelper reflectionHelper; @Autowired private LazyAssociationExclusionStrategy lazyAssociationExclusionStrategy; @Autowired private UserTypeExclusionStrategy userTypeExclusionStrategy; @Autowired private CrysonExcludeExclusionStrategy crysonExcludeExclusionStrategy; @PostConstruct public void setupGson() { jsonParser = new JsonParser(); GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.serializeNulls(); gsonBuilder.setDateFormat("yyyy-MM-dd HH:mm:ss Z"); HibernateProxyTypeAdapter hibernateProxyTypeAdapter = new HibernateProxyTypeAdapter(); gsonBuilder.registerTypeHierarchyAdapter(HibernateProxy.class, hibernateProxyTypeAdapter); gsonAllInclusive = gsonBuilder.create(); gsonBuilder.setExclusionStrategies(lazyAssociationExclusionStrategy, userTypeExclusionStrategy, crysonExcludeExclusionStrategy); gson = gsonBuilder.create(); hibernateProxyTypeAdapter.setGson(gson); } public JsonElement serializeToTree(Object object, Set<String> associationsToInclude) { JsonElement jsonElement = gson.toJsonTree(object); augmentJsonElement(object, jsonElement, associationsToInclude); return jsonElement; } public String serializeTree(JsonElement jsonElement) { return gson.toJson(jsonElement); } public String serialize(Object object, Set<String> associationsToInclude) { return serializeTree(serializeToTree(object, associationsToInclude)); } public String serialize(Object object) { return serialize(object, Collections.<String>emptySet()); } public String serializeUnauthorizedEntity(String entityName, Long id) { UnauthorizedEntity entity = new UnauthorizedEntity(entityName, id); return gson.toJson(entity); } public String serializeWithoutAugmentation(Object object) { return gson.toJson(object); } public JsonElement serializeToTreeWithoutAugmentation(Object object) { return gson.toJsonTree(object); } public <T> T deserialize(String json, Class<T> classOfT, Map<Long, Long> replacedTemporaryIds) { JsonElement jsonElement = jsonParser.parse(json); return augmentEntity(gson.fromJson(json, classOfT), jsonElement, replacedTemporaryIds); } public <T> T deserialize(JsonElement jsonElement, Class<T> classOfT, Map<Long, Long> replacedTemporaryIds) { return augmentEntity(gson.fromJson(jsonElement, classOfT), jsonElement, replacedTemporaryIds); } private void augmentJsonElement(Object rawObject, JsonElement jsonElement, Set<String> associationsToInclude) { Object object = HibernateProxyTypeAdapter.initializeAndUnproxy(rawObject); try { if (object instanceof Collection) { int ix = 0; JsonArray jsonArray = jsonElement.getAsJsonArray(); for(Object subObject : (Collection)object) { JsonElement subJsonElement = jsonArray.get(ix++); augmentJsonElement(subObject, subJsonElement, associationsToInclude); } } else { jsonElement.getAsJsonObject().add("crysonEntityClass", new JsonPrimitive(object.getClass().getSimpleName())); if (object instanceof Restrictable) { if (!((Restrictable)object).isReadableBy(SecurityContextHolder.getContext().getAuthentication())) { transformJsonObjectToUnauthorizedEntity(jsonElement.getAsJsonObject()); return; } } for(Map.Entry<String, JsonElement> member : jsonElement.getAsJsonObject().entrySet()) { if (member.getValue().isJsonObject() || member.getValue().isJsonArray()) { Field field = reflectionHelper.getField(object, member.getKey()); field.setAccessible(true); augmentJsonElement(field.get(object), member.getValue(), subAssociationsToInclude(associationsToInclude, member.getKey())); } } Set<Field> userTypeFields = getUserTypeFields(object.getClass()); for (Field field : userTypeFields) { Object fieldValue = field.get(object); if (fieldValue != null && Map.class.isAssignableFrom(field.getType())) { JsonElement jsonFieldValue = gsonAllInclusive.toJsonTree(fieldValue); jsonElement.getAsJsonObject().add(field.getName() + "_cryson_usertype", jsonFieldValue); } } Set<Method> transientGetters = reflectionHelper.getAllDeclaredVirtualAttributeGetters(object.getClass()); for (Method method : transientGetters) { Object methodValue = method.invoke(object); JsonElement jsonFieldValue = gsonAllInclusive.toJsonTree(methodValue); jsonElement.getAsJsonObject().add(reflectionHelper.getAttributeNameFromGetterName(method.getName()), jsonFieldValue); } Set<Field> fields = getLazyFields(object.getClass()); for(Field field : fields) { Object fieldValue = field.get(object); if (fieldValue != null && associationsToInclude.contains(field.getName())) { JsonElement fieldValueJsonElement = gson.toJsonTree(fieldValue); augmentJsonElement(fieldValue, fieldValueJsonElement, subAssociationsToInclude(associationsToInclude, field.getName())); jsonElement.getAsJsonObject().add(field.getName(), fieldValueJsonElement); } else if (fieldValue != null) { if (fieldValue instanceof Collection) { JsonArray primaryKeyArray = new JsonArray(); for(Object subElement : (Collection)fieldValue) { primaryKeyArray.add(new JsonPrimitive(reflectionHelper.getPrimaryKey(subElement))); } jsonElement.getAsJsonObject().add(field.getName() + "_cryson_ids", primaryKeyArray); } else { jsonElement.getAsJsonObject().addProperty(field.getName() + "_cryson_id", reflectionHelper.getPrimaryKey(fieldValue)); } } else { jsonElement.getAsJsonObject().add(field.getName() + "_cryson_id", JsonNull.INSTANCE); } } } } catch(Throwable t) { throw new RuntimeException(t); } } private void transformJsonObjectToUnauthorizedEntity(JsonObject jsonObject) { for(Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) { if (!allowedUnauthorizedAttributeNames.contains(entry.getKey())) { jsonObject.remove(entry.getKey()); } } jsonObject.addProperty("crysonUnauthorized", true); } private Set<String> subAssociationsToInclude(Set<String> associationsToInclude, String association) { if (associationsToInclude.size() == 0) { return associationsToInclude; } Set<String> subAssociations = new HashSet<String>(); String subAssociationPrefix = association + "."; for(String associationToInclude : associationsToInclude) { if (associationToInclude.startsWith(subAssociationPrefix)) { subAssociations.add(associationToInclude.replaceFirst(Matcher.quoteReplacement(subAssociationPrefix), "")); } } return subAssociations; } public String getEntityClassName(Object entity) { if (entity instanceof HibernateProxy) { return ((HibernateProxy)entity).getHibernateLazyInitializer().getPersistentClass().getName(); } else { return entity.getClass().getName(); } } private <T> T augmentEntity(T object, JsonElement jsonElement, Map<Long, Long> replacedTemporaryIds) { try { Set<Map.Entry<String,JsonElement>> attributes = jsonElement.getAsJsonObject().entrySet(); for(Map.Entry<String,JsonElement> attribute : attributes) { if (attribute.getKey().endsWith("_cryson_id")) { String attributeName = attribute.getKey().split("_cryson_id")[0]; Field field = reflectionHelper.getField(object, attributeName); if (field != null) { if (attribute.getValue() != JsonNull.INSTANCE) { Object placeHolderObject = field.getType().newInstance(); reflectionHelper.setPrimaryKey(placeHolderObject, primaryKeyForReplacementObject(attribute.getValue().getAsLong(), replacedTemporaryIds)); field.set(object, placeHolderObject); } } } else if (attribute.getKey().endsWith("_cryson_ids")) { String attributeName = attribute.getKey().split("_cryson_ids")[0]; Field field = reflectionHelper.getField(object, attributeName); if (field != null) { Iterator<JsonElement> attributeIterator = attribute.getValue().getAsJsonArray().iterator(); Collection<Object> placeHolderObjects = emptyCollectionForField(field); while(attributeIterator.hasNext()) { JsonElement attributeValue = attributeIterator.next(); Object placeHolderObject = ((Class)((ParameterizedType)(field.getGenericType())).getActualTypeArguments()[0]).newInstance(); reflectionHelper.setPrimaryKey(placeHolderObject, primaryKeyForReplacementObject(attributeValue.getAsLong(), replacedTemporaryIds)); placeHolderObjects.add(placeHolderObject); } field.set(object, placeHolderObjects); } } else if (attribute.getKey().endsWith("_cryson_usertype")) { String attributeName = attribute.getKey().split("_cryson_usertype")[0]; Field field = reflectionHelper.getField(object, attributeName); if (field != null && Map.class.isAssignableFrom(field.getType())) { Map<Object, Object> placeHolderObjects = new HashMap<Object, Object>(); for (Map.Entry<String, JsonElement> valueEntry : attribute.getValue().getAsJsonObject().entrySet()) { String placeHolderKeyObject = valueEntry.getKey(); Object placeHolderValueObject = gson.fromJson(valueEntry.getValue(), Object.class); // Support string->string maps only placeHolderObjects.put(placeHolderKeyObject, placeHolderValueObject); } field.set(object, placeHolderObjects); } } } return object; } catch(Throwable t) { throw new RuntimeException(t); } } private Long primaryKeyForReplacementObject(Long candidatePrimaryKey, Map<Long, Long> replacedTemporaryIds) { if (replacedTemporaryIds != null) { Long replacedTemporaryId = replacedTemporaryIds.get(candidatePrimaryKey); if (replacedTemporaryId != null) { return replacedTemporaryId; } } return candidatePrimaryKey; } private Collection<Object> emptyCollectionForField(Field field) { if (field.getType() == Set.class) { return new HashSet<Object>(); } else { return new ArrayList<Object>(); } } private Set<Field> getLazyFields(Class<?> klazz) { Set<Field> annotatedFields = lazyFieldsCache.get(klazz); if (annotatedFields == null) { annotatedFields = new HashSet<Field>(); @SuppressWarnings("rawtypes") Class currentKlazz = klazz; while(currentKlazz != null) { Set<Field> declaredFields = reflectionHelper.getAllDeclaredFields(currentKlazz); for(Field declaredField : declaredFields) { if (reflectionHelper.isLazyField(declaredField)) { declaredField.setAccessible(true); annotatedFields.add(declaredField); } } currentKlazz = currentKlazz.getSuperclass(); } lazyFieldsCache.put(klazz, annotatedFields); } return annotatedFields; } // Todo: refactor this copy-pasted method private Set<Field> getUserTypeFields(Class<?> klazz) { Set<Field> annotatedFields = userTypeFieldsCache.get(klazz); if (annotatedFields == null) { annotatedFields = new HashSet<Field>(); @SuppressWarnings("rawtypes") Class currentKlazz = klazz; while(currentKlazz != null) { Set<Field> declaredFields = reflectionHelper.getAllDeclaredFields(currentKlazz); for(Field declaredField : declaredFields) { if (declaredField.isAnnotationPresent(org.hibernate.annotations.Type.class)) { declaredField.setAccessible(true); annotatedFields.add(declaredField); } } currentKlazz = currentKlazz.getSuperclass(); } userTypeFieldsCache.put(klazz, annotatedFields); } return annotatedFields; } public JsonElement parse(String json) { return jsonParser.parse(json); } void setReflectionHelper(ReflectionHelper reflectionHelper) { this.reflectionHelper = reflectionHelper; } void setLazyAssociationExclusionStrategy(LazyAssociationExclusionStrategy lazyAssociationExclusionStrategy) { this.lazyAssociationExclusionStrategy = lazyAssociationExclusionStrategy; } void setUserTypeExclusionStrategy(UserTypeExclusionStrategy userTypeExclusionStrategy) { this.userTypeExclusionStrategy = userTypeExclusionStrategy; } public void setCrysonExcludeExclusionStrategy(CrysonExcludeExclusionStrategy crysonExcludeExclusionStrategy) { this.crysonExcludeExclusionStrategy = crysonExcludeExclusionStrategy; } }