/* * Copyright: (c) 2004-2012 Mayo Foundation for Medical Education and * Research (MFMER). All rights reserved. MAYO, MAYO CLINIC, and the * triple-shield Mayo logo are trademarks and service marks of MFMER. * * Except as contained in the copyright notice above, or as used to identify * MFMER as the author of this software, the trade names, trademarks, service * marks, or product names of the copyright holder shall not be used in * advertising, promotion or otherwise in connection with this software without * prior written authorization of the copyright holder. * * 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 edu.mayo.cts2.framework.core.json; import com.google.gson.*; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import edu.mayo.cts2.framework.model.Cts2ModelObject; import edu.mayo.cts2.framework.model.core.Changeable; import edu.mayo.cts2.framework.model.core.ChangeableElementGroup; import edu.mayo.cts2.framework.model.core.TsAnyType; import edu.mayo.cts2.framework.model.entity.EntityDescription; import edu.mayo.cts2.framework.model.updates.ChangeableResource; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import org.reflections.Reflections; import org.reflections.util.ClasspathHelper; import org.reflections.util.ConfigurationBuilder; import org.reflections.util.FilterBuilder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.*; import java.util.Map.Entry; /** * Responsible for converting CTS2 Model output into JSON. * * @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a> */ @Component public class JsonConverter { private static final String MODEL_PACKAGE = "edu.mayo.cts2.framework.model"; private static final String WSDL_PACKAGE = "edu.mayo.cts2.framework.model.wsdl.*"; private static final String LIST_SUFFIX = "List"; private static final String CHOICE_VALUE = "_choiceValue"; private static final String ISO_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm'Z'"; private Gson gson; private JsonParser jsonParser = new JsonParser(); private Map<String, Class<? extends Cts2ModelObject>> classNameCache; /** * Instantiates a new json converter. */ public JsonConverter() { super(); this.gson = this.buildGson(); this.classNameCache = this.cacheClasses(); } /** * Cache classes. * * @return the map< string, class<? extends cts2 model object>> */ protected Map<String, Class<? extends Cts2ModelObject>> cacheClasses() { Map<String, Class<? extends Cts2ModelObject>> cache = new HashMap<String, Class<? extends Cts2ModelObject>>(); Reflections reflections = new Reflections(new ConfigurationBuilder() .filterInputsBy( new FilterBuilder().include( "edu.mayo.cts2.framework.model.*").exclude( WSDL_PACKAGE)).setUrls( ClasspathHelper.forPackage(MODEL_PACKAGE))); Set<Class<? extends Cts2ModelObject>> types = reflections .getSubTypesOf(Cts2ModelObject.class); for (Class<? extends Cts2ModelObject> type : types) { String name = type.getSimpleName(); cache.put(name, type); } return cache; } /** * Convert a CTS2 Model Object to JSON. * * @param cts2Object the cts2 object * @return the string */ public String toJson(Object cts2Object) { JsonElement element = this.gson.toJsonTree(cts2Object); JsonObject object = new JsonObject(); object.add(cts2Object.getClass().getSimpleName(), element); return object.toString(); } /** * Convert JSON to a CTS2 Model Object. * * @param <T> the generic type * @param json the json * @param clazz the clazz * @return the t */ public <T> T fromJson(String json, Class<T> clazz) { JsonElement element = this.jsonParser.parse(json); Set<Entry<String, JsonElement>> entrySet = element.getAsJsonObject() .entrySet(); Assert.isTrue(entrySet.size() == 1); T obj = gson.fromJson(entrySet.iterator().next().getValue(), clazz); return obj; } /** * Convert JSON to a CTS2 Model Object. * * @param json the json * @return the object */ public Object fromJson(String json) { Class<?> clazz = this.getJsonClass(json); return this.fromJson(json, clazz); } /** * Gets the json class. * * @param json the json * @return the json class */ protected Class<?> getJsonClass(String json) { JsonElement element = this.jsonParser.parse(json); Set<Entry<String, JsonElement>> entrySet = element.getAsJsonObject() .entrySet(); if(entrySet.size() != 1){ throw new JsonUnmarshallingException("Could not determine the type of the JSON String: " + json); } return this.classNameCache.get(entrySet.iterator().next().getKey()); } /** * Sets the choice value. * * @param obj the new choice value */ protected void setChoiceValue(Object obj) { try { for (Field f : obj.getClass().getDeclaredFields()) { f.setAccessible(true); Object fieldValue = f.get(obj); if (fieldValue != null && !ClassUtils.isPrimitiveOrWrapper(fieldValue.getClass())) { Field choiceValue = obj.getClass().getDeclaredField(CHOICE_VALUE); choiceValue.setAccessible(true); choiceValue.set(obj, fieldValue); break; } } } catch (Exception e) { throw new IllegalStateException(e); } } /** * Builds the gson. * * @return the gson */ protected Gson buildGson(){ GsonBuilder gson = new GsonBuilder(); gson.setDateFormat(ISO_DATE_FORMAT); gson.setExclusionStrategies(new ExclusionStrategy(){ @Override public boolean shouldSkipField(FieldAttributes f) { return f.getName().equals(CHOICE_VALUE); } @Override public boolean shouldSkipClass(Class<?> clazz) { return false; } }); gson.registerTypeAdapter(List.class, new EmptyCollectionSerializer()); gson.registerTypeAdapter(TsAnyType.class, new TsAnyTypeSerializer()); gson.registerTypeAdapter(Date.class, new DateTypeAdapter()); gson.registerTypeAdapterFactory(new ChangeableTypeAdapterFactory()); gson.registerTypeAdapterFactory(new ChangeableResourceTypeAdapterFactory()); gson.setFieldNamingStrategy(new FieldNamingStrategy(){ @Override public String translateName(Field field) { String fieldName = field.getName(); char[] array = fieldName.toCharArray(); if(array[0] == '_'){ array = ArrayUtils.remove(array, 0); } String name = new String(array); if(name.endsWith(LIST_SUFFIX)){ name = StringUtils.removeEnd(name, LIST_SUFFIX); } return name; } }); return gson.create(); } /** * The Class EmptyCollectionSerializer. * * @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a> */ public static class EmptyCollectionSerializer implements JsonSerializer<List<?>> { /* (non-Javadoc) * @see com.google.gson.JsonSerializer#serialize(java.lang.Object, java.lang.reflect.Type, com.google.gson.JsonSerializationContext) */ @Override public JsonElement serialize(List<?> collection, Type typeOfSrc, JsonSerializationContext context) { if(CollectionUtils.isNotEmpty(collection)){ return context.serialize(collection); } else { return null; } } } /** * The Class TsAnyTypeSerializer. * * @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a> */ public static class TsAnyTypeSerializer implements JsonSerializer<TsAnyType>, JsonDeserializer<TsAnyType> { /* (non-Javadoc) * @see com.google.gson.JsonSerializer#serialize(java.lang.Object, java.lang.reflect.Type, com.google.gson.JsonSerializationContext) */ @Override public JsonElement serialize(TsAnyType tsAnyType, Type typeOfSrc, JsonSerializationContext context) { if(tsAnyType == null || tsAnyType.getContent() == null){ return null; } return new JsonPrimitive(tsAnyType.getContent()); } /* (non-Javadoc) * @see com.google.gson.JsonDeserializer#deserialize(com.google.gson.JsonElement, java.lang.reflect.Type, com.google.gson.JsonDeserializationContext) */ @Override public TsAnyType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if(json == null){ return null; } if(! json.isJsonPrimitive()){ throw new IllegalStateException("TsAnytype is not a JSON Primitive."); } if(json.getAsJsonPrimitive().getAsString() == null){ return null; } else { TsAnyType tsAnyType = new TsAnyType(); tsAnyType.setContent(json.getAsJsonPrimitive().getAsString()); return tsAnyType; } } } private class ChangeableTypeAdapterFactory implements TypeAdapterFactory { public TypeAdapter create(final Gson gson, TypeToken type) { final TypeAdapter<Object> delegate = gson.getDelegateAdapter(this, type); return new TypeAdapter<Object>() { public void write(JsonWriter out, Object value) throws IOException { if(value instanceof Changeable){ Changeable changeable = (Changeable)value; ChangeableElementGroup changeableElementGroup = changeable.getChangeableElementGroup(); JsonElement changeableJson; if(changeableElementGroup != null){ changeable.setChangeableElementGroup(null); changeableJson = delegate.toJsonTree(changeable); JsonObject changeableElementGroupJson = gson.toJsonTree(changeableElementGroup).getAsJsonObject(); for(Entry<String, JsonElement> entry : changeableElementGroupJson.entrySet()){ changeableJson.getAsJsonObject().add(entry.getKey(),entry.getValue()); } } else { changeableJson = delegate.toJsonTree(changeable); } gson.toJson(changeableJson, out); } else { delegate.write(out, value); } } public Object read(JsonReader in) throws IOException { JsonParser jsonParser = new JsonParser(); JsonElement jsonElement = jsonParser.parse(in); Object obj = delegate.fromJsonTree(jsonElement); if(obj instanceof Changeable){ ChangeableElementGroup changeableElementGroup = gson.fromJson(jsonElement, ChangeableElementGroup.class); ((Changeable) obj).setChangeableElementGroup(changeableElementGroup); } return obj; } }; } } private class ChangeableResourceTypeAdapterFactory implements TypeAdapterFactory { public TypeAdapter create(Gson gson, TypeToken type) { final TypeAdapter<Object> delegate = gson.getDelegateAdapter(this, type); return new TypeAdapter<Object>() { public void write(JsonWriter out, Object value) throws IOException { delegate.write(out, value); } public Object read(JsonReader in) throws IOException { Object obj = delegate.read(in); if (obj instanceof EntityDescription || obj instanceof ChangeableResource) { setChoiceValue(obj); } return obj; } }; } } private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> { private DateTimeFormatter formatter = ISODateTimeFormat.dateTime(); @Override public synchronized JsonElement serialize(Date date, Type type, JsonSerializationContext jsonSerializationContext) { return new JsonPrimitive(formatter.print(date.getTime())); } @Override public synchronized Date deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) { return formatter.parseDateTime(jsonElement.getAsString()).toDate(); } } }