/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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;
import static com.google.wave.api.OperationType.ROBOT_NOTIFY;
import static com.google.wave.api.OperationType.ROBOT_NOTIFY_CAPABILITIES_HASH;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.wave.api.JsonRpcConstant.ParamsProperty;
import com.google.wave.api.JsonRpcConstant.RequestProperty;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
/**
* Utility class to serialize and deserialize Events and Operations to and from
* JSON string for V2.* of the protocol.
*
* @author mprasetya@google.com (Marcel Prasetya)
* @author ljvderijk@google.com (Lennard de Rijk)
*/
public class RobotSerializer {
/** The counter for protocol versions. */
public static final Map<ProtocolVersion, AtomicInteger> PROTOCOL_VERSION_COUNTERS;
static {
PROTOCOL_VERSION_COUNTERS = new HashMap<ProtocolVersion, AtomicInteger>();
// Put in the V2 protocols that are used in this serializer
for (ProtocolVersion protcolVersion : ProtocolVersion.values()) {
if (protcolVersion.isGreaterThanOrEqual(ProtocolVersion.V2)) {
PROTOCOL_VERSION_COUNTERS.put(protcolVersion, new AtomicInteger());
}
}
}
private static final Logger LOG = Logger.getLogger(RobotSerializer.class.getName());
/** An map of {@link Gson}s for serializing and deserializing JSON. */
private final NavigableMap<ProtocolVersion, Gson> gsons;
/** The default protocol version. */
private final ProtocolVersion defaultProtocolVersion;
/**
* An instance of {@link JsonParser} to parse JSON string into
* {@link JsonElement}.
*/
private final JsonParser jsonParser;
/**
* Constructor. Note that the defaultprotocol version must occur in the map
* of {@link Gson}s.
*
* @param gsons an map of {@link Gson}s for serializing and deserializing
* JSON, keyed by protocol version.
* @param defaultProtocolVersion the default protocol version.
*/
public RobotSerializer(NavigableMap<ProtocolVersion, Gson> gsons,
ProtocolVersion defaultProtocolVersion) {
if (!gsons.containsKey(defaultProtocolVersion)) {
throw new IllegalArgumentException(
"The serializer map does not contain a serializer for the default protocol version");
}
this.gsons = gsons;
this.defaultProtocolVersion = defaultProtocolVersion;
this.jsonParser = new JsonParser();
}
/**
* Deserializes the given JSON string into an instance of the given type.
*
* @param <T> the generic type of the given class.
* @param jsonString the JSON string to deserialize.
* @param type the type to deserialize the JSON string into.
* @param protocolVersion the wire protocol version of the given JSON string.
* @return an instance of {@code type}, that is constructed by deserializing
* the given {@code jsonString}
*/
public <T> T deserialize(String jsonString, Type type, ProtocolVersion protocolVersion) {
return getGson(protocolVersion).<T>fromJson(jsonString, type);
}
/**
* Serializes the given object into a JSON string.
*
* @param <T> the generic type of the given object.
* @param object the object to serialize.
* @return a JSON string representation of {@code object}.
*/
public <T> String serialize(T object) {
return serialize(object, defaultProtocolVersion);
}
/**
* Serializes the given object into a JSON string.
*
* @param <T> the generic type of the given object.
* @param object the object to serialize.
* @param type the specific genericized type of {@code object}.
* @return a JSON string representation of {@code object}.
*/
public <T> String serialize(T object, Type type) {
return serialize(object, type, defaultProtocolVersion);
}
/**
* Serializes the given object into a JSON string.
*
* @param <T> the generic type of the given object.
* @param object the object to serialize.
* @param protocolVersion the version of the serializer to use.
* @return a JSON string representation of {@code object}.
*/
public <T> String serialize(T object, ProtocolVersion protocolVersion) {
return getGson(protocolVersion).toJson(object);
}
/**
* Serializes the given object into a JSON string.
*
* @param <T> the generic type of the given object.
* @param object the object to serialize.
* @param type the specific genericized type of {@code object}.
* @param protocolVersion the version of the serializer to use.
* @return a JSON string representation of {@code object}.
*/
public <T> String serialize(T object, Type type, ProtocolVersion protocolVersion) {
return getGson(protocolVersion).toJson(object, type);
}
/**
* Parses the given JSON string into a {@link JsonElement}.
*
* @param jsonString the string to parse.
* @return a {@link JsonElement} representation of the input
* {@code jsonString}.
*/
public JsonElement parse(String jsonString) {
return jsonParser.parse(jsonString);
}
/**
* Deserializes operations. This method supports only the new JSON-RPC style
* operations.
*
* @param jsonString the operations JSON string to deserialize.
* @return a list of {@link OperationRequest},that represents the operations.
* @throws InvalidRequestException if there is a problem deserializing the
* operations.
*/
public List<OperationRequest> deserializeOperations(String jsonString)
throws InvalidRequestException {
if (Util.isEmptyOrWhitespace(jsonString)) {
return Collections.emptyList();
}
// Parse incoming operations.
JsonArray requestsAsJsonArray = null;
JsonElement json = null;
try {
json = jsonParser.parse(jsonString);
} catch (JsonParseException e) {
throw new InvalidRequestException("Couldn't deserialize incoming operations: " +
jsonString, null, e);
}
if (json.isJsonArray()) {
requestsAsJsonArray = json.getAsJsonArray();
} else {
requestsAsJsonArray = new JsonArray();
requestsAsJsonArray.add(json);
}
// Convert incoming operations into a list of JsonRpcRequest.
ProtocolVersion protocolVersion = determineProtocolVersion(requestsAsJsonArray);
PROTOCOL_VERSION_COUNTERS.get(protocolVersion).incrementAndGet();
List<OperationRequest> requests = new ArrayList<OperationRequest>(requestsAsJsonArray.size());
for (JsonElement requestAsJsonElement : requestsAsJsonArray) {
validate(requestAsJsonElement);
requests.add(getGson(protocolVersion).fromJson(requestAsJsonElement,
OperationRequest.class));
}
return requests;
}
/**
* Determines the protocol version of a given operation bundle JSON by
* inspecting the first operation in the bundle. If it is a
* {@code robot.notify} operation, and contains {@code protocolVersion}
* parameter, then this method will return the value of that parameter.
* Otherwise, this method will return the default version.
*
* @param operationBundle the operation bundle to check.
* @return the wire protocol version of the given operation bundle.
*/
private ProtocolVersion determineProtocolVersion(JsonArray operationBundle) {
if (operationBundle.size() == 0 || !operationBundle.get(0).isJsonObject()) {
return defaultProtocolVersion;
}
JsonObject firstOperation = operationBundle.get(0).getAsJsonObject();
if (!firstOperation.has(RequestProperty.METHOD.key())) {
return defaultProtocolVersion;
}
String method = firstOperation.get(RequestProperty.METHOD.key()).getAsString();
if (isRobotNotifyOperationMethod(method)) {
JsonObject params = firstOperation.get(RequestProperty.PARAMS.key()).getAsJsonObject();
if (params.has(ParamsProperty.PROTOCOL_VERSION.key())) {
JsonElement protocolVersionElement = params.get(ParamsProperty.PROTOCOL_VERSION.key());
if (!protocolVersionElement.isJsonNull()) {
return ProtocolVersion.fromVersionString(protocolVersionElement.getAsString());
}
}
}
return defaultProtocolVersion;
}
/**
* Determines the protocol version of a given operation bundle by inspecting
* the first operation in the bundle. If it is a {@code robot.notify}
* operation, and contains {@code protocolVersion} parameter, then this method
* will return the value of that parameter. Otherwise, this method will return
* the default version.
*
* @param operationBundle the operation bundle to check.
* @return the wire protocol version of the given operation bundle.
*/
private ProtocolVersion determineProtocolVersion(List<OperationRequest> operationBundle) {
if (operationBundle.size() == 0) {
return defaultProtocolVersion;
}
OperationRequest firstOperation = operationBundle.get(0);
if (isRobotNotifyOperationMethod(firstOperation.getMethod())) {
String versionString = (String) firstOperation.getParameter(ParamsProperty.PROTOCOL_VERSION);
if (versionString != null) {
return ProtocolVersion.fromVersionString(versionString);
}
}
return defaultProtocolVersion;
}
/**
* Serializes a list of {@link OperationRequest} objects into a JSON string.
*
* @param operations List of operations to serialize.
* @return A JSON string representing the serialized operations.
*/
public String serializeOperations(List<OperationRequest> operations)
throws JsonParseException {
ProtocolVersion protocolVersion = determineProtocolVersion(operations);
return getGson(protocolVersion).toJson(operations);
}
/**
* Returns an instance of Gson for the given protocol version.
*
* @param protocolVersion the protocol version.
* @return an instance of {@link Gson}.
*/
private Gson getGson(ProtocolVersion protocolVersion) {
// Returns the last entry which protocol version is less than or equal to
// the given protocol version.
Entry<ProtocolVersion, Gson> entry = gsons.floorEntry(protocolVersion);
if (entry == null) {
LOG.severe("Could not find the proper Gson for protocol version " + protocolVersion);
return null;
}
return entry.getValue();
}
/**
* Validates that the incoming JSON is a JSON object that represents a
* JSON-RPC request.
*
* @param jsonElement the incoming JSON.
* @throws InvalidRequestException if the incoming JSON does not have the
* required properties.
*/
private static void validate(JsonElement jsonElement) throws InvalidRequestException {
if (!jsonElement.isJsonObject()) {
throw new InvalidRequestException("The incoming JSON is not a JSON object: " + jsonElement);
}
JsonObject jsonObject = jsonElement.getAsJsonObject();
StringBuilder missingProperties = new StringBuilder();
for (RequestProperty requestProperty : RequestProperty.values()) {
if (!jsonObject.has(requestProperty.key())) {
missingProperties.append(requestProperty.key());
}
}
if (missingProperties.length() > 0) {
throw new InvalidRequestException("Missing required properties " + missingProperties +
"operation: " + jsonObject);
}
}
/**
* Checks whether the given operation method is of a robot notify operation.
*
* @param method the method to check.
* @return {@code true} if the given method is a robot notify operation's
* method.
*/
@SuppressWarnings("deprecation")
private static boolean isRobotNotifyOperationMethod(String method) {
return ROBOT_NOTIFY_CAPABILITIES_HASH.method().equals(method) ||
ROBOT_NOTIFY.method().equals(method);
}
}