/* * Copyright (c) 2013 EMC Corporation * All Rights Reserved */ package com.emc.storageos.db.client.model; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.emc.storageos.db.exceptions.DatabaseException; /** * Serializer for object to byte array */ public class GenericSerializer { private static final int MAX_PROPERTIES = 256; private static final Charset ENCODING = Charset.forName("UTF-8"); private class PropertiesMap { private PropertyDescriptor[] _array; private ArrayList<Integer> _indices; public PropertiesMap() { _array = new PropertyDescriptor[MAX_PROPERTIES]; Arrays.fill(_array, null); _indices = new ArrayList<Integer>(); } /** * Adds PropertyDescriptor at the given index * * @param index Serialization index for the property * @param pd PropertyDescriptor */ public void add(int index, PropertyDescriptor pd) { if (index >= MAX_PROPERTIES) { throw DatabaseException.fatals.serializationFailedIndexGreaterThanMax(pd.getName(), index, MAX_PROPERTIES); } if (_array[index] != null) { throw DatabaseException.fatals.serializationFailedIndexReused(pd.getName(), index); } _array[index] = pd; _indices.add(index); } public ArrayList<Integer> getIndices() { return _indices; } public PropertyDescriptor get(int index) { return _array[index]; } } private ConcurrentMap<Class<?>, PropertiesMap> _typeCache; /** * default constructor */ public GenericSerializer() { _typeCache = new ConcurrentHashMap<Class<?>, PropertiesMap>(); } /** * Initialize the property descriptor map for the given class type * * @param clazz */ private void initForType(Class<?> clazz) { if (_typeCache.containsKey(clazz)) { return; } BeanInfo bInfo; try { bInfo = Introspector.getBeanInfo(clazz); } catch (final IntrospectionException ex) { throw DatabaseException.fatals.serializationFailedInitializingBeanInfo(clazz, ex); } PropertyDescriptor[] pds = bInfo.getPropertyDescriptors(); PropertiesMap properties = new PropertiesMap(); for (int i = 0; i < pds.length; i++) { PropertyDescriptor pd = pds[i]; if (pd.getName().equals("class")) { continue; } byte index = 0; Annotation[] annotations = pd.getReadMethod().getAnnotations(); for (int j = 0; j < annotations.length; j++) { Annotation a = annotations[j]; if (a instanceof SerializationIndex) { index = ((SerializationIndex) a).value(); } } properties.add(index, pd); } _typeCache.putIfAbsent(clazz, properties); } public PropertiesMap getProperties(Class<?> clazz) { if (!_typeCache.containsKey(clazz)) { // init the map for this type initForType(clazz); } return _typeCache.get(clazz); } /** * Decode a long number from variable length encoded byte array * 1. decode 7 bits from each bit, starting with LSB * 2. undo the sign-unsigned conversion we did, to restore the sign bit * * @param read byte array * @return long */ public long decodeVariantLong(byte[] read) { int shift = 0; long result = 0; ByteArrayInputStream in = new ByteArrayInputStream(read); while (shift < 64) { final byte b = (byte) in.read(); result |= (long) (b & 0x7F) << shift; if ((b & 0x80) == 0) { break; } shift += 7; } return ((result >>> 1) ^ -(result & 1)); } /** * Encode a long number as variable length encoded byte array * 1. get equivalent unsigned long * 2. encode 7 bits at a time starting from LSB, set the highest bit if there is a next byte needed * * @param read long value to encode * @return byte array */ public byte[] encodeVariantLong(long read) { long value = (read << 1) ^ (read >> 63); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); while (true) { if ((value & ~0x7FL) == 0) { bytes.write((int) value); break; } else { bytes.write(((int) value & 0x7F) | 0x80); value >>>= 7; } } return bytes.toByteArray(); } /** * Get byte[] representation of this object * each field is encoded as 3 values, { index [byte], len [2 bytes], value [len bytes] } * * @param clazz * @param obj * @param <T> * @return */ public <T> byte[] toByteArray(Class<T> clazz, T obj) throws DatabaseException { PropertiesMap propertiesMap = getProperties(clazz); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { for (int index : propertiesMap.getIndices()) { PropertyDescriptor pd = propertiesMap.get(index); if (pd == null) { // this should not happen throw DatabaseException.fatals.serializationFailedInconsistentPropertyMap(clazz); } Class<?> type = pd.getPropertyType(); byte[] value; if (type == String.class) { String str = (String) pd.getReadMethod().invoke(obj); if (str == null) { continue; } value = str.getBytes(ENCODING); } else if (type == URI.class) { URI uri = (URI) pd.getReadMethod().invoke(obj); if (uri == null) { continue; } value = uri.toString().getBytes(ENCODING); } else if (type == long.class) { long lvalue = (Long) pd.getReadMethod().invoke(obj); value = encodeVariantLong(lvalue); } else if (type == boolean.class) { boolean lvalue = (Boolean) pd.getReadMethod().invoke(obj); value = new byte[1]; value[0] = lvalue ? (byte) 1 : (byte) 0; } else if (type == byte[].class) { byte[] lvalue = (byte[]) pd.getReadMethod().invoke(obj); value = lvalue; } else { // throw -- implement value for this type throw DatabaseException.fatals.serializationFailedNotImplementedForType(clazz, pd.getName(), type); } // now encode this field if ((value.length & 0x0000) > 0) { throw DatabaseException.fatals.serializationFailedFieldLengthTooLong(clazz, pd.getName(), value.length); } // write index out.write(index); int len = value.length; // length, encoded as 2 bytes out.write((len >> 8)); out.write((len & 0xff)); // actual value out.write(value); } } catch (final IOException ex) { throw DatabaseException.fatals.serializationFailedClass(clazz, ex); } catch (final IllegalAccessException ex) { throw DatabaseException.fatals.serializationFailedClass(clazz, ex); } catch (final InvocationTargetException ex) { throw DatabaseException.fatals.serializationFailedClass(clazz, ex); } return out.toByteArray(); } /** * Create object of specified type, from given byte[] * encoding is expected as 3-tuple per field { index [byte], len [2 bytes], value [len bytes] } * * @param clazz * @param <T> * @return */ public <T> T fromByteArray(Class<T> clazz, byte[] bytes) throws DatabaseException { PropertiesMap propertiesMap = getProperties(clazz); ByteArrayInputStream in = new ByteArrayInputStream(bytes); try { T retObj = clazz.newInstance(); // at least, 3 bytes expected while (in.available() > 3) { // read len int index = in.read(); int high = in.read(); int low = in.read(); int len = (high << 8 | low); byte[] value = new byte[len]; in.read(value, 0, len); if (index >= MAX_PROPERTIES) { throw DatabaseException.fatals.deserializationFailedUnexpectedIndex(clazz, index, MAX_PROPERTIES); } // now, set value to the respective field PropertyDescriptor pd = propertiesMap.get(index); if (pd == null) { // old field we don't have anymore, ignore continue; } Class<?> type = pd.getPropertyType(); if (type == String.class) { pd.getWriteMethod().invoke(retObj, new String(value)); } else if (type == URI.class) { URI uri = URI.create(new String(value)); pd.getWriteMethod().invoke(retObj, uri); } else if (type == long.class) { pd.getWriteMethod().invoke(retObj, decodeVariantLong(value)); } else if (type == boolean.class) { pd.getWriteMethod().invoke(retObj, value[0] == (byte) 1 ? true : false); } else if (type == byte[].class) { pd.getWriteMethod().invoke(retObj, value); } else { // throw -- implement value for this type throw DatabaseException.fatals.deserializationFailedUnsupportedType(clazz, pd.getName(), type); } } return retObj; } catch (InstantiationException e) { throw DatabaseException.fatals.deserializationFailed(clazz, e); } catch (IllegalAccessException e) { throw DatabaseException.fatals.deserializationFailed(clazz, e); } catch (IllegalArgumentException e) { throw DatabaseException.fatals.deserializationFailed(clazz, e); } catch (InvocationTargetException e) { throw DatabaseException.fatals.deserializationFailed(clazz, e); } finally { try { in.close(); } catch (final IOException e) { // ignore } } } }