/* * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates. * * 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 org.jboss.errai.enterprise.client.jaxrs; import static org.jboss.errai.common.client.protocols.SerializationParts.ENCODED_TYPE; import static org.jboss.errai.common.client.protocols.SerializationParts.ENUM_STRING_VALUE; import static org.jboss.errai.common.client.protocols.SerializationParts.NUMERIC_VALUE; import static org.jboss.errai.common.client.protocols.SerializationParts.OBJECT_ID; import static org.jboss.errai.common.client.protocols.SerializationParts.QUALIFIED_VALUE; import java.util.HashMap; import java.util.Map; import org.jboss.errai.common.client.protocols.SerializationParts; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONNumber; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONString; import com.google.gwt.json.client.JSONValue; /** * Utility to transform Errai's JSON to a Jackson compatible JSON and vice versa. * <p> * Limitations: * <ul> * <li>Fields with nested parameterized types are not supported (e.g. List<List<String>>)</li> * </ul> * * @author Christian Sadilek <csadilek@redhat.com> */ public class JacksonTransformer { private JacksonTransformer() {}; /** * Transforms Errai JSON into a Jackson compatible JSON. * * @param erraiJson * JSON generated by Errai * @return jackson compatible JSON */ public static String toJackson(String erraiJson) { JSONValue val = JSONParser.parseStrict(erraiJson); val = toJackson(val, null, null, new HashMap<String, JSONValue>()); return val.toString(); } /** * The transformation from Errai JSON to Jackson's JSON contains the following steps: * <ul> * <li>For all JSON objects, recursively remove the Errai specific OBJECT_ID and ENCODED_TYPE * values</li> * <li>Keep a reference to the removed OBJECT_IDs, so back-references can be resolved</li> * <li>If an array is encountered, process all its elements, then remove the Errai specific * QUALIFIED_VALUE key, by associating its actual value with the object's key directly: "list": * {"^Value": ["e1","e2"]} becomes "list": ["e1","e2"]</li> * <li>If an enum is encountered, remove the Errai specific ENUM_STRING_VALUE key, by associating * its actual value with the object's key directly: "gender": {"^EnumStringValue": "MALE"} becomes * "gender": "MALE"</li> * <li>If a number is encountered, remove the Errai specific NUMERIC_VALUE key, by associating its * actual value with the object's key directly: "id": {"^NumValue": "1"} becomes "id": "1"</li> * <li>If a date is encountered, remove the Errai specific QUALIFIED_VALUE key, by associating its * actual value with the object's key directly and turning it into a JSON number</li> * <li>If EMBEDDED_JSON is encountered, turn in into standard json</li> * </ul> * * @param val * the JSON value to transform * @param key * the key of the JSON value to transform * @param parent * the parent object of the current value * @param objectCache * a cache for removed objects, that is used to resolve backreferences * @return the modified JSON value */ private static JSONValue toJackson(JSONValue val, String key, JSONObject parent, Map<String, JSONValue> objectCache) { JSONObject obj; if ((obj = val.isObject()) != null) { JSONValue encType = obj.get(ENCODED_TYPE); JSONValue objectIdVal = obj.get(OBJECT_ID); if (objectIdVal != null && encType != null) { String objectId = objectIdVal.isString().stringValue(); String cacheKey = objectId + "/" + encType.isString().stringValue(); if (!objectId.equals("-1")) { JSONValue backRef = objectCache.get(cacheKey); if (backRef != null) { if (parent != null) { obj = backRef.isObject(); parent.put(key, obj); } else { return backRef.isObject(); } } else { objectCache.put(cacheKey, obj); } } } obj.put(OBJECT_ID, null); obj.put(ENCODED_TYPE, null); for (String k : obj.keySet()) { JSONArray arr; if ((arr = obj.get(k).isArray()) != null) { for (int i = 0; i < arr.size(); i++) { if (arr.get(i).isObject() != null && arr.get(i).isObject().get(NUMERIC_VALUE) != null) { arr.set(i, arr.get(i).isObject().get(NUMERIC_VALUE)); } else if (arr.get(i).isObject() != null) { arr.set(i, toJackson(arr.get(i), null, null, objectCache)); } } if (k.equals(QUALIFIED_VALUE)) { if (parent != null) { parent.put(key, arr); } else { return arr; } } } else if (k.equals(ENUM_STRING_VALUE) || k.equals(NUMERIC_VALUE)) { if (parent != null) { parent.put(key, obj.get(k)); } return toJackson(obj.get(k), k, obj, objectCache); } else if (k.equals(QUALIFIED_VALUE)) { if (parent != null) { if (encType.isString().stringValue().equals("java.util.Date")) { String dateValue = obj.get(k).isString().stringValue(); parent.put(key, new JSONNumber(Double.parseDouble(dateValue))); } else { parent.put(key, obj.get(k)); } } } else if (k.startsWith(SerializationParts.EMBEDDED_JSON)) { final JSONValue newKey = JSONParser.parseStrict((k.substring(SerializationParts.EMBEDDED_JSON.length()))); JSONValue value = obj.get(k); JSONObject tmpObject = new JSONObject(); toJackson(newKey, QUALIFIED_VALUE, tmpObject, objectCache); String embeddedKey = null; JSONValue qualVal = tmpObject.get(QUALIFIED_VALUE); if (qualVal.isString() != null) { embeddedKey = qualVal.isString().stringValue(); } else { embeddedKey = qualVal.toString(); } obj.put(embeddedKey, value); } toJackson(obj.get(k), k, obj, objectCache); } } return (obj != null) ? cleanUpEmbeddedJson(obj) : val; } private static JSONObject cleanUpEmbeddedJson(JSONObject obj) { if (obj != null) { for (String k : obj.keySet()) { if (k.startsWith(SerializationParts.EMBEDDED_JSON)) { obj.put(k, null); } } } return obj; } /** * Transforms Jackson generated JSON into Errai compatible JSON. * * @param jackson * JSON generated by Jackson * @return Errai compatible JSON */ public static String fromJackson(String jackson) { JSONValue val = JSONParser.parseStrict(jackson); val = fromJackson(val, null, null, new int[1]); return val.toString(); } /** * The transformation from Jackson's JSON to Errai JSON contains the following steps: * <ul> * <li>Recursively add an incremented OBJECT_ID to every JSON object</li> * <li>If a number is encountered, wrap it in a new JSON object with an OBJECT_ID and * NUMERIC_VALUE property</li> * <li>If an array is encountered, wrap it in a new JSON object with an OBJECT_ID and * QUALIFIED_VALUE property</li> * </ul> * * @param val * the JSON value to transform * @param key * the key of the JSON value to transform * @param parent * the parent object of the current value * @param objectId * last used object id * @return modified JSON value */ private static JSONValue fromJackson(JSONValue val, String key, JSONObject parent, int[] objectId) { JSONObject obj; JSONNumber num; JSONArray arr; if ((obj = val.isObject()) != null) { obj.put(OBJECT_ID, new JSONString(new Integer(++objectId[0]).toString())); for (String k : obj.keySet()) { fromJackson(obj.get(k), k, obj, objectId); } } else if ((num = val.isNumber()) != null) { JSONObject numObject = new JSONObject(); numObject.put(OBJECT_ID, new JSONString(new Integer(++objectId[0]).toString())); numObject.put(NUMERIC_VALUE, num); if (parent != null) { parent.put(key, numObject); } else { val = numObject; } } else if ((arr = val.isArray()) != null) { JSONObject arrayObject = new JSONObject(); arrayObject.put(OBJECT_ID, new JSONString(new Integer(++objectId[0]).toString())); arrayObject.put(QUALIFIED_VALUE, arr); if (parent != null) { parent.put(key, arrayObject); } else { val = arrayObject; } for (int i = 0; i < arr.size(); i++) { arr.set(i, fromJackson(arr.get(i), QUALIFIED_VALUE, null, objectId)); } } return val; } }