/* Copyright (c) 2009 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.wave.api.event; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.wave.api.Wavelet; import com.google.wave.api.JsonRpcConstant.ParamsProperty; import com.google.wave.api.impl.EventMessageBundle; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; /** * Object that is responsible for serializing and deserializing implementors * of {@link Event} to and from {@link JsonObject}. */ public class EventSerializer { /** * An interface for map key conversion. * * @param <T> the generic type of the resulting key. */ private static interface KeyConverter<T> { /** * Converts the given string key into an instance of {@code T}. * * @param key the key to be converted. * @return an instance of {@code T} that represents the given string key. */ T convert(String key); } /** Constants for accessing the properties of the given JSON object. */ private static final String TYPE = "type"; private static final String MODIFIED_BY = "modifiedBy"; private static final String TIMESTAMP = "timestamp"; private static final String BLIP_ID = "blipId"; private static final String WAVELET = "wavelet"; private static final String PROPERTIES = "properties"; private static final String BUNDLE = "bundle"; /** * Serializes the given {@link Event} into a {@link JsonObject}. * * @param event the {@link Event} to be serialized. * @param context the serialization context. * @return an instance of {@link JsonObject}, that is the JSON representation * of the given {@link Event}. */ public static JsonObject serialize(Event event, JsonSerializationContext context) throws EventSerializationException { JsonObject result = new JsonObject(); // Serialize basic properties from Event. result.addProperty(TYPE, event.getType().name()); result.addProperty(MODIFIED_BY, event.getModifiedBy()); result.addProperty(TIMESTAMP, event.getTimestamp()); result.addProperty(TIMESTAMP, event.getTimestamp()); // Construct a properties object. JsonObject properties = new JsonObject(); try { // Serialize the blip id. Field blipIdField = AbstractEvent.class.getDeclaredField(BLIP_ID); blipIdField.setAccessible(true); properties.addProperty(BLIP_ID, (String) blipIdField.get(event)); // Serialize event specific properties. for (Field field : event.getClass().getDeclaredFields()) { field.setAccessible(true); properties.add(field.getName(), context.serialize(field.get(event))); } } catch (IllegalArgumentException e) { throw new EventSerializationException("Unable to serialize event: " + BLIP_ID + " in " + event.getClass() + " is not accessible."); } catch (IllegalAccessException e) { throw new EventSerializationException("Unable to serialize event: " + BLIP_ID + " in " + event.getClass() + " is not accessible."); } catch (NoSuchFieldException e) { throw new EventSerializationException("Unable to serialize event: " + BLIP_ID + " in " + event.getClass() + " is not accessible."); } result.add(PROPERTIES, properties); return result; } /** * Deserializes the given {@link JsonObject} into an {@link Event}, and * assign the given {@link Wavelet} to the {@link Event}. * * @param wavelet the wavelet where the event occurred. * @param json the JSON representation of {@link Event}. * @param context the deserialization context. * @return an instance of {@link Event}. * * @throw {@link EventSerializationException} if there is a problem * deserializing the event JSON. */ public static Event deserialize(Wavelet wavelet, EventMessageBundle bundle, JsonObject json, JsonDeserializationContext context) throws EventSerializationException { // Construct the event object. String eventTypeString = json.get(TYPE).getAsString(); EventType type = EventType.valueOfIgnoreCase(eventTypeString); if (type == EventType.UNKNOWN) { throw new EventSerializationException("Trying to deserialize event JSON with unknown " + "type: " + json, json); } // Parse the generic parameters. String modifiedBy = json.get(MODIFIED_BY).getAsString(); Long timestamp = json.get(TIMESTAMP).getAsLong(); // Construct the event object. Class<? extends Event> clazz = type.getClazz(); Constructor<? extends Event> ctor; try { ctor = clazz.getDeclaredConstructor(); ctor.setAccessible(true); Event event = ctor.newInstance(); // Set the default fields from AbstractEvent. Class<?> rootClass = AbstractEvent.class; setField(event, rootClass.getDeclaredField(WAVELET), wavelet); setField(event, rootClass.getDeclaredField(MODIFIED_BY), modifiedBy); setField(event, rootClass.getDeclaredField(TIMESTAMP), timestamp); setField(event, rootClass.getDeclaredField(TYPE), type); setField(event, rootClass.getDeclaredField(BUNDLE), bundle); JsonObject properties = json.get(PROPERTIES).getAsJsonObject(); // Set the blip id field, that can be null for certain events, such as // OPERATION_ERROR. JsonElement blipId = properties.get(BLIP_ID); if (blipId != null && !(blipId instanceof JsonNull)) { setField(event, rootClass.getDeclaredField(BLIP_ID), blipId.getAsString()); } // Set the additional fields. for (Field field : clazz.getDeclaredFields()) { String fieldName = field.getName(); if (properties.has(fieldName)) { setField(event, field, context.deserialize(properties.get(fieldName), field.getGenericType())); } } return event; } catch (NoSuchMethodException e) { throw new EventSerializationException("Unable to deserialize event JSON: " + json, json); } catch (NoSuchFieldException e) { throw new EventSerializationException("Unable to deserialize event JSON: " + json, json); } catch (InstantiationException e) { throw new EventSerializationException("Unable to deserialize event JSON: " + json, json); } catch (IllegalAccessException e) { throw new EventSerializationException("Unable to deserialize event JSON: " + json, json); } catch (InvocationTargetException e) { throw new EventSerializationException("Unable to deserialize event JSON: " + json, json); } catch (JsonParseException e) { throw new EventSerializationException("Unable to deserialize event JSON: " + json, json); } } /** * Extracts event specific properties into a map. This method will not include * the basic properties from {@link AbstractEvent}, except for blip id, in the * resulting map. * * @param event the event whose properties will be extracted. * @return a map of {@link ParamsProperty} to {@link Object} of properties. * * @throws EventSerializationException if there is a problem accessing the * event's fields. */ public static Map<ParamsProperty, Object> extractPropertiesToParamsPropertyMap(Event event) throws EventSerializationException { return extractProperties(event, new KeyConverter<ParamsProperty>() { @Override public ParamsProperty convert(String key) { return ParamsProperty.fromKey(key); } }); } /** * Extracts event specific properties into a map. This method will not include * the basic properties from {@link AbstractEvent}, except for blip id, in the * resulting map. * * @param event the event whose properties will be extracted. * @return a map of {@link String} to {@link Object} of properties. * * @throws EventSerializationException if there is a problem accessing the * event's fields. */ public static Map<String, Object> extractPropertiesToStringMap(Event event) throws EventSerializationException { return extractProperties(event, new KeyConverter<String>() { @Override public String convert(String key) { return key; } }); } /** * Extracts event specific properties into a map. This method will not include * the basic properties from {@link AbstractEvent}, except for blip id, in the * resulting map. * * @param event the event whose properties will be extracted. * @param keyConverter the converter to convert the event property name into * a proper key object for the resulting map. * @return a map of {@code T} to {@link Object} of properties. * * @throws EventSerializationException if there is a problem accessing the * event's fields. */ private static <T> Map<T, Object> extractProperties(Event event, KeyConverter<T> keyConverter) throws EventSerializationException { Field[] fields = event.getClass().getDeclaredFields(); Map<T, Object> data = new HashMap<T, Object>(fields.length + 1); try { for (Field field : fields) { field.setAccessible(true); data.put(keyConverter.convert(field.getName()), field.get(event)); } Field field = AbstractEvent.class.getDeclaredField(BLIP_ID); field.setAccessible(true); data.put(keyConverter.convert(BLIP_ID), field.get(event)); } catch (IllegalArgumentException e) { throw new EventSerializationException(e.getMessage()); } catch (IllegalAccessException e) { throw new EventSerializationException(e.getMessage()); } catch (NoSuchFieldException e) { throw new EventSerializationException(e.getMessage()); } return data; } /** * Sets the field of the given {@link Event} object. * * @param event the {@link Event} object whose field will be set. * @param field the {@link Field} object that represents the field. * @param value the value to be set to the field. * * @throws IllegalAccessException if the field is not accessible. */ private static void setField(Event event, Field field, Object value) throws IllegalAccessException { field.setAccessible(true); field.set(event, value); } }