View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package com.addthis.codec.binary;
15  
16  import javax.annotation.Nullable;
17  
18  import java.io.InputStream;
19  import java.io.OutputStream;
20  import java.io.PrintWriter;
21  import java.io.StringWriter;
22  
23  import java.lang.reflect.Array;
24  import java.lang.reflect.Modifier;
25  
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  import java.util.concurrent.atomic.AtomicInteger;
33  import java.util.concurrent.atomic.AtomicLong;
34  
35  import com.addthis.basis.util.Bytes;
36  
37  import com.addthis.codec.Codec;
38  import com.addthis.codec.codables.Codable;
39  import com.addthis.codec.codables.ConcurrentCodable;
40  import com.addthis.codec.codables.SuperCodable;
41  import com.addthis.codec.reflection.CodableClassInfo;
42  import com.addthis.codec.reflection.CodableFieldInfo;
43  import com.addthis.codec.reflection.Fields;
44  
45  import com.google.common.base.Strings;
46  
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  /**
51   * Like CodecBin1 but does not support upgrade/downgrade of objects to prev/later versions.
52   * Stores all fields, does not use a map.  This is generally faster while using less space.
53   */
54  public final class CodecBin2 implements Codec {
55  
56      private static final Logger log = LoggerFactory.getLogger(CodecBin2.class);
57  
58      public static final CodecBin2 INSTANCE            = new CodecBin2(false);
59      public static final int       CODEC_VERSION       = 2;
60  
61      private final boolean charstring;
62  
63      private CodecBin2(boolean cs) { this.charstring = cs; }
64  
65      @Override
66      public byte[] encode(Object obj) throws Exception {
67          return encodeBytes(obj);
68      }
69  
70      @Override
71      public Object decode(Class type, byte[] data) throws Exception {
72          return decode(type.newInstance(), data);
73      }
74  
75      @Override
76      public Object decode(Object shell, byte[] data) throws Exception {
77          return decodeBytes(shell, data);
78      }
79  
80      @Override
81      public boolean storesNull(byte[] data) {
82          return (data.length == 5) && (data[4] == 0);
83      }
84  
85      public static byte[] encodeBytes(Object object) throws Exception {
86          BufferOut buf = new BufferOut();
87          Bytes.writeInt(CODEC_VERSION, buf.out());
88          INSTANCE.encodeObject(object, buf);
89          return buf.out.toByteArray();
90      }
91  
92      @Nullable @SuppressWarnings("unchecked")
93      public static Object decodeBytes(Object object, byte[] data) throws Exception {
94          BufferIn buf = new BufferIn(data);
95          int ver = Bytes.readInt(buf.in);
96          require(ver == CODEC_VERSION, "version mismatch " + ver + " != " + CODEC_VERSION);
97          return INSTANCE.decodeObject(Fields.getClassFieldMap(object.getClass()), object, buf);
98      }
99  
100     private void encodeObject(Object object, BufferOut buf)
101             throws Exception {
102         log.trace("encodeObject: {} {}", object, buf);
103         if (object == null) {
104             buf.out.write(0);
105             return;
106         }
107         boolean lock = object instanceof ConcurrentCodable;
108         if (lock) {
109             ((ConcurrentCodable) object).encodeLock();
110         }
111         try {
112             if (object instanceof SuperCodable) {
113                 ((SuperCodable) object).preEncode();
114             }
115             Class objectClass = object.getClass();
116             CodableClassInfo classInfo = Fields.getClassFieldMap(objectClass);
117             if (objectClass.isArray()) {
118                 encodeArray(object, objectClass, buf);
119             } else if ((classInfo.size() == 0) && !(object instanceof Codable)) {
120                 encodeNative(object, buf);
121             } else {
122                 buf.out.write(1);
123                 writeStringHelper(classInfo.getClassName(object), buf.out());
124                 for (CodableFieldInfo field : classInfo.values()) {
125                     encodeField(field.get(object), field, buf);
126                 }
127             }
128         } finally {
129             if (lock) {
130                 ((ConcurrentCodable) object).encodeUnlock();
131             }
132         }
133     }
134 
135     @Nullable private Object decodeObject(Class<?> type, BufferIn buf) throws Exception {
136         log.trace("decodeObject: {} {}", type, buf);
137         if (Fields.isNative(type)) {
138             return decodeNative(type, buf);
139         } else {
140             CodableClassInfo classInfo = Fields.getClassFieldMap(type);
141             return decodeObject(classInfo, null, buf);
142         }
143     }
144 
145     @Nullable private Object decodeObject(CodableClassInfo classInfo, @Nullable Object object, BufferIn buf) throws Exception {
146         int ck = buf.in.read();
147         if (ck == 0) {
148             return null;
149         }
150         Class<?> type = classInfo.getBaseClass();
151         log.trace("decodeObject: {} {} {}", classInfo, object, buf);
152         String stype = readStringHelper(buf.in);
153         if (!Strings.isNullOrEmpty(stype)) {
154             Class<?> atype = classInfo.getClass(stype);
155             if (type != atype) {
156                 classInfo = Fields.getClassFieldMap(atype);
157                 type = atype;
158             }
159         }
160         if (object == null) {
161             object = type.newInstance();
162         }
163         for (CodableFieldInfo field : classInfo.values()) {
164             field.set(object, decodeField(field, buf));
165         }
166         if (object instanceof SuperCodable) {
167             ((SuperCodable) object).postDecode();
168         }
169         return object;
170     }
171 
172     private void encodeArray(Object value, Class<?> type, BufferOut buf) throws Exception {
173         int len = Array.getLength(value);
174         log.trace("encodeArray: {} {} {} len={}", value, type, buf, len);
175         Bytes.writeLength(len, buf.out());
176         if ((type == byte.class) || (type == Byte.class)) {
177             buf.out.write((byte[]) value);
178         } else if ((type == int.class) || (type == Integer.class)) {
179             int[] val = (int[]) value;
180             for (int i = 0; i < len; i++) {
181                 Bytes.writeInt(val[i], buf.out());
182             }
183         } else if ((type == long.class) || (type == Long.class)) {
184             long[] val = (long[]) value;
185             for (int i = 0; i < len; i++) {
186                 Bytes.writeLong(val[i], buf.out());
187             }
188         } else if (type.isEnum()) {
189             for (int i = 0; i < len; i++) {
190                 encodeNative(Array.get(value, i).toString(), buf);
191             }
192         } else {
193             for (int i = 0; i < len; i++) {
194                 encodeObject(Array.get(value, i), buf);
195             }
196         }
197     }
198 
199     @Nullable private Object decodeArray(Class<?> type, BufferIn buf) throws Exception {
200         log.trace("decodeArray: {} {}", type, buf);
201         int len = (int) Bytes.readLength(buf.in);
202         Object value = null;
203         if (len > 0) {
204             value = Array.newInstance(type, len);
205             if ((type == byte.class) || (type == Byte.class)) {
206                 buf.in.read((byte[]) value);
207             } else if ((type == int.class) || (type == Integer.class)) {
208                 int[] val = (int[]) value;
209                 for (int i = 0; i < len; i++) {
210                     val[i] = Bytes.readInt(buf.in);
211                 }
212                 value = val;
213             } else if ((type == long.class) || (type == Long.class)) {
214                 long[] val = (long[]) value;
215                 for (int i = 0; i < len; i++) {
216                     val[i] = Bytes.readLong(buf.in);
217                 }
218                 value = val;
219             } else if (type.isEnum()) {
220                 for (int i = 0; i < len; i++) {
221                     Array.set(value, i, decodeEnum((Class<Enum>) type, buf));
222                 }
223             } else {
224                 for (int i = 0; i < len; i++) {
225                     Array.set(value, i, decodeObject(type, buf));
226                 }
227             }
228         }
229         return value;
230     }
231 
232     private void encodeField(Object value, CodableFieldInfo field, BufferOut buf) throws Exception {
233         log.trace("encodeField: {} {} {}", value, field, buf);
234         if (value != null) {
235             try {
236                 buf.out.write(1);
237                 if (field.isArray()) {
238                     encodeArray(value, field.getTypeOrComponentType(), buf);
239                 } else if (field.isNative()) {
240                     encodeNative(value, buf);
241                 } else if (field.isMap()) {
242                     Map<?, ?> map = (Map<?, ?>) value;
243                     Bytes.writeLength(map.size(), buf.out());
244                     for (Entry<?, ?> entry : map.entrySet()) {
245                         Object key = entry.getKey();
246                         encodeObject(key, buf);
247                         encodeObject(entry.getValue(), buf);
248                     }
249                 } else if (field.isCollection()) {
250                     Collection<?> coll = (Collection<?>) value;
251                     Bytes.writeLength(coll.size(), buf.out());
252                     for (Object aColl : coll) {
253                         encodeObject(aColl, buf);
254                     }
255                 } else if (field.isCodable()) {
256                     encodeObject(value, buf);
257                 } else if (field.isEnum()) {
258                     encodeNative(value.toString(), buf);
259                 } else {
260                     log.warn("[encodeField] unhandled field : {} {}", value, field);
261                 }
262             } catch (Exception ex) {
263                 log.warn("failed encoding {} class {} type {}", value, value.getClass(), field, ex);
264                 StringWriter sw = new StringWriter();
265                 PrintWriter pw = new PrintWriter(sw);
266                 ex.printStackTrace(pw);
267                 log.warn(sw.toString());
268                 throw ex;
269             }
270         } else {
271             buf.out.write(0);
272         }
273     }
274 
275     private static boolean isNotConcrete(Class<?> type) {
276         int mod = type.getModifiers();
277         return Modifier.isAbstract(mod) || Modifier.isInterface(mod);
278     }
279 
280     @SuppressWarnings("unchecked")
281     private static Map<Object, Object> newMap(Class<?> type) throws InstantiationException, IllegalAccessException {
282         return isNotConcrete(type) ? new HashMap<>() : (Map<Object, Object>) type.newInstance();
283     }
284 
285     @SuppressWarnings("unchecked")
286     private static Collection<Object> newCollection(Class<?> type, int size) throws InstantiationException, IllegalAccessException {
287         return isNotConcrete(type) ? new ArrayList<>(size) : (Collection<Object>) type.newInstance();
288     }
289 
290     @Nullable private Object decodeField(CodableFieldInfo field, BufferIn buf) throws Exception {
291         log.trace("decodeField: {} {}", field, buf);
292         int ck = buf.in.read();
293         if (ck == 0) {
294             return null;
295         }
296         Class<?> type = field.getTypeOrComponentType();
297         if (field.isArray()) {
298             return decodeArray(type, buf);
299         } else if (field.isMap()) {
300             Map<Object, Object> map = (Map<Object, Object>) newMap(type);
301             int elements = (int) Bytes.readLength(buf.in);
302             if (elements == 0) {
303                 return map;
304             }
305             // value type, assume key is String
306             Class<?> kc = field.getMapKeyClass();
307             Class<?> vc = field.getMapValueClass();
308             boolean ka = field.isMapKeyArray();
309             boolean va = field.isMapValueArray();
310             for (int i = 0; i < elements; i++) {
311                 if (ka) {
312                     if (va) {
313                         map.put(decodeArray(kc, buf), decodeArray(vc, buf));
314                     } else {
315                         map.put(decodeArray(kc, buf), decodeObject(vc, buf));
316                     }
317                 } else {
318                     if (va) {
319                         map.put(decodeObject(kc, buf), decodeArray(vc, buf));
320                     } else {
321                         map.put(decodeObject(kc, buf), decodeObject(vc, buf));
322                     }
323                 }
324             }
325             return map;
326         } else if (field.isCollection()) {
327             int elements = (int) Bytes.readLength(buf.in);
328             Collection<Object> coll = (Collection<Object>) newCollection(type, elements);
329             if (elements == 0) {
330                 return coll;
331             }
332             Class<?> vc = field.getCollectionClass();
333             boolean va = field.isCollectionArray();
334             for (int i = 0; i < elements; i++) {
335                 coll.add(va ? decodeArray(vc, buf) : decodeObject(vc, buf));
336             }
337             return coll;
338         } else if (field.isCodable()) {
339             return decodeObject(type, buf);
340         } else if (field.isEnum()) {
341             return decodeEnum((Class<Enum>) type, buf);
342         } else if (field.isNative()) {
343             return decodeNative(type, buf);
344         } else {
345             log.warn("unhandled decode {}", field);
346             return null;
347         }
348     }
349 
350     private void encodeNative(Object value, BufferOut buf) throws Exception {
351         log.trace("encodeNative: {} {}", value, buf);
352         Class<?> type = value.getClass();
353         if (type == String.class) {
354             writeStringHelper(value.toString(), buf.out());
355         } else if ((type == Integer.class) || (type == int.class)) {
356             Bytes.writeInt((Integer) value, buf.out());
357         } else if ((type == Long.class) || (type == long.class)) {
358             Bytes.writeLong((Long) value, buf.out());
359         } else if ((type == Short.class) || (type == short.class)) {
360             Bytes.writeShort((Short) value, buf.out());
361         } else if ((type == Boolean.class) || (type == boolean.class)) {
362             buf.out.write((Boolean) value ? 1 : 0);
363         } else if ((type == Float.class) || (type == float.class)) {
364             Bytes.writeInt(Float.floatToIntBits(((Float) value)), buf.out());
365         } else if ((type == Double.class) || (type == double.class)) {
366             Bytes.writeLong(Double.doubleToLongBits(((Double) value)), buf.out());
367         } else if (type == AtomicLong.class) {
368             Bytes.writeLong(((AtomicLong) value).get(), buf.out());
369         } else if (type == AtomicInteger.class) {
370             Bytes.writeInt(((AtomicInteger) value).get(), buf.out());
371         } else if (type == AtomicBoolean.class) {
372             buf.out.write(((AtomicBoolean) value).get() ? 1 : 0);
373         } else {
374             log.warn("skip native encode for {} / {}", value, value.getClass());
375         }
376     }
377 
378     private Object decodeEnum(Class<Enum> type, BufferIn buf) throws Exception {
379         String val = readStringHelper(buf.in);
380         return Enum.valueOf(type, val);
381     }
382 
383     @Nullable private Object decodeNative(Class<?> type, BufferIn buf) throws Exception {
384         Object result = null;
385         if (type == String.class) {
386             result = readStringHelper(buf.in);
387         } else if ((type == Integer.class) || (type == int.class)) {
388             result = Bytes.readInt(buf.in);
389         } else if ((type == Long.class) || (type == long.class)) {
390             result = Bytes.readLong(buf.in);
391         } else if ((type == Short.class) || (type == short.class)) {
392             result = Bytes.readShort(buf.in);
393         } else if ((type == Boolean.class) || (type == boolean.class)) {
394             result = buf.in.read() != 0 ? true : false;
395         } else if ((type == Double.class) || (type == double.class)) {
396             result = Double.longBitsToDouble(Bytes.readLong(buf.in));
397         } else if ((type == Float.class) || (type == float.class)) {
398             result = Float.intBitsToFloat(Bytes.readInt(buf.in));
399         } else if (type == AtomicLong.class) {
400             result = new AtomicLong(Bytes.readLong(buf.in));
401         } else if (type == AtomicInteger.class) {
402             result = new AtomicInteger(Bytes.readInt(buf.in));
403         } else if (type == AtomicBoolean.class) {
404             result = buf.in.read() != 0 ? new AtomicBoolean(true) : new AtomicBoolean(false);
405         } else {
406             log.warn("unhandled native decode {}", type);
407         }
408         return result;
409     }
410 
411     private static void require(boolean bool, String msg) throws Exception {
412         if (!bool) {
413             throw new Exception(msg);
414         }
415     }
416 
417     @Nullable private String readStringHelper(InputStream in) throws Exception {
418         if (charstring) {
419             return Bytes.readCharString(in);
420         } else {
421             return Bytes.readString(in);
422         }
423     }
424 
425     private void writeStringHelper(String str, OutputStream out) throws Exception {
426         if (charstring) {
427             Bytes.writeCharString(str, out);
428         } else {
429             Bytes.writeString(str, out);
430         }
431     }
432 }