View Javadoc

1   package org.lsst.ccs.bus;
2   
3   
4   import java.beans.ConstructorProperties;
5   import java.io.*;
6   import java.lang.annotation.Annotation;
7   import java.lang.reflect.Constructor;
8   import java.lang.reflect.Field;
9   import java.lang.reflect.Modifier;
10  import java.math.BigDecimal;
11  import java.util.*;
12  import java.util.concurrent.ConcurrentHashMap;
13  
14  import static org.lsst.ccs.bus.KeyData.CodedData;
15  
16  /**
17   * utilities to code/decode DataStatus_Deprecated.
18   * <p/>
19   * TODO: this code is in a circular dependency with the KeyData class.
20   * <p/>
21   * TODO! make this class package friendly
22   *
23   * @author bamade
24   */
25  // Date: 20/12/2013
26  
27  
28  public class StatusCodec {
29      /**
30       * Immutable objects of this class describe what we can do with instances of a class.
31       * They are kept in a local cache to fasten object encoding/decoding.
32       */
33      static class ClassDescriptor {
34          /**
35           * can we decompose objects of this class in a list of key-values ?
36           */
37          final boolean decomposable;
38          /**
39           * do we need to serialize the object in a byte array (like a Marshalled object)?
40           * This can happen :
41           * <UL>
42           * <LI/> if and only if the object is <TT>Serializable</TT>
43           * <LI/> if the object is not <TT>decomposable</TT>
44           * <LI/> if the object is <TT>decomposable</TT> but has no usable constructor.
45           * </UL>
46           */
47          final boolean toCrystallize;
48          /**
49           * if this object has a usable constructor here it is.
50           * Note that it can be private or protected.
51           * Can be null if the Class is not locally reachable by the ClassLoader
52           * (when decoding)
53           */
54          final Constructor usableCtor;
55          /**
56           * if true use the no-arg ctor, if false use <TT>ConstructorProperties</TT>
57           * annotations or new java 8 parameter names introspection.
58           */
59          final boolean isNoargCtor;
60          /**
61           * The super-class.
62           * may be null when decoding (class not locally reachable by ClassLoader)
63           */
64          final Class superClass;
65          //final List<Field> fieldList;// deprecated and mapFields replaces by a LinkedHashMap
66          /**
67           * The coding needs the list of fields and their name (when encoding key-value pairs).
68           * the decoding needs to link a field to a name (hence the Map).
69           * So a LinkedHashMap provides both access through list and map.
70           */
71          final LinkedHashMap<String, Field> mapFields;
72  
73          /**
74           * simple constructor for immutable value object
75           *
76           * @param decomposable
77           * @param toCrystallize
78           * @param usableCtor
79           * @param superClass
80           * @param fieldList
81           */
82          ClassDescriptor(boolean decomposable, boolean toCrystallize, Constructor usableCtor, boolean useNoargCtor, Class superClass, List<Field> fieldList) {
83              this.decomposable = decomposable;
84              this.toCrystallize = toCrystallize;
85              this.usableCtor = usableCtor;
86              this.isNoargCtor = useNoargCtor;
87              this.superClass = superClass;
88              //this.fieldList = fieldList;
89              if (fieldList != null) {
90                  this.mapFields = new LinkedHashMap<>();
91                  for (Field field : fieldList) {
92                      mapFields.put(field.getName(), field);
93                  }
94              } else {
95                  mapFields = null;
96              }
97          }
98  
99          @Override
100         public String toString() {
101             return "ClassDescriptor{" +
102                     "decomposable=" + decomposable +
103                     ", toCrystallize=" + toCrystallize +
104                     ", usableCtor=" + usableCtor +
105                     ", superClass=" + superClass +
106                     ", fieldList=" + mapFields.values() +
107                     '}';
108         }
109     }
110 
111     /**
112      * local cache for <TT>ClassDescriptor</TT> objects
113      */
114     static ConcurrentHashMap<Class, ClassDescriptor> classMap = new ConcurrentHashMap<>();
115     /**
116      * predefined list of types where values won't be encoded either by "crystalizing"
117      * or with a list of key-value.
118      */
119     public static final Class[] WELL_KNOWN_TYPES = {
120             Boolean.class,
121             Boolean.TYPE,
122             Integer.class,
123             Integer.TYPE,
124             Float.class,
125             Float.TYPE,
126             Double.class,
127             Double.TYPE,
128             String.class,
129             byte[].class,
130             boolean[].class,
131             int[].class,
132             float[].class,
133             double[].class,
134             String[].class,
135             // dangerous : Serializable[].class,
136             BitSet.class,
137             BigDecimal.class,
138     };
139     /**
140      * This descriptor is used to describe data that will not be encoded (either with a key-value list or
141      * with a crystallized byte array).
142      */
143     public static final ClassDescriptor NULL_DESCRIPTOR = new ClassDescriptor(false, false, null, false, null, null) {
144         public String toString() {
145             return "NULL_DESCRIPTOR";
146         }
147     };
148 
149     /**
150      * This decriptor is common to all enums.
151      * Enums will be encoded in a special way: the key-value list will have an empty key and a String value
152      * that represents the enums instance, the object will also be crystallized (this is necessary if the client side
153      * does not have the Enum class at hand).
154      */
155     public static final ClassDescriptor ENUM_DESCRIPTOR = new ClassDescriptor(true, true, null, false, null, null) {
156         public String toString() {
157             return "ENUM_DESCRIPTOR";
158         }
159     };
160 
161     /**
162      * this initialisation block will mark all WELL_KNOWN_TYPES to be linked to a NULL_DESCRIPTOR
163      */
164     static {
165         for (Class clazz : WELL_KNOWN_TYPES) {
166             classMap.put(clazz, NULL_DESCRIPTOR);
167         }
168     }
169 
170     /**
171      * gets a <TT>ClassDescriptor</TT> object for a given class.
172      * The descriptor cache is read and if no descriptor found a new one is built
173      * and inserted into the cache.
174      * <BR/>
175      * TODO: is it necessary to synchronize?
176      *
177      * @param clazz
178      * @return
179      */
180     static ClassDescriptor getDescriptorFor(Class clazz) {
181         ClassDescriptor first = classMap.get(clazz);
182         if (first == null) {
183             first = buildDescriptorFor(clazz);
184             classMap.put(clazz, first);
185         }
186         return first;
187     }
188 
189     /**
190      * Builds a <TT>ClassDescriptor</TT>a for a given class
191      *
192      * @param clazz
193      * @return
194      * @implSpec <UL>
195      * <LI>if it is an enum class the common <TT>ENUM_DESCRIPTOR</TT> is returned</LI>
196      * <LI> does the class have a usable constructor? if yes it is kept and made accessible</LI>
197      * <LI> we get the super class and if it is not Object :</LI>
198      * <UL>
199      * <LI> we get the descriptor for the superClass</LI>
200      * <LI> if the descriptor for the superclass notes that it is to crystallize then the current descriptor
201      * is not decomposable and is to crystallize. This may be changed in further versions when we decide that
202      * crystallization is not incompatible with list decomposition</LI>
203      * <LI> the fields of the superclass are added to the fields descriptions of the current class</LI>
204      * </UL>
205      * <LI> we add the fields to the list of fields in the descriptor.
206      * if a field which is not an enum does not have a usable constructor then the current object is to be
207      * crystallized. If a field (which is not an enum) is to be crystallized then the current object is
208      * to be crystallized  </LI>
209      * </UL>
210      */
211     private static synchronized ClassDescriptor buildDescriptorFor(Class clazz) {
212         // if enum
213         if (clazz.isEnum()) {
214             return ENUM_DESCRIPTOR;
215         }
216         // is it known to everybody ?
217         //todo: if startsWith "java" && Serializable -> NULL-DESCRIPTOR ?
218         // todo: but beware of profiles in java8
219 
220         Constructor usableCtor = null;
221         boolean isNoArgCtor = false;
222         try { // first we go after a no-arg ctor
223             usableCtor = clazz.getDeclaredConstructor();
224             usableCtor.setAccessible(true);
225             isNoArgCtor = true;
226         } catch (NoSuchMethodException e) {
227             //then we go after ConstructorProperties
228             Constructor[] ctors = clazz.getConstructors();
229             for (Constructor ctor : ctors) {
230                 Annotation annotation = ctor.getAnnotation(ConstructorProperties.class);
231                 if (annotation != null) {
232                     usableCtor = ctor;
233                     usableCtor.setAccessible(true);
234                     isNoArgCtor = false;
235                     break;
236                 }
237             }
238         }
239         boolean crystallize = (usableCtor == null);
240         // introspect the class
241         ArrayList<Field> listFields = new ArrayList<>();
242         // get super class
243         // if supserClass is not Object
244         // call recursively getDescriptorFor
245         // and complete the FieldList
246         Class superClazz = clazz.getSuperclass();
247         if(superClazz != null) { // the type is itself Object if superclass is null!!!
248             if (!Object.class.equals(superClazz)) {
249                 ClassDescriptor superDescriptor =
250                         getDescriptorFor(superClazz);
251                 if (superDescriptor.toCrystallize) {
252                     //TODO : change that in future versions ?
253                     return new ClassDescriptor(false, true, usableCtor, isNoArgCtor, superClazz, null);
254                 } else {
255                     //listFields.addAll(superDescriptor.fieldList);
256                     listFields.addAll(superDescriptor.mapFields.values());
257                 }
258             }
259         }
260         Field[] fields = clazz.getDeclaredFields();
261         for (Field field : fields) {
262             int modifiers = field.getModifiers();
263             if (Modifier.isStatic(modifiers) ) {
264                 //if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) {
265                 continue;
266             }
267             /*
268             THis code has been added because some classes (such as ArrayList) use transient only for implementation reasons.
269             so the transient field cannot be used for encoding/decoding ....  this is to circumvent this feature...
270             */
271             // if transient and of complex type: skip? examples : Thread, IO ... and so on (transient and not Serializable)
272             if(Modifier.isTransient(modifiers)) {
273              if(!   Serializable.class.isAssignableFrom( field.getType()))  {
274                  continue;
275              }
276             }
277             Class fieldType = field.getType();
278             ClassDescriptor descriptor = getDescriptorFor(fieldType);
279             // TO BE CHANGED IN THE FUTURE
280             if (descriptor != NULL_DESCRIPTOR && descriptor != ENUM_DESCRIPTOR) {
281                 if (descriptor.toCrystallize) {
282                     //TODO: not necessarily true: we may crystallize elements of a list?
283                     crystallize = true;
284                     break;// todo: same remark as above
285                 }
286                 if (descriptor.usableCtor == null) {
287                     crystallize = true;
288                     //usableCtor = null ;
289                 }
290                 /*
291                 */
292             }
293             field.setAccessible(true);
294             listFields.add(field);
295         }
296         ClassDescriptor res = new ClassDescriptor(true, crystallize, usableCtor, isNoArgCtor, superClazz, listFields);
297         return res;
298     }
299 
300     /**
301      * creates a list of key-value pairs (<TT>KeyData</TT>) to describe the content of an Object.
302      * each value is encoded (but should not throw any exception at this stage of the implementation)
303      *
304      * @param object
305      * @param descriptor
306      * @return
307      */
308     static List<KeyData> listEncode(Object object, ClassDescriptor descriptor) {
309         Collection<Field> descriptorValues = descriptor.mapFields.values();
310         //List<KeyData> res = new ArrayList<>(descriptor.fieldList.size());
311         List<KeyData> res = new ArrayList<>(descriptorValues.size());
312         for (Field field : descriptorValues) {
313             //for (Field field : descriptor.fieldList) {
314             try {
315                 Object val = encode(field.get(object));
316                 res.add(new KeyData(field.getName(), val));
317             } catch (Exception e) {
318                 //todo: to be changed if we authorize decomposable+toCrystallize
319                 //TODO LOG!
320                 System.err.println("OOPS " + e);
321             }
322         }
323         return res;
324     }
325 
326     /**
327      * Encodes an object.
328      * May build a list of key-value pairs and/or a crystallized value of this object if it is not
329      * of a "well known type".
330      *
331      * @param value
332      * @return the argument if it is of a "well known type" or a <TT>CodedData</TT> object
333      * @throws IOException
334      */
335     public static Object encode(Object value) throws IOException {
336         if(value == null) return null ;
337         Class clazz = value.getClass();
338         ClassDescriptor descriptor = getDescriptorFor(clazz);
339         if (descriptor == NULL_DESCRIPTOR) {
340             return value;
341         }
342         if (descriptor == ENUM_DESCRIPTOR) {
343             CodedData codedEnum = new CodedData(clazz.getName());
344             codedEnum.content = crystallize(value);
345             List<KeyData> res = new ArrayList<>(1);
346             res.add(new KeyData("", String.valueOf(value)));
347             codedEnum.listKV = res;
348             return codedEnum;
349         }
350         CodedData codedRes = new CodedData(clazz.getName());
351         //if ((descriptor.usableCtor == null) && (Serializable.class.isAssignableFrom(clazz))) {
352         //TODO: if ! decomposable ?
353         // TODO : this class can be read but not deserialized
354         //}
355         if (descriptor.decomposable) {
356             // decompose
357             codedRes.listKV = listEncode(value, descriptor);
358         }
359         if (descriptor.toCrystallize) {
360             codedRes.content = crystallize(value);
361 
362         }
363         return codedRes;
364     }
365 
366     /**
367      * a creates a byte array containing a serialized object.
368      *
369      * @param value
370      * @return
371      * @throws IOException if the argument is not <TT>Serializable</TT>
372      */
373     static byte[] crystallize(Object value) throws IOException {
374         ByteArrayOutputStream bos = new ByteArrayOutputStream();
375         ObjectOutputStream oos = new ObjectOutputStream(bos);
376         oos.writeObject(value);
377         return bos.toByteArray();
378     }
379 
380     /**
381      * Decodes an object.
382      * If the value is of a "well-known type" it is returned if it is <TT>CodedData</TT>
383      * it is either deserialized from a crystallized state or rebuilt from the list of key-value pairs
384      *
385      * @param value
386      * @return
387      * @throws Exception
388      */
389     public static Object decode(Object value) throws Exception {
390         if(value == null) return null ;
391         if (value instanceof CodedData) {
392             CodedData codedData = (CodedData) value;
393             String className = codedData.className;
394             Class clazz = Class.forName(className);
395             ClassDescriptor descriptor = getDescriptorFor(clazz);
396             // now can we build from ctor or deserialize?
397             if (descriptor.toCrystallize) {
398                 byte[] array = (byte[]) codedData.content;
399                 ByteArrayInputStream bis = new ByteArrayInputStream(array);
400                 ObjectInputStream ois = new ObjectInputStream(bis);
401                 return ois.readObject();
402             } else if (descriptor.usableCtor != null) {
403                 Object res = null;
404                 if (descriptor.isNoargCtor) {
405                     res = descriptor.usableCtor.newInstance();
406                     // which is best hashcode for descriptor or just read in sequence?
407                     for (KeyData keyValue : codedData.listKV) {
408                         String key = keyValue.getKey();
409                         Field field = descriptor.mapFields.get(key);
410                         if (field != null) {
411                             Object valForField = decode(keyValue.getRawValue());
412                             field.set(res, valForField);
413                         }
414                     }
415                 } else {
416                     //TODO get the information once in the descriptor instead of doing it always again
417                     ConstructorProperties ctorProps = (ConstructorProperties) descriptor.usableCtor.getAnnotation(ConstructorProperties.class);
418                     String[] names = ctorProps.value();
419                     Object[] args = new Object[names.length];
420                     for (int ix = 0; ix < names.length; ix++) {
421                         String name = names[ix];
422                         //TODO inefficient and copy of KVList! change
423                         for (KeyData keyData : codedData.listKV) {
424                             if (keyData.getKey().equals(name)) {
425                                 args[ix] = decode(keyData.getRawValue());
426                                 break;
427                             }
428                         }
429                     }
430                     res = descriptor.usableCtor.newInstance(args);
431                 }
432                 return res;
433 
434             } else {
435                 //TODO throw exception
436                 throw new IllegalArgumentException("no way to recompose the object :" + value);
437             }
438 
439         }
440         return value;
441     }
442 
443     /**
444      * builds a key-value list from a coded composite data.
445      * the key are assigned a composite name that start with the "rootName".
446      *
447      * @param rootName
448      * @param codedData
449      * @return
450      */
451     static List<KeyData> simpleKeyValueList(String rootName, CodedData codedData) {
452         List<KeyData> resList = new ArrayList<>();
453         for (KeyData keyVal : codedData.listKV) {
454             Object val = keyVal.getRawValue();
455             String key = keyVal.getKey();
456             String longName;
457             if (key.length() == 0) {
458                 longName = rootName;
459             } else {
460                 longName = rootName + '/' + key;
461             }
462             if (val instanceof CodedData) {
463                 List<KeyData> contentList = asSimpleKeyValueList(longName, (CodedData) val);
464                 resList.addAll(contentList);
465 
466             } else {
467                 resList.add(new KeyData(longName, val));
468             }
469         }
470         return resList;
471     }
472 
473     /**
474      * @param keyName
475      * @param data
476      * @return
477      */
478     public static List<KeyData> asSimpleKeyValueList(String keyName, Object data) {
479         if (data instanceof CodedData) {
480             return simpleKeyValueList(keyName, (CodedData) data);
481         }
482         List<KeyData> res = new ArrayList<>(1);
483         res.add(new KeyData(keyName, data));
484         return res;
485     }
486 
487 
488 }