/*
* Copyright (c) 2009, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.dao.json;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import javax.annotation.Nonnull;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import ca.sqlpower.dao.MessageDecoder;
import ca.sqlpower.dao.SPPersistenceException;
import ca.sqlpower.dao.SPPersister;
import ca.sqlpower.dao.SPPersister.DataType;
import ca.sqlpower.dao.SPPersister.SPPersistMethod;
/**
* An implementation of {@link MessageDecoder} that takes in a String that is
* intended to be a JSON-formatted message, and constructs a JSONObject from it.
* It then expects JSONObject key values that map to {@link SPPersister}
* methods and their expected parameters. It then extracts this information from
* the JSONObject and makes the appropriate method calls to an
* {@link SPPersister} provided in the constructor.
*/
public class SPJSONMessageDecoder implements MessageDecoder<String> {
private static final Logger logger = Logger
.getLogger(SPJSONMessageDecoder.class);
/**
* A {@link SPPersister} that the decoder will make method calls on
*/
private SPPersister persister;
/**
* Creates an SPMessageDecoder with the given {@link SPPersister}. The
* messages that this class decodes will contain SPPersister method calls
* with their parameters. This decoder will use the messages to make method
* calls to the given SPPersister.
*
* @param persister
* The {@link SPPersister} that this decoder will make method
* calls to
*/
public SPJSONMessageDecoder(@Nonnull SPPersister persister) {
this.persister = persister;
}
/**
* Takes in a String, which is passed as a {@link JSONTokener}, where each
* token represents a single persister call.
*
* @see #decode(JSONTokener)
*/
public void decode(@Nonnull String message) throws SPPersistenceException {
decode(new JSONTokener(message));
}
/**
* Takes in a {@link JSONTokener} that contains persister calls. The tokener
* is used to parse each {@link JSONObject} token. Each JSONObject contains
* details for making a SPPersister method call. The parsing is done in this
* method in this way for performance reasons. Every time the next token is
* parsed, the corresponding persister call is made immediately after. Since
* the token is not used after parsing it, it will eventually be garbage
* collected.
*
* It expects the following key-value pairs in each JSONObject message:
* <ul>
* <li>method - The String value of a {@link SPPersistMethod}. This is used
* to determine which {@link SPPersister} method to call.</li>
* <li>uuid - The UUID of the SPObject, if there is one, that the persist
* method call will act on. If there is none, it expects
* {@link JSONObject#NULL}</li>
* </ul>
* Other possible key-value pairs (depending on the intended method call)
* include:
* <ul>
* <li>parentUUID</li>
* <li>type</li>
* <li>newValue</li>
* <li>oldValue</li>
* <li>propertyName</li>
* </ul>
* See the method documentation of {@link SPPersister} for full details on
* the expected values
* <p>
*/
public void decode(JSONTokener tokener) throws SPPersistenceException {
String uuid = null;
JSONObject jsonObject = null;
try {
synchronized (persister) {
// This code comes from JSONArray's constructor that takes in a
// JSONTokener. The reason why a JSONArray is not used is because
// we do not want to store all of the JSONObjects first before
// persisting the calls. Instead, we want to make the persist calls
// on the fly while parsing each token. By doing so, we allow
// garbage collection on each JSONObject immediately after the
// persist call is made.
char c = tokener.nextClean();
char q;
if (c == '[') {
q = ']';
} else if (c == '(') {
q = ')';
} else {
throw tokener.syntaxError("A JSONArray text must start with '['");
}
if (tokener.nextClean() == ']') {
return;
}
tokener.back();
int index = 0;
while(true) {
if (tokener.nextClean() == ',') {
tokener.back();
throw new JSONException("JSONArray[" + index + "] not found.");
} else {
tokener.back();
Object nextValue = tokener.nextValue();
if (nextValue instanceof JSONObject) {
// Instead of storing the JSONObject in a List as
// JSONArray does, simply make the persist call straight
// from this object. Since it is not used after, it
// will eventually be garbage collected.
jsonObject = (JSONObject) nextValue;
logger.debug("Decoding Message: " + jsonObject);
uuid = jsonObject.getString("uuid");
decode(jsonObject);
index++;
} else {
throw new JSONException("JSONArray[" + index + "] is not a JSONObject.");
}
}
c = tokener.nextClean();
switch (c) {
case ';':
case ',':
if (tokener.nextClean() == ']') {
return;
}
tokener.back();
break;
case ']':
case ')':
if (q != c) {
throw tokener.syntaxError("Expected a '" + new Character(q) + "'");
}
return;
default:
throw tokener.syntaxError("Expected a ',' or ']'");
}
}
}
} catch (JSONException e) {
if (jsonObject != null) {
logger.error("Error decoding JSONObject " + jsonObject);
}
throw new SPPersistenceException(uuid, e);
}
}
/**
* Takes in a JSONArray of persister calls. The JSON message is expected to
* be a JSONArray of JSONObjects. Each JSONObject contains details for
* making a SPPersister method call.
*
* It expects the following key-value pairs in each JSONObject message:
* <ul>
* <li>method - The String value of a {@link SPPersistMethod}. This is used
* to determine which {@link SPPersister} method to call.</li>
* <li>uuid - The UUID of the SPObject, if there is one, that the persist
* method call will act on. If there is none, it expects
* {@link JSONObject#NULL}</li>
* </ul>
* Other possible key-value pairs (depending on the intended method call)
* include:
* <ul>
* <li>parentUUID</li>
* <li>type</li>
* <li>newValue</li>
* <li>oldValue</li>
* <li>propertyName</li>
* </ul>
* See the method documentation of {@link SPPersister} for full details on
* the expected values
*/
public void decode(JSONArray json) throws SPPersistenceException {
JSONObject jsonObject = null;
String uuid = null;
try {
synchronized (persister) {
for (int i=0; i < json.length(); i++) {
jsonObject = json.getJSONObject(i);
logger.debug("Decoding Message: " + jsonObject);
uuid = jsonObject.getString("uuid");
decode(jsonObject);
}
}
} catch (JSONException e) {
if (jsonObject != null) {
logger.error("Error decoding JSONObject " + jsonObject);
}
throw new SPPersistenceException(uuid, e);
}
}
private static Object getNullable(@Nonnull JSONObject jo, String propName) throws JSONException {
final Object value = jo.get(propName);
if (value == JSONObject.NULL) {
return null;
} else {
return value;
}
}
public static Object getWithType(@Nonnull JSONObject jo, DataType type, String propName) throws JSONException {
if (getNullable(jo, propName) == null) return null;
switch (type) {
case BOOLEAN:
return Boolean.valueOf(jo.getBoolean(propName));
case DOUBLE:
return Double.valueOf(jo.getDouble(propName));
case INTEGER:
return Integer.valueOf(jo.getInt(propName));
case LONG:
return Long.valueOf(jo.getLong(propName));
case SHORT:
return Short.valueOf(jo.getShort(propName));
case FLOAT:
return Float.valueOf(jo.getFloat(propName));
case PNG_IMG:
getNullable(jo, propName);
String base64Data = jo.getString(propName);
byte[] decodedBytes;
try {
decodedBytes = Base64.decodeBase64(base64Data.getBytes("ascii"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("ASCII should always be supported!", e);
}
return new ByteArrayInputStream(decodedBytes);
case NULL:
case STRING:
case REFERENCE:
default:
return getNullable(jo, propName);
}
}
/**
* Takes in a {@link JSONObject} that represents a single persister call.
* The {@link JSONObject} contains details for making a {@link SPPersister}
* method call.
*
* It expects the following key-value pairs in the {@link JSONObject}
* message:
* <ul>
* <li>method - The String value of a {@link SPPersistMethod}. This is used
* to determine which {@link SPPersister} method to call.</li>
* <li>uuid - The UUID of the SPObject, if there is one, that the persist
* method call will act on. If there is none, it expects
* {@link JSONObject#NULL}</li>
* </ul>
* Other possible key-value pairs (depending on the intended method call)
* include:
* <ul>
* <li>parentUUID</li>
* <li>type</li>
* <li>newValue</li>
* <li>oldValue</li>
* <li>propertyName</li>
* </ul>
* See the method documentation of {@link SPPersister} for full details on
* the expected values
*
* @param jsonObject
* The {@link JSONObject} that represents the single persister
* call.
* @throws SPPersistenceException
* Thrown if a key-value pair in the {@link JSONObject} cannot
* be retrieved.
*/
private void decode(JSONObject jsonObject) throws SPPersistenceException {
String uuid = null;
try {
uuid = jsonObject.getString("uuid");
SPPersistMethod method = SPPersistMethod.getMethodForCode(jsonObject.getString(SPJSONPersister.METHOD));
String parentUUID;
String propertyName;
DataType propertyType;
Object newValue;
switch (method) {
case begin:
persister.begin();
break;
case commit:
persister.commit();
break;
case persistObject:
parentUUID = jsonObject.getString(SPJSONPersister.PARENT_UUID);
if (parentUUID.equals("")) {
//throw new SPPersistenceException(null, "Cannot persist object with null UUID, json is " + jsonObject);
}
String type = jsonObject.getString("type");
int index = jsonObject.getInt("index");
persister.persistObject(parentUUID, type, uuid, index);
break;
case changeProperty:
propertyName = jsonObject.getString(SPJSONPersister.PROPERTY_NAME);
propertyType = DataType.valueOf(jsonObject.getString("type"));
newValue = getWithType(jsonObject, propertyType, SPJSONPersister.NEW_VALUE);
Object oldValue = getWithType(jsonObject, propertyType, "oldValue");
persister.persistProperty(uuid, propertyName,
propertyType, oldValue, newValue);
break;
case persistProperty:
propertyName = jsonObject.getString(SPJSONPersister.PROPERTY_NAME);
propertyType = DataType.valueOf(jsonObject.getString("type"));
newValue = getWithType(jsonObject, propertyType, SPJSONPersister.NEW_VALUE);
if (newValue == null) logger.debug("newValue was null for propertyName " + propertyName);
persister.persistProperty(uuid, propertyName,
propertyType, newValue);
break;
case removeObject:
parentUUID = jsonObject.getString(SPJSONPersister.PARENT_UUID);
if (parentUUID.equals("")) {
throw new SPPersistenceException(null, "Cannot persist object with null UUID");
}
persister.removeObject(parentUUID, uuid);
break;
case rollback:
persister.rollback();
break;
default:
throw new SPPersistenceException(uuid,
"Does not support SP persistence method " + method);
}
} catch (JSONException e) {
if (jsonObject != null) {
logger.error("Error decoding JSONObject " + jsonObject);
}
throw new SPPersistenceException(uuid, e);
}
}
}