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 }