/* * 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.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.json.JSONException; import org.json.JSONObject; import ca.sqlpower.dao.MessageSender; import ca.sqlpower.dao.SPPersistenceException; import ca.sqlpower.dao.SPPersister; import ca.sqlpower.util.SQLPowerUtils; /** * A {@link SPPersister} implementation that serializes * {@link SPPersister} method calls as {@link JSONObject}s and transmits them * to a destination using a {@link MessageSender}. This allows these method * calls to be transmitted to other systems, typically (but not necessarily) * over a network connection. */ public class SPJSONPersister implements SPPersister { /** * The JSON parameter representing the method of the persist call. Value can be * any of the ones defined at {@link SPPersistMethod}. */ static final String METHOD = "M"; /** * The JSON parameter representing the new value field of a persist * property event. */ static final String NEW_VALUE = "NV"; /** * The JSON parameter representing the property name field of a persist * property event. */ static final String PROPERTY_NAME = "PN"; /** * The JSON parameter representing the parent UUID for child add and remove * events. */ static final String PARENT_UUID = "pID"; private static final Logger logger = Logger .getLogger(SPJSONPersister.class); /** * A count of transactions, mainly to keep track of nested transactions. */ private int transactionCount = 0; /** * A MessagePasser object that is responsible for transmitting the * JSONObject contents. */ private final MessageSender<JSONObject> messageSender; private final List<JSONObject> messageBuffer; /** * Create a {@link SPJSONPersister} that uses the given * {@link MessageSender} to transmit the JSON content */ public SPJSONPersister(MessageSender<JSONObject> messageSender) { this.messageSender = messageSender; this.messageBuffer = new ArrayList<JSONObject>(); } public void begin() throws SPPersistenceException{ JSONObject jsonObject = new JSONObject(); try { jsonObject.put(METHOD, SPPersistMethod.begin.getCode()); // Need to put this in or anything calling get on the key "uuid" // will throw a JSONException jsonObject.put("uuid", JSONObject.NULL); } catch (JSONException e) { logger.error("Exception encountered while building JSON message. Rollback initiated.",e); rollback(); throw new SPPersistenceException(null, e); } logger.debug(jsonObject); messageBuffer.add(jsonObject); transactionCount++; } public void commit() throws SPPersistenceException { if (transactionCount == 0) { throw new SPPersistenceException(null, "Commit attempted while not in a transaction"); } JSONObject jsonObject = new JSONObject(); try { jsonObject.put(METHOD, SPPersistMethod.commit.getCode()); // Need to put this in or anything calling get on the key "uuid" // will throw a JSONException jsonObject.put("uuid", JSONObject.NULL); } catch (JSONException e) { logger.error("Exception encountered while building JSON message. Rollback initiated.",e); rollback(); throw new SPPersistenceException(null, e); } try { logger.debug(jsonObject); messageBuffer.add(jsonObject); if (transactionCount == 1) { for (JSONObject obj: messageBuffer) { messageSender.send(obj); } messageBuffer.clear(); messageSender.flush(); transactionCount = 0; } else { transactionCount--; } } catch (Throwable t) { logger.error("Exception encountered while building JSON message. Rollback initiated.",t); messageBuffer.clear(); messageSender.clear(); transactionCount = 0; rollback(); if (t instanceof SPPersistenceException) { throw (SPPersistenceException) t; } else if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new RuntimeException(t); } } } /** * XXX Any JSON persisters that extend this class must override this method * before calling super.{@link #persistObject(String, String, String, int)} * if null parentUUIDs need to be handled correctly. */ public void persistObject(String parentUUID, String type, String uuid, int index) throws SPPersistenceException { // XXX Not handling null parentUUIDs here. Extending classes should handle them before calling super.persistObject if (transactionCount == 0) { throw new SPPersistenceException("Operation attempted while not in a transaction."); } JSONObject jsonObject = new JSONObject(); try { jsonObject.put(METHOD, SPPersistMethod.persistObject.getCode()); if (parentUUID == null) { jsonObject.put(PARENT_UUID, JSONObject.NULL); } else { jsonObject.put(PARENT_UUID, parentUUID); } jsonObject.put("type", type); jsonObject.put("uuid", uuid); jsonObject.put("index", index); } catch (JSONException e) { logger.error(e); rollback(); throw new SPPersistenceException(uuid, e); } logger.debug(jsonObject); messageBuffer.add(jsonObject); } public void persistProperty(String uuid, String propertyName, DataType type, Object oldValue, Object newValue) throws SPPersistenceException { if (transactionCount == 0) { throw new SPPersistenceException("Operation attempted while not in a transaction."); } JSONObject jsonObject = new JSONObject(); try { jsonObject.put(METHOD, SPPersistMethod.changeProperty.getCode()); jsonObject.put("uuid", uuid); jsonObject.put(PROPERTY_NAME, propertyName); jsonObject.put("type", type.toString()); setValueInJSONObject(jsonObject, "oldValue", type, oldValue); setValueInJSONObject(jsonObject, NEW_VALUE, type, newValue); } catch (JSONException e) { logger.error(e); rollback(); throw new SPPersistenceException(uuid, e); } catch (IOException e) { logger.error(e); rollback(); throw new SPPersistenceException(uuid, e); } logger.debug(jsonObject); messageBuffer.add(jsonObject); } public void persistProperty(String uuid, String propertyName, DataType type, Object newValue) throws SPPersistenceException { if (transactionCount == 0) { throw new SPPersistenceException("Operation attempted while not in a transaction."); } JSONObject jsonObject = new JSONObject(); try { jsonObject.put(METHOD, SPPersistMethod.persistProperty.getCode()); jsonObject.put("uuid", uuid); jsonObject.put(PROPERTY_NAME, propertyName); jsonObject.put("type", type.toString()); setValueInJSONObject(jsonObject, NEW_VALUE, type, newValue); } catch (JSONException e) { logger.error(e); rollback(); throw new SPPersistenceException(uuid, e); } catch (IOException e) { logger.error(e); rollback(); throw new SPPersistenceException(uuid, e); } logger.debug(jsonObject); messageBuffer.add(jsonObject); } /** * Sets the named property of the given JSON object to the given value. This * is a nontrivial operation because JSON nulls are special, and so are * values of type PNG_IMAGE. * * @param jsonObject * The object whose property to set. * @param jsonPropName * The property name to set on jsonObject. * @param type * the SPPersister framework datatype for value. * @param value * The actual value to set. Values for all possible DataTypes are * properly converted to a JavaScript data type before being set. */ private void setValueInJSONObject(JSONObject jsonObject, String jsonPropName, DataType type, Object value) throws IOException, JSONException, UnsupportedEncodingException { if (type == DataType.PNG_IMG && value != null) { InputStream in = (InputStream) value; ByteArrayOutputStream out = new ByteArrayOutputStream(); SQLPowerUtils.copyStream(in, out); byte[] bytes = out.toByteArray(); byte[] base64Bytes = Base64.encodeBase64(bytes); jsonObject.put(jsonPropName, new String(base64Bytes, "ascii")); } else { jsonObject.put(jsonPropName, value == null ? JSONObject.NULL : value); } }; public void removeObject(String parentUUID, String uuid) throws SPPersistenceException { if (transactionCount == 0) { throw new SPPersistenceException("Operation attempted while not in a transaction."); } JSONObject jsonObject = new JSONObject(); try { jsonObject.put(METHOD, SPPersistMethod.removeObject.getCode()); jsonObject.put(PARENT_UUID, parentUUID); jsonObject.put("uuid", uuid); } catch (JSONException e) { logger.error(e); rollback(); throw new SPPersistenceException(uuid, e); } logger.debug(jsonObject); messageBuffer.add(jsonObject); } public void rollback() { messageBuffer.clear(); messageSender.clear(); transactionCount = 0; } public MessageSender<JSONObject> getMessageSender() { return messageSender; } }