/**
* Copyright (c) 2011-2014 Optimax Software Ltd.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Optimax Software, ElasticInbox, nor the names
* of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.elasticinbox.core.cassandra.persistence;
import static me.prettyprint.hector.api.factory.HFactory.createColumn;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.prettyprint.cassandra.serializers.BytesArraySerializer;
import me.prettyprint.cassandra.serializers.DateSerializer;
import me.prettyprint.cassandra.serializers.LongSerializer;
import me.prettyprint.cassandra.serializers.SerializerTypeInferer;
import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.hector.api.beans.HColumn;
import com.elasticinbox.common.utils.IOUtils;
import com.elasticinbox.common.utils.JSONUtils;
import com.elasticinbox.config.Configurator;
import com.elasticinbox.core.model.Address;
import com.elasticinbox.core.model.AddressList;
import com.elasticinbox.core.model.Marker;
import com.elasticinbox.core.model.Message;
import com.elasticinbox.core.model.MimePart;
public final class Marshaller
{
public final static String CN_DATE = "date";
public final static String CN_SIZE = "size";
public final static String CN_FROM = "from";
public final static String CN_TO = "to";
public final static String CN_CC = "cc";
public final static String CN_BCC = "bcc";
public final static String CN_REPLY_TO = "replyto";
public final static String CN_MESSAGE_ID = "mid";
public final static String CN_SUBJECT = "subject";
public final static String CN_HTML_BODY = "html";
public final static String CN_PLAIN_BODY = "plain";
public final static String CN_PARTS = "parts";
public final static String CN_BRI = "bri"; // Blob Resource Identifier
public final static String CN_LABEL_PREFIX = "l:";
public final static String CN_MARKER_PREFIX = "m:";
private final static DateSerializer dateSe = DateSerializer.get();
private final static LongSerializer longSe = LongSerializer.get();
private final static StringSerializer strSe = StringSerializer.get();
private final static BytesArraySerializer byteSe = BytesArraySerializer.get();
/**
* Unmarshall message contents from Cassandra {@link HColumn} columns and
* return resulting {@link Message} object.
*
* @param columns
* Cassandra columns to unmarshall
* @param includeBody
* Contents of the message body (plain and HTML) will not be
* included in the result if set to false
* @return
*/
protected static Message unmarshall(
final List<HColumn<String, byte[]>> columns,
final boolean includeBody)
{
Message message = new Message();
for (HColumn<String, byte[]> c : columns)
{
if (c != null && c.getValue() != null)
{
// map columns to Message object
if (c.getName().equals(CN_DATE)) {
message.setDate(dateSe.fromBytes(c.getValue()));
} else if (c.getName().equals(CN_SIZE)) {
message.setSize(longSe.fromBytes(c.getValue()));
} else if (c.getName().equals(CN_SUBJECT)) {
message.setSubject(strSe.fromBytes(c.getValue()));
} else if (c.getName().equals(CN_MESSAGE_ID)) {
message.setMessageId(strSe.fromBytes(c.getValue()));
} else if (c.getName().equals(CN_FROM)) {
message.setFrom(unserializeAddress(c.getValue()));
} else if (c.getName().equals(CN_TO)) {
message.setTo(unserializeAddress(c.getValue()));
} else if (c.getName().equals(CN_CC)) {
message.setCc(unserializeAddress(c.getValue()));
} else if (c.getName().equals(CN_BCC)) {
message.setBcc(unserializeAddress(c.getValue()));
} else if (c.getName().equals(CN_REPLY_TO)) {
message.setReplyTo(unserializeAddress(c.getValue()));
} else if (c.getName().equals(CN_BRI)) {
message.setLocation(URI.create(
strSe.fromBytes(c.getValue())));
} else if (c.getName().startsWith(CN_LABEL_PREFIX)) {
Integer labelId = Integer
.parseInt(c.getName().split("\\:")[1]);
message.addLabel(labelId);
} else if (c.getName().startsWith(CN_MARKER_PREFIX)) {
Integer markerId = Integer
.parseInt(c.getName().split("\\:")[1]);
message.addMarker(Marker.fromInt(markerId));
} else if (includeBody && c.getName().equals(CN_HTML_BODY)) {
try {
message.setHtmlBody(strSe.fromBytes(
IOUtils.decompress(c.getValue())));
} catch (Exception e) {
//TODO: logger.error("Decompression of message body failed: ", e);
}
} else if (includeBody && c.getName().equals(CN_PLAIN_BODY)) {
try {
message.setPlainBody(strSe.fromBytes(
IOUtils.decompress(c.getValue())));
} catch (Exception e) {
//TODO: logger.error("Decompression of message body failed: ", e);
}
} else if (c.getName().equals(CN_PARTS)) {
Map<String, MimePart> parts = null;
parts = JSONUtils.toObject(c.getValue(), parts);
message.setParts(parts);
}
}
}
return message;
}
/**
* Marshall the {@link Message} object contents to Cassandra columns
*
* @param m Message to marshall
* @return
* @throws IOException
*/
protected static List<HColumn<String, byte[]>> marshall(final Message m)
throws IOException
{
Map<String, Object> columns = new HashMap<String, Object>();
if (m.getSize() != null) {
columns.put(CN_SIZE, m.getSize());
}
if (m.getDate() != null) {
columns.put(CN_DATE, m.getDate());
}
if (m.getFrom() != null) {
columns.put(CN_FROM, serializeAddress(m.getFrom()));
}
if (m.getTo() != null) {
columns.put(CN_TO, serializeAddress(m.getTo()));
}
if (m.getCc() != null) {
columns.put(CN_CC, serializeAddress(m.getCc()));
}
if (m.getBcc() != null) {
columns.put(CN_BCC, serializeAddress(m.getBcc()));
}
if (m.getReplyTo() != null) {
columns.put(CN_REPLY_TO, serializeAddress(m.getReplyTo()));
}
if (m.getMessageId() != null) {
columns.put(CN_MESSAGE_ID, m.getMessageId());
}
if (m.getSubject() != null) {
columns.put(CN_SUBJECT, m.getSubject());
}
if (m.getLocation() != null) {
columns.put(CN_BRI, m.getLocation().toString());
}
if (m.getParts() != null) {
columns.put(CN_PARTS, JSONUtils.fromObject(m.getParts()));
}
// add markers
if (!m.getMarkers().isEmpty())
{
for (Marker marker : m.getMarkers())
{
String cn = CN_MARKER_PREFIX + marker.toInt();
columns.put(cn, new byte[0]);
}
}
// add labels
if (!m.getLabels().isEmpty())
{
for (Integer labelId : m.getLabels())
{
String cn = CN_LABEL_PREFIX + labelId;
columns.put(cn, new byte[0]);
}
}
// add HTML message text
// Fallback: if HTML is enabled but not available, store PLAIN message text
if (Configurator.isStoreHtmlWithMetadata())
{
if (m.getHtmlBody() != null) {
columns.put(CN_HTML_BODY, IOUtils.compress(m.getHtmlBody()));
} else if (!Configurator.isStorePlainWithMetadata() && (m.getPlainBody() != null)) {
columns.put(CN_PLAIN_BODY, IOUtils.compress(m.getPlainBody()));
}
}
// add PLAIN message text
if (Configurator.isStorePlainWithMetadata() && (m.getPlainBody() != null)) {
columns.put(CN_PLAIN_BODY, IOUtils.compress(m.getPlainBody()));
}
return mapToHColumns(columns);
}
/**
* Serialize {@link AddressList} to JSON
*
* @param addresses
* @return
*/
private static byte[] serializeAddress(final AddressList addresses)
{
List<String[]> result = new ArrayList<String[]>();
for (Address address : addresses)
{
result.add(new String[]{
(address.getName() == null ? "" : address.getName()),
(address.getAddress() == null ? "" : address.getAddress()) });
}
return JSONUtils.fromObject(result);
}
/**
* Unserialize JSON sting to {@link AddressList}
*
* @param val
* @return
*/
private static AddressList unserializeAddress(final byte[] val)
{
List<Address> result = new ArrayList<Address>();
List<List<String>> addresses = null;
addresses = JSONUtils.toObject(val, addresses);
for (List<String> address : addresses) {
result.add(new Address(address.get(0), address.get(1)));
}
return new AddressList(result);
}
/**
* Convert Map of arbitrary Objects to the List of HColumns serialized as
* <code>byte[]</code>
*
* @param map
* @return
*/
protected static List<HColumn<String, byte[]>> mapToHColumns(
final Map<String, Object> map)
{
List<HColumn<String, byte[]>> columns =
new ArrayList<HColumn<String, byte[]>>(map.size());
for (Map.Entry<String, Object> a : map.entrySet())
{
columns.add(createColumn(a.getKey(), SerializerTypeInferer
.getSerializer(a.getValue()).toBytes(a.getValue()),
strSe, byteSe));
}
return columns;
}
}