Java tutorial
/* * This file is part of Px100 Data. * * Px100 Data 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. * * This program 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 com.px100systems.util.serialization; import com.px100systems.util.PropertyAccessor; import com.px100systems.util.SpringELCtx; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ReflectionUtils; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Definition to serialize class.<br> * Used in universal reflection-based Externalizable, Portable, and other implementations.<br> * Application (JVM) wide singleton. Not thread-safe during creation for performance reasons - expected to be created by one "initialization" thread. * Register every class using {@link SerializationDefinition#register register()} and then call {@link SerializationDefinition#lock lock()}. * * @version 0.3 <br>Copyright (c) 2015 Px100 Systems. All Rights Reserved.<br> * @author Alex Rogachevsky */ public class SerializationDefinition { private static Map<Class<?>, SerializationDefinition> definitions = new HashMap<>(); private static int classId = 1; private static Map<Integer, SerializationDefinition> classIds = new HashMap<>(); private static boolean locked = false; private static class FieldDefinition { private String name; private Class<?> type; private boolean primitive = false; private Class<?> collectionType = null; private Method accessor; private Method mutator = null; private Expression calculator = null; public FieldDefinition() { } public FieldDefinition(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FieldDefinition)) return false; FieldDefinition that = (FieldDefinition) o; return name.equals(that.name); } @Override public int hashCode() { return name.hashCode(); } } private List<FieldDefinition> fields = new ArrayList<>(); private List<FieldDefinition> gettersOnly = new ArrayList<>(); private Constructor<?> constructor; private Method serializingSetter = null; private Integer id; /** * Get the definition by class * @param cls class * @return definition or null */ public static SerializationDefinition get(Class<?> cls) { return definitions.get(cls); } /** * Get the definition by assigned ID (Hazelcast portable only) * @param id auto-assigned ID * @return definition or null */ public static SerializationDefinition getById(Integer id) { return classIds.get(id); } /** * Auto-assigned ID * @return registered class ID */ public Integer getId() { return id; } /** * Helper method leveraging already parsed reflection data. Throws an exception oif the getter is not found. * @param name field name */ public void checkGetter(String name) { Set<FieldDefinition> list = new HashSet<>(gettersOnly); if (!list.contains(new FieldDefinition(name))) throw new RuntimeException( "Getter for " + name + " not found in " + constructor.getDeclaringClass().getSimpleName()); } /** * Helper method leveraging already parsed reflection data. Throws an exception if the field is not found. * @param fieldNames field names */ public void checkFields(List<String> fieldNames) { Set<FieldDefinition> list = new HashSet<>(fields); list.addAll(gettersOnly); for (String name : fieldNames) { FieldDefinition f = new FieldDefinition(name); if (!list.contains(f)) throw new RuntimeException( "Field " + name + " not found in " + constructor.getDeclaringClass().getSimpleName()); } } /** * Get bean's field leveraging already parsed reflection data. Throws an exception if the field is not found. * @param bean the bean * @param name field name * @return teh field value */ public Object getField(Object bean, String name) { List<FieldDefinition> list = new ArrayList<>(fields); list.addAll(gettersOnly); for (FieldDefinition f : list) if (f.name.equals(name)) return invokeMethod(f.accessor, bean); throw new RuntimeException( "Field " + name + " not found in " + constructor.getDeclaringClass().getSimpleName()); } /** * Set bean field. Throws an exception if the field is not found. * @param bean the bean * @param field field name * @param value field value */ @SuppressWarnings("unused") public void setField(Object bean, String field, Object value) { for (FieldDefinition fd : fields) if (fd.name.equals(field)) { invokeMethod(fd.mutator, bean, value); return; } throw new RuntimeException( "Field " + field + " not found in " + constructor.getDeclaringClass().getSimpleName()); } /** * Get field type leveraging already parsed reflection data. Throws an exception if the field is not found. * @param name field name * @return the field type */ public Class<?> getFieldType(String name) { List<FieldDefinition> list = new ArrayList<>(fields); list.addAll(gettersOnly); for (FieldDefinition f : list) if (f.name.equals(name)) return f.type; throw new RuntimeException( "Field " + name + " not found in " + constructor.getDeclaringClass().getSimpleName()); } /** * Should be called after all definitions have been registered on startup allowing them to be used concurrently w/o synchronization since they are immutable. */ public static void lock() { definitions = Collections.unmodifiableMap(definitions); classIds = Collections.unmodifiableMap(classIds); locked = true; } /** * Creates and registers the definition * @param cls class to register * @return assigned ID */ public static int register(Class<?> cls) { SerializationDefinition def = definitions.get(cls); if (def == null) def = new SerializationDefinition(cls); def.id = classId; classIds.put(classId++, def); return def.id; } private SerializationDefinition(Class<?> cls) { definitions.put(cls, this); if (cls.getName().startsWith("java")) throw new RuntimeException("System classes are not supported: " + cls.getSimpleName()); try { constructor = cls.getConstructor(); } catch (NoSuchMethodException e) { throw new RuntimeException("Missing no-arg constructor: " + cls.getSimpleName()); } serializingSetter = ReflectionUtils.findMethod(cls, "setSerializing", boolean.class); for (Class<?> c = cls; c != null && !c.equals(Object.class); c = c.getSuperclass()) { for (Field field : c.getDeclaredFields()) { if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) continue; FieldDefinition fd = new FieldDefinition(); fd.name = field.getName(); fd.type = field.getType(); if (fd.type.isPrimitive()) throw new RuntimeException("Primitives are not supported: " + fd.type.getSimpleName()); Calculated calc = field.getAnnotation(Calculated.class); if (!fd.type.equals(Integer.class) && !fd.type.equals(Long.class) && !fd.type.equals(Double.class) && !fd.type.equals(Boolean.class) && !fd.type.equals(Date.class) && !fd.type.equals(String.class)) if (fd.type.equals(List.class) || fd.type.equals(Set.class)) { SerializedCollection sc = field.getAnnotation(SerializedCollection.class); if (sc == null) throw new RuntimeException( cls.getSimpleName() + "." + fd.name + " is missing @SerializedCollection"); if (calc != null) throw new RuntimeException(cls.getSimpleName() + "." + fd.name + " cannot have a calculator because it is a collection"); fd.collectionType = sc.type(); fd.primitive = fd.collectionType.equals(Integer.class) || fd.collectionType.equals(Long.class) || fd.collectionType.equals(Double.class) || fd.collectionType.equals(Boolean.class) || fd.collectionType.equals(Date.class) || fd.collectionType.equals(String.class); if (!fd.primitive) { if (cls.getName().startsWith("java")) throw new RuntimeException(cls.getSimpleName() + "." + fd.name + ": system collection types are not supported: " + fd.collectionType.getSimpleName()); if (!definitions.containsKey(fd.collectionType)) new SerializationDefinition(fd.collectionType); } } else { if (cls.getName().startsWith("java")) throw new RuntimeException( "System classes are not supported: " + fd.type.getSimpleName()); if (!definitions.containsKey(fd.type)) new SerializationDefinition(fd.type); } else fd.primitive = true; try { fd.accessor = c.getMethod(PropertyAccessor.methodName("get", fd.name)); } catch (NoSuchMethodException e) { throw new RuntimeException(cls.getSimpleName() + "." + fd.name + " is missing getter"); } try { fd.mutator = c.getMethod(PropertyAccessor.methodName("set", fd.name), fd.type); } catch (NoSuchMethodException e) { throw new RuntimeException(cls.getSimpleName() + "." + fd.name + " is missing setter"); } if (calc != null) fd.calculator = new SpelExpressionParser().parseExpression(calc.value()); fields.add(fd); } for (Method method : c.getDeclaredMethods()) if (Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers()) && method.getName().startsWith("get") && method.isAnnotationPresent(SerializedGetter.class)) { FieldDefinition fd = new FieldDefinition(); fd.name = method.getName().substring(3); fd.name = fd.name.substring(0, 1).toLowerCase() + fd.name.substring(1); fd.type = method.getReturnType(); fd.primitive = fd.type != null && (fd.type.equals(Integer.class) || fd.type.equals(Long.class) || fd.type.equals(Double.class) || fd.type.equals(Boolean.class) || fd.type.equals(Date.class) || fd.type.equals(String.class)); if (!fd.primitive) throw new RuntimeException("Not compact-serializable getter type: " + (fd.type == null ? "void" : fd.type.getSimpleName())); fd.accessor = method; gettersOnly.add(fd); } } } /** * Universal bean constructor * @return the created bean */ public Object newInstance() { try { return constructor.newInstance(); } catch (Exception e) { throw new RuntimeException("Constructor " + constructor.getDeclaringClass().getSimpleName() + "." + constructor.getName() + " invocation error: " + e.getMessage(), e); } } /** * Externalizable helper - should be used in Externalizable implementations. * @param out output * @param bean bean */ public void write(ObjectOutput out, Object bean) { DataStream ds = new DataStream(); try { write(ds, bean); byte[] data = ds.getData(); out.writeInt(data.length); out.write(data); } catch (IOException e) { throw new RuntimeException(e); } finally { ds.close(); } } /** * Externalizable helper - should be used in Externalizable implementations. * @param in input * @param bean bean */ public void read(ObjectInput in, Object bean) { DataStream ds = null; try { int length = in.readInt(); byte[] data = new byte[length]; in.readFully(data); ds = new DataStream(data); read(ds, bean); } catch (IOException e) { throw new RuntimeException(e); } finally { if (ds != null) ds.close(); } } /** * Normal stream serialization * @param stream the stream * @param bean the bean */ public void write(DataStream stream, Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); for (FieldDefinition fd : fields) { Object value = invokeMethod(fd.accessor, bean); if (fd.collectionType != null) { if (value == null) stream.writeInteger(null); else { Collection<?> collection = (Collection<?>) value; DataStream newStream = new DataStream(); newStream.writeInteger(collection.size()); for (Object o : collection) write(newStream, o, fd.collectionType); stream.writeBytes(newStream); newStream.close(); } } else write(stream, value, fd.type); } } private void write(DataStream stream, Object value, Class<?> type) { if (type.equals(Integer.class)) stream.writeInteger((Integer) value); else if (type.equals(Long.class)) stream.writeLong((Long) value); else if (type.equals(Double.class)) stream.writeDouble((Double) value); else if (type.equals(Boolean.class)) stream.writeBoolean((Boolean) value); else if (type.equals(Date.class)) stream.writeDate((Date) value); else if (type.equals(String.class)) stream.writeString((String) value); else { if (value == null) stream.writeInteger(null); else { DataStream newStream = new DataStream(); definitions.get(type).write(newStream, value); stream.writeBytes(newStream); newStream.close(); } } } /** * External field-level writer serialization: used by Hazelcast's Portable implementation and Mongo serialization. * Serializes all top level fields plus serialized getters. Collections and sub-objects are serialized as binary data (byte arrays). * @param writer the field writer * @param bean the bean */ public void write(ExternalWriter writer, Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); for (FieldDefinition fd : fields) { Object value = invokeMethod(fd.accessor, bean); if (fd.type.equals(Integer.class)) writer.writeInteger(fd.name, (Integer) value); else if (fd.type.equals(Long.class)) writer.writeLong(fd.name, (Long) value); else if (fd.type.equals(Double.class)) writer.writeDouble(fd.name, (Double) value); else if (fd.type.equals(Boolean.class)) writer.writeBoolean(fd.name, (Boolean) value); else if (fd.type.equals(Date.class)) writer.writeDate(fd.name, (Date) value); else if (fd.type.equals(String.class)) writer.writeString(fd.name, (String) value); else if (fd.collectionType != null) { if (value == null) writer.writeBytes(fd.name, null); else { Collection<?> collection = (Collection<?>) value; DataStream newStream = new DataStream(); newStream.writeInteger(collection.size()); for (Object member : collection) write(newStream, member, fd.collectionType); writer.writeBytes(fd.name, newStream.getData()); newStream.close(); } } else if (value == null) writer.writeBytes(fd.name, null); else { DataStream newStream = new DataStream(); definitions.get(fd.type).write(newStream, value); writer.writeBytes(fd.name, newStream.getData()); newStream.close(); } } for (FieldDefinition fd : gettersOnly) { Object value = invokeMethod(fd.accessor, bean); if (fd.type.equals(Integer.class)) writer.writeInteger(fd.name, (Integer) value); else if (fd.type.equals(Long.class)) writer.writeLong(fd.name, (Long) value); else if (fd.type.equals(Double.class)) writer.writeDouble(fd.name, (Double) value); else if (fd.type.equals(Boolean.class)) writer.writeBoolean(fd.name, (Boolean) value); else if (fd.type.equals(Date.class)) writer.writeDate(fd.name, (Date) value); else if (fd.type.equals(String.class)) writer.writeString(fd.name, (String) value); } } public interface MapFactory { Map<String, Object> create(); } public interface CollectionFactory { Collection<Object> create(); } /** * Serialize bean into a map (used by Mongo and similar hierarchical databases) * @param mapFactory map factory * @param collectionFactory collection factory * @param bean bean to serialize * @return the map */ public Map<String, Object> write(MapFactory mapFactory, CollectionFactory collectionFactory, Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); Map<String, Object> result = mapFactory.create(); for (FieldDefinition fd : fields) { Object value = invokeMethod(fd.accessor, bean); if (value == null) continue; if (fd.collectionType != null) { Collection<Object> dest = collectionFactory.create(); Collection<?> src = (Collection<?>) value; if (fd.primitive) for (Object o : src) dest.add(o); else { SerializationDefinition def = get(fd.collectionType); for (Object o : src) dest.add(def.write(mapFactory, collectionFactory, o)); } result.put(fd.name, dest); } else if (fd.primitive) result.put(fd.name, value); else result.put(fd.name, get(fd.type).write(mapFactory, collectionFactory, value)); } for (FieldDefinition fd : gettersOnly) { Object value = invokeMethod(fd.accessor, bean); if (value != null) result.put(fd.name, value); } return result; } /** * Deserialize bean from a map (used by Mongo and similar hierarchical databases) * @param map map * @param bean bean to populate */ @SuppressWarnings("unchecked") public void read(Map<String, Object> map, Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); if (serializingSetter != null) invokeMethod(serializingSetter, bean, true); for (FieldDefinition fd : fields) { Object value = map.get(fd.name); if (value == null) continue; if (fd.collectionType != null) { Collection<Object> collection = fd.type.equals(List.class) ? new ArrayList<Object>() : new HashSet<Object>(); if (fd.primitive) for (Object o : (Collection<?>) value) collection.add(o); else { SerializationDefinition def = get(fd.collectionType); for (Object o : (Collection<?>) value) { Object m = def.newInstance(); def.read((Map<String, Object>) o, m); collection.add(m); } } value = collection; } else if (!fd.primitive) { SerializationDefinition def = get(fd.type); Object m = def.newInstance(); def.read((Map<String, Object>) value, m); value = m; } invokeMethod(fd.mutator, bean, value); } if (serializingSetter != null) invokeMethod(serializingSetter, bean, false); } /** * Normal stream deserialization * @param stream the stream * @param bean the bean */ public void read(DataStream stream, Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); if (serializingSetter != null) invokeMethod(serializingSetter, bean, true); for (FieldDefinition fd : fields) if (fd.collectionType != null) { DataStream newStream = stream.readBytes(); Collection<Object> collection = null; if (newStream != null) { collection = fd.type.equals(List.class) ? new ArrayList<Object>() : new HashSet<Object>(); int size = newStream.readInteger(); for (int i = 0; i < size; i++) { Object member = read(newStream, fd.collectionType); collection.add(member); } newStream.close(); } invokeMethod(fd.mutator, bean, collection); } else invokeMethod(fd.mutator, bean, read(stream, fd.type)); if (serializingSetter != null) invokeMethod(serializingSetter, bean, false); } private Object read(DataStream stream, Class<?> type) { if (type.equals(Integer.class)) return stream.readInteger(); if (type.equals(Long.class)) return stream.readLong(); if (type.equals(Double.class)) return stream.readDouble(); if (type.equals(Boolean.class)) return stream.readBoolean(); if (type.equals(Date.class)) return stream.readDate(); if (type.equals(String.class)) return stream.readString(); else { DataStream newStream = stream.readBytes(); if (newStream == null) return null; SerializationDefinition def = definitions.get(type); Object bean = def.newInstance(); def.read(newStream, bean); newStream.close(); return bean; } } /** * External field-level reader deserialization: used by Hazelcast's Portable implementation and Mongo serialization. * Serializes all top level fields plus serialized getters. Collections and sub-objects are serialized as binary data (byte arrays). * @param reader the field reader * @param bean the bean */ public void read(ExternalReader reader, Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); if (serializingSetter != null) invokeMethod(serializingSetter, bean, true); for (FieldDefinition fd : fields) if (fd.type.equals(Integer.class)) invokeMethod(fd.mutator, bean, reader.readInteger(fd.name)); else if (fd.type.equals(Long.class)) invokeMethod(fd.mutator, bean, reader.readLong(fd.name)); else if (fd.type.equals(Double.class)) invokeMethod(fd.mutator, bean, reader.readDouble(fd.name)); else if (fd.type.equals(Boolean.class)) invokeMethod(fd.mutator, bean, reader.readBoolean(fd.name)); else if (fd.type.equals(Date.class)) invokeMethod(fd.mutator, bean, reader.readDate(fd.name)); else if (fd.type.equals(String.class)) invokeMethod(fd.mutator, bean, reader.readString(fd.name)); else if (fd.collectionType != null) { byte[] data = reader.readBytes(fd.name); Collection<Object> collection = null; if (data != null) { DataStream newStream = new DataStream(data); collection = fd.type.equals(List.class) ? new ArrayList<Object>() : new HashSet<Object>(); for (int i = 0, size = newStream.readInteger(); i < size; i++) { Object member = read(newStream, fd.collectionType); collection.add(member); } newStream.close(); } invokeMethod(fd.mutator, bean, collection); } else { byte[] data = reader.readBytes(fd.name); Object value = null; if (data != null) { DataStream newStream = new DataStream(data); SerializationDefinition def = definitions.get(fd.type); value = def.newInstance(); def.read(newStream, value); newStream.close(); } invokeMethod(fd.mutator, bean, value); } if (serializingSetter != null) invokeMethod(serializingSetter, bean, false); } private Object invokeMethod(Method method, Object bean, Object... args) { try { return method.invoke(bean, args); } catch (Exception e) { throw new RuntimeException("Method " + method.getDeclaringClass().getSimpleName() + "." + method.getName() + " invocation error: " + e.getMessage(), e); } } /** * DataStream-based cloning. * @param bean bean to clone * @return cloned bean */ @SuppressWarnings("unchecked") public <T> T clone(T bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); DataStream ds = new DataStream(); try { write(ds, bean); } finally { ds.close(); } T result = (T) newInstance(); ds = new DataStream(ds.getData()); try { read(ds, result); } finally { ds.close(); } return result; } public void calculate(Object bean) { if (!locked) throw new RuntimeException("Lock the definitions after creating all of them at startup"); for (FieldDefinition fd : fields) { if (fd.calculator != null) invokeMethod(fd.mutator, bean, fd.calculator.getValue(new SpringELCtx(bean))); if (fd.collectionType != null && !fd.primitive) { Collection<?> collection = (Collection) invokeMethod(fd.accessor, bean); if (collection != null) for (Object member : collection) SerializationDefinition.get(fd.collectionType).calculate(member); } else if (!fd.primitive) { Object subObject = invokeMethod(fd.accessor, bean); if (subObject != null) SerializationDefinition.get(fd.type).calculate(subObject); } } } }