/*
* Copyright 2013 eBuddy B.V.
*
* 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 com.ebuddy.cassandra.structure;
import static org.apache.commons.lang3.ObjectUtils.NULL;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Converter for embedded objects in values encoded as bytes.
*
* @author Eric Zoerner <a href="mailto:ezoerner@ebuddy.com">ezoerner@ebuddy.com</a>
*/
public class StructureConverter {
private static final String UTF_8 = "UTF-8";
private static final Charset UTF8_CHARSET = Charset.forName(UTF_8);
private static final StructureConverter INSTANCE = new StructureConverter();
protected static final ObjectMapper JSON_MAPPER = new ObjectMapper();
/**
* Header char, a unicode non-character, used to flag a JSON deserialized object.
*/
private static final int HEADER_CHAR = '\uFFFE';
/**
* utf-8 encoded bytes for HEADER_CHAR.
*/
private static final byte[] UTF8_HEADER_BYTES = {(byte)0xef, (byte)0xbf, (byte)0xbe};
/**
* Only instantiated once for the static singleton.
*/
private StructureConverter() { }
/**
* Get the singleton instance of StructureConverter.
*
* @return the singleton StructureConverter
*/
public static StructureConverter get() {
return INSTANCE;
}
/**
* @throws DataFormatException if format of the string is incorrect and could not be parsed as JSON
*/
public Object fromString(String str) {
if (str == null) {
return null;
}
return decodeBytes(str.getBytes(UTF8_CHARSET));
}
/**
* @throws DataFormatException if the object could not be encoded as JSON
*/
public String toString(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof String) {
return (String)obj;
}
// write as special header bytes followed by JSON
// intercept the Null token which stands in for a real null
if (obj == NULL) {
obj = null;
}
String jsonString = encodeJson(obj);
char[] chars = new char[jsonString.length() + 1];
chars[0] = HEADER_CHAR;
jsonString.getChars(0, jsonString.length(), chars, 1);
return new String(chars);
}
/**
* @throws DataFormatException if object cannot be encoded as JSON
*/
public ByteBuffer toByteBuffer(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof String) {
return ByteBuffer.wrap(((String)obj).getBytes(UTF8_CHARSET));
}
// write as special header bytes followed by JSON
// intercept the Null token which stands in for a real null
if (obj == NULL) {
obj = null;
}
String jsonString = encodeJson(obj);
byte[] jsonBytes = jsonString.getBytes(UTF8_CHARSET);
byte[] result = new byte[jsonBytes.length + UTF8_HEADER_BYTES.length];
System.arraycopy(UTF8_HEADER_BYTES, 0, result, 0, UTF8_HEADER_BYTES.length);
System.arraycopy(jsonBytes, 0, result, UTF8_HEADER_BYTES.length, jsonBytes.length);
return ByteBuffer.wrap(result);
}
/**
* @throws DataFormatException is data in the byte buffer is incorrect and cannot be decoded
*/
public Object fromByteBuffer(ByteBuffer byteBuffer) {
if (byteBuffer == null) {
return null;
}
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes, 0, bytes.length);
return decodeBytes(bytes);
}
@SuppressWarnings("fallthrough")
private Object decodeBytes(byte[] bytes) {
if (bytes.length == 0) {
return "";
}
// look for header char to determine if a JSON object or legacy NestedProperties
PushbackReader reader = new PushbackReader(new InputStreamReader(new ByteArrayInputStream(bytes), UTF8_CHARSET));
try {
int firstChar = reader.read();
switch (firstChar) {
case '\uFFFF':
// legacy NestedProperties, obsolete and interpreted now as simply a JSON encoded Map or
// beginning of a list terminator
// if the second character is \uFFFF then this is a list terminator value and just return it
int secondChar = reader.read();
if (secondChar == '\uFFFF') {
return Types.LIST_TERMINATOR_VALUE;
}
if (secondChar == -1) {
throw new DataFormatException("Found header FFFF but no data");
}
reader.unread(secondChar);
// fall through and read as a JSON object
case HEADER_CHAR:
try {
return JSON_MAPPER.readValue(reader, Object.class);
} catch (IOException e) {
throw new DataFormatException("Could not parse JSON", e);
}
default:
// if no special header, then bytes are just a string
return new String(bytes, UTF8_CHARSET);
}
} catch (IOException ioe) {
throw new DataFormatException("Could read data", ioe);
}
}
private String encodeJson(Object value) {
String jsonString;
try {
jsonString = JSON_MAPPER.writeValueAsString(value);
return jsonString;
} catch (IOException ioe) {
throw new DataFormatException("Could not encode object as JSON: class=" + value.getClass().getName(), ioe);
}
}
}