001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.commons.encoding;
021
022import java.io.DataInputStream;
023import java.io.DataOutputStream;
024import java.io.IOException;
025import java.io.ObjectInputStream;
026import java.io.ObjectOutputStream;
027import java.io.Serializable;
028import java.lang.reflect.Array;
029import java.lang.reflect.Constructor;
030import java.lang.reflect.InvocationTargetException;
031import java.util.HashMap;
032import java.util.Map;
033
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037/**
038 * Typesafe writing and reading of fields, providing some level of integrity
039 * checking of encoded messages.
040 * 
041 * <p>
042 * The {@link #write(DataOutputExtended, Object)} writes out field type and then
043 * the data for that field type. The field type is represented by this
044 * enumberation, with the {@link FieldType#getIdx() index} being what is written
045 * to the stream (hence of type <tt>byte</tt> to keep small).
046 * 
047 * <p>
048 * Conversely, the {@link #read(DataInputExtended)} reads the field type and
049 * then the data for that field type.
050 */
051public abstract class FieldType<T> {
052
053    private static Logger LOG = LoggerFactory.getLogger(FieldType.class);
054
055    private static String LOG_INDENT = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
056    private static final int NULL_BIT = 64; // 2 to the 6
057
058    private static Map<Byte, FieldType<?>> cache = new HashMap<Byte, FieldType<?>>();
059    private static int next = 0;
060
061    static enum Indenting {
062        INDENT_ONLY, INDENT_AND_OUTDENT;
063    }
064
065    public static FieldType<Boolean> BOOLEAN = new FieldType<Boolean>((byte) next++, Boolean.class, Indenting.INDENT_ONLY) {
066        @Override
067        protected void doWrite(final DataOutputExtended output, final Boolean value) throws IOException {
068            try {
069                if (LOG.isDebugEnabled()) {
070                    log(this, new StringBuilder().append(value));
071                }
072                final DataOutputStream outputStream = output.getDataOutputStream();
073                outputStream.writeBoolean(value);
074            } finally {
075                if (LOG.isDebugEnabled()) {
076                    unlog(this);
077                }
078            }
079        }
080
081        @Override
082        protected Boolean doRead(final DataInputExtended input) throws IOException {
083            try {
084                final DataInputStream inputStream = input.getDataInputStream();
085                final boolean value = inputStream.readBoolean();
086                if (LOG.isDebugEnabled()) {
087                    log(this, new StringBuilder().append(value));
088                }
089                return value;
090            } finally {
091                if (LOG.isDebugEnabled()) {
092                    unlog(this);
093                }
094            }
095        }
096    };
097
098    public static FieldType<boolean[]> BOOLEAN_ARRAY = new FieldType<boolean[]>((byte) next++, boolean[].class, Indenting.INDENT_AND_OUTDENT) {
099        @Override
100        protected void doWrite(final DataOutputExtended output, final boolean[] values) throws IOException {
101            try {
102                final StringBuilder buf = new StringBuilder();
103                final DataOutputStream outputStream = output.getDataOutputStream();
104                outputStream.writeInt(values.length);
105                if (LOG.isDebugEnabled()) {
106                    buf.append("length: ").append(values.length);
107                }
108                for (int i = 0; i < values.length; i++) {
109                    outputStream.writeBoolean(values[i]);
110                    if (LOG.isDebugEnabled()) {
111                        buf.append(i == 0 ? ": " : ", ");
112                        buf.append(values[i]);
113                    }
114                }
115                if (LOG.isDebugEnabled()) {
116                    log(this, buf);
117                }
118            } finally {
119                if (LOG.isDebugEnabled()) {
120                    unlog(this);
121                }
122            }
123        }
124
125        @Override
126        protected boolean[] doRead(final DataInputExtended input) throws IOException {
127            try {
128                final StringBuilder buf = new StringBuilder();
129                final DataInputStream inputStream = input.getDataInputStream();
130                final int length = inputStream.readInt();
131                if (LOG.isDebugEnabled()) {
132                    buf.append("length: ").append(length);
133                }
134                final boolean[] values = new boolean[length];
135                for (int i = 0; i < values.length; i++) {
136                    values[i] = inputStream.readBoolean();
137                    if (LOG.isDebugEnabled()) {
138                        buf.append(i == 0 ? ": " : ", ");
139                        buf.append(values[i]);
140                    }
141                }
142                if (LOG.isDebugEnabled()) {
143                    log(this, buf);
144                }
145                return values;
146            } finally {
147                if (LOG.isDebugEnabled()) {
148                    unlog(this);
149                }
150            }
151        }
152    };
153
154    public static FieldType<Byte> BYTE = new FieldType<Byte>((byte) next++, Byte.class, Indenting.INDENT_ONLY) {
155        @Override
156        protected void doWrite(final DataOutputExtended output, final Byte value) throws IOException {
157            try {
158                if (LOG.isDebugEnabled()) {
159                    log(this, new StringBuilder().append(value));
160                }
161                final DataOutputStream outputStream = output.getDataOutputStream();
162                outputStream.writeByte(value.byteValue());
163            } finally {
164                if (LOG.isDebugEnabled()) {
165                    unlog(this);
166                }
167            }
168        }
169
170        @Override
171        protected Byte doRead(final DataInputExtended input) throws IOException {
172            try {
173                final DataInputStream inputStream = input.getDataInputStream();
174                final byte value = inputStream.readByte();
175                if (LOG.isDebugEnabled()) {
176                    log(this, new StringBuilder().append(value));
177                }
178                return value;
179            } finally {
180                if (LOG.isDebugEnabled()) {
181                    unlog(this);
182                }
183            }
184        }
185    };
186
187    public static FieldType<byte[]> BYTE_ARRAY = new FieldType<byte[]>((byte) next++, byte[].class, Indenting.INDENT_AND_OUTDENT) {
188        @Override
189        protected void doWrite(final DataOutputExtended output, final byte[] values) throws IOException {
190            try {
191                final DataOutputStream outputStream = output.getDataOutputStream();
192                final int length = values.length;
193                outputStream.writeInt(length);
194                if (LOG.isDebugEnabled()) {
195                    log(this, new StringBuilder().append("length:").append(length).append(" [BYTE ARRAY]"));
196                }
197
198                // rather than looping through the array,
199                // we take advantage of optimization built into DataOutputStream
200                outputStream.write(values);
201            } finally {
202                if (LOG.isDebugEnabled()) {
203                    unlog(this);
204                }
205            }
206        }
207
208        @Override
209        protected byte[] doRead(final DataInputExtended input) throws IOException {
210            try {
211                final DataInputStream inputStream = input.getDataInputStream();
212                final int length = inputStream.readInt();
213                if (LOG.isDebugEnabled()) {
214                    final StringBuilder msg = new StringBuilder().append("length:").append(length).append(" [BYTE ARRAY]");
215                    log(this, msg);
216                }
217
218                final byte[] bytes = new byte[length];
219                readBytes(inputStream, bytes);
220                return bytes;
221            } finally {
222                if (LOG.isDebugEnabled()) {
223                    unlog(this);
224                }
225            }
226        }
227
228        // rather than looping through the array,
229        // we take advantage of optimization built into DataInputStream
230        private void readBytes(final DataInputStream inputStream, final byte[] bytes) throws IOException {
231            inputStream.read(bytes);
232        }
233    };
234
235    public static FieldType<Short> SHORT = new FieldType<Short>((byte) next++, Short.class, Indenting.INDENT_ONLY) {
236        @Override
237        protected void doWrite(final DataOutputExtended output, final Short value) throws IOException {
238            try {
239                if (LOG.isDebugEnabled()) {
240                    log(this, new StringBuilder().append(value));
241                }
242                final DataOutputStream outputStream = output.getDataOutputStream();
243                outputStream.writeShort(value.shortValue());
244            } finally {
245                if (LOG.isDebugEnabled()) {
246                    unlog(this);
247                }
248            }
249        }
250
251        @Override
252        protected Short doRead(final DataInputExtended input) throws IOException {
253            try {
254                final DataInputStream inputStream = input.getDataInputStream();
255                final short value = inputStream.readShort();
256                if (LOG.isDebugEnabled()) {
257                    log(this, new StringBuilder().append(value));
258                }
259                return value;
260            } finally {
261                if (LOG.isDebugEnabled()) {
262                    unlog(this);
263                }
264            }
265        }
266    };
267
268    public static FieldType<short[]> SHORT_ARRAY = new FieldType<short[]>((byte) next++, short[].class, Indenting.INDENT_AND_OUTDENT) {
269        @Override
270        protected void doWrite(final DataOutputExtended output, final short[] values) throws IOException {
271            try {
272                final StringBuilder buf = new StringBuilder();
273                final DataOutputStream outputStream = output.getDataOutputStream();
274                outputStream.writeInt(values.length);
275                if (LOG.isDebugEnabled()) {
276                    buf.append("length: ").append(values.length);
277                }
278
279                for (int i = 0; i < values.length; i++) {
280                    outputStream.writeShort(values[i]);
281                    if (LOG.isDebugEnabled()) {
282                        buf.append(i == 0 ? ": " : ", ");
283                        buf.append(values[i]);
284                    }
285                }
286                if (LOG.isDebugEnabled()) {
287                    log(this, buf);
288                }
289            } finally {
290                if (LOG.isDebugEnabled()) {
291                    unlog(this);
292                }
293            }
294        }
295
296        @Override
297        protected short[] doRead(final DataInputExtended input) throws IOException {
298            try {
299                final StringBuilder buf = new StringBuilder();
300                final DataInputStream inputStream = input.getDataInputStream();
301                final int length = inputStream.readInt();
302                if (LOG.isDebugEnabled()) {
303                    buf.append("length: ").append(length);
304                }
305
306                final short[] values = new short[length];
307                for (int i = 0; i < values.length; i++) {
308                    values[i] = inputStream.readShort();
309                    if (LOG.isDebugEnabled()) {
310                        buf.append(i == 0 ? ": " : ", ");
311                        buf.append(values[i]);
312                    }
313                }
314                if (LOG.isDebugEnabled()) {
315                    log(this, buf);
316                }
317                return values;
318            } finally {
319                if (LOG.isDebugEnabled()) {
320                    unlog(this);
321                }
322            }
323        }
324    };
325
326    public static FieldType<Integer> INTEGER = new FieldType<Integer>((byte) next++, Integer.class, Indenting.INDENT_ONLY) {
327        @Override
328        protected void doWrite(final DataOutputExtended output, final Integer value) throws IOException {
329            try {
330                if (LOG.isDebugEnabled()) {
331                    log(this, new StringBuilder().append(value));
332                }
333                final DataOutputStream outputStream = output.getDataOutputStream();
334                outputStream.writeInt(value.intValue());
335            } finally {
336                if (LOG.isDebugEnabled()) {
337                    unlog(this);
338                }
339            }
340        }
341
342        @Override
343        protected Integer doRead(final DataInputExtended input) throws IOException {
344            try {
345                final DataInputStream inputStream = input.getDataInputStream();
346                final int value = inputStream.readInt();
347                if (LOG.isDebugEnabled()) {
348                    log(this, new StringBuilder().append(value));
349                }
350                return value;
351            } finally {
352                if (LOG.isDebugEnabled()) {
353                    unlog(this);
354                }
355            }
356        }
357    };
358
359    public static FieldType<Integer> UNSIGNED_BYTE = new FieldType<Integer>((byte) next++, Integer.class, Indenting.INDENT_ONLY) {
360        @Override
361        protected void doWrite(final DataOutputExtended output, final Integer value) throws IOException {
362            try {
363                if (LOG.isDebugEnabled()) {
364                    log(this, new StringBuilder().append(value));
365                }
366                final DataOutputStream outputStream = output.getDataOutputStream();
367                outputStream.writeByte(value);
368            } finally {
369                if (LOG.isDebugEnabled()) {
370                    unlog(this);
371                }
372            }
373        }
374
375        @Override
376        protected Integer doRead(final DataInputExtended input) throws IOException {
377            try {
378                final DataInputStream inputStream = input.getDataInputStream();
379                final int value = inputStream.readUnsignedByte();
380                if (LOG.isDebugEnabled()) {
381                    log(this, new StringBuilder().append(value));
382                }
383                return value;
384            } finally {
385                if (LOG.isDebugEnabled()) {
386                    unlog(this);
387                }
388            }
389        }
390    };
391
392    public static FieldType<Integer> UNSIGNED_SHORT = new FieldType<Integer>((byte) next++, Integer.class, Indenting.INDENT_ONLY) {
393        @Override
394        protected void doWrite(final DataOutputExtended output, final Integer value) throws IOException {
395            try {
396                if (LOG.isDebugEnabled()) {
397                    log(this, new StringBuilder().append(value));
398                }
399                final DataOutputStream outputStream = output.getDataOutputStream();
400                outputStream.writeShort(value);
401            } finally {
402                if (LOG.isDebugEnabled()) {
403                    unlog(this);
404                }
405            }
406        }
407
408        @Override
409        protected Integer doRead(final DataInputExtended input) throws IOException {
410            try {
411                final DataInputStream inputStream = input.getDataInputStream();
412                final int value = inputStream.readUnsignedShort();
413                if (LOG.isDebugEnabled()) {
414                    log(this, new StringBuilder().append(value));
415                }
416                return value;
417            } finally {
418                if (LOG.isDebugEnabled()) {
419                    unlog(this);
420                }
421            }
422        }
423    };
424
425    public static FieldType<int[]> INTEGER_ARRAY = new FieldType<int[]>((byte) next++, int[].class, Indenting.INDENT_AND_OUTDENT) {
426        @Override
427        protected void doWrite(final DataOutputExtended output, final int[] values) throws IOException {
428            try {
429                final StringBuilder buf = new StringBuilder();
430                final DataOutputStream outputStream = output.getDataOutputStream();
431                outputStream.writeInt(values.length);
432                if (LOG.isDebugEnabled()) {
433                    buf.append("length: ").append(values.length);
434                }
435
436                for (int i = 0; i < values.length; i++) {
437                    outputStream.writeInt(values[i]);
438                    if (LOG.isDebugEnabled()) {
439                        buf.append(i == 0 ? ": " : ", ");
440                        buf.append(values[i]);
441                    }
442                }
443                if (LOG.isDebugEnabled()) {
444                    log(this, buf);
445                }
446            } finally {
447                if (LOG.isDebugEnabled()) {
448                    unlog(this);
449                }
450            }
451        }
452
453        @Override
454        protected int[] doRead(final DataInputExtended input) throws IOException {
455            try {
456                final StringBuilder buf = new StringBuilder();
457                final DataInputStream inputStream = input.getDataInputStream();
458                final int length = inputStream.readInt();
459                if (LOG.isDebugEnabled()) {
460                    buf.append("length: ").append(length);
461                }
462
463                final int[] values = new int[length];
464                for (int i = 0; i < values.length; i++) {
465                    values[i] = inputStream.readInt();
466                    if (LOG.isDebugEnabled()) {
467                        buf.append(i == 0 ? ": " : ", ");
468                        buf.append(values[i]);
469                    }
470                }
471                if (LOG.isDebugEnabled()) {
472                    log(this, buf);
473                }
474                return values;
475            } finally {
476                if (LOG.isDebugEnabled()) {
477                    unlog(this);
478                }
479            }
480        }
481    };
482
483    public static FieldType<Long> LONG = new FieldType<Long>((byte) next++, Long.class, Indenting.INDENT_ONLY) {
484        @Override
485        protected void doWrite(final DataOutputExtended output, final Long value) throws IOException {
486            try {
487                if (LOG.isDebugEnabled()) {
488                    log(this, new StringBuilder().append(value));
489                }
490                final DataOutputStream outputStream = output.getDataOutputStream();
491                outputStream.writeLong(value.intValue());
492            } finally {
493                if (LOG.isDebugEnabled()) {
494                    unlog(this);
495                }
496            }
497        }
498
499        @Override
500        protected Long doRead(final DataInputExtended input) throws IOException {
501            try {
502                final DataInputStream inputStream = input.getDataInputStream();
503                final long value = inputStream.readLong();
504                if (LOG.isDebugEnabled()) {
505                    log(this, new StringBuilder().append(value));
506                }
507                return value;
508            } finally {
509                if (LOG.isDebugEnabled()) {
510                    unlog(this);
511                }
512            }
513        }
514    };
515    public static FieldType<long[]> LONG_ARRAY = new FieldType<long[]>((byte) next++, long[].class, Indenting.INDENT_AND_OUTDENT) {
516        @Override
517        protected void doWrite(final DataOutputExtended output, final long[] values) throws IOException {
518            try {
519                final StringBuilder buf = new StringBuilder();
520                final DataOutputStream outputStream = output.getDataOutputStream();
521                outputStream.writeInt(values.length);
522                if (LOG.isDebugEnabled()) {
523                    buf.append("length: ").append(values.length);
524                }
525
526                for (int i = 0; i < values.length; i++) {
527                    outputStream.writeLong(values[i]);
528                    if (LOG.isDebugEnabled()) {
529                        buf.append(i == 0 ? ": " : ", ");
530                        buf.append(values[i]);
531                    }
532                }
533                if (LOG.isDebugEnabled()) {
534                    log(this, buf);
535                }
536            } finally {
537                if (LOG.isDebugEnabled()) {
538                    unlog(this);
539                }
540            }
541        }
542
543        @Override
544        protected long[] doRead(final DataInputExtended input) throws IOException {
545            try {
546                final StringBuilder buf = new StringBuilder();
547
548                final DataInputStream inputStream = input.getDataInputStream();
549                final int length = inputStream.readInt();
550                if (LOG.isDebugEnabled()) {
551                    buf.append("length: ").append(length);
552                }
553
554                final long[] values = new long[length];
555                for (int i = 0; i < values.length; i++) {
556                    values[i] = inputStream.readLong();
557                    if (LOG.isDebugEnabled()) {
558                        buf.append(i == 0 ? ": " : ", ");
559                        buf.append(values[i]);
560                    }
561                }
562                if (LOG.isDebugEnabled()) {
563                    log(this, buf);
564                }
565                return values;
566            } finally {
567                if (LOG.isDebugEnabled()) {
568                    unlog(this);
569                }
570            }
571        }
572    };
573
574    public static FieldType<Character> CHAR = new FieldType<Character>((byte) next++, Character.class, Indenting.INDENT_ONLY) {
575        @Override
576        protected void doWrite(final DataOutputExtended output, final Character value) throws IOException {
577            try {
578                if (LOG.isDebugEnabled()) {
579                    log(this, new StringBuilder().append(value));
580                }
581                final DataOutputStream outputStream = output.getDataOutputStream();
582                outputStream.writeLong(value.charValue());
583            } finally {
584                if (LOG.isDebugEnabled()) {
585                    unlog(this);
586                }
587            }
588        }
589
590        @Override
591        protected Character doRead(final DataInputExtended input) throws IOException {
592            try {
593                final DataInputStream inputStream = input.getDataInputStream();
594                final char value = inputStream.readChar();
595                if (LOG.isDebugEnabled()) {
596                    log(this, new StringBuilder().append(value));
597                }
598                return value;
599            } finally {
600                if (LOG.isDebugEnabled()) {
601                    unlog(this);
602                }
603            }
604        }
605    };
606
607    public static FieldType<char[]> CHAR_ARRAY = new FieldType<char[]>((byte) next++, char[].class, Indenting.INDENT_AND_OUTDENT) {
608        // TODO: could perhaps optimize by writing out as a string
609        @Override
610        protected void doWrite(final DataOutputExtended output, final char[] values) throws IOException {
611            try {
612                final StringBuilder buf = new StringBuilder();
613                final DataOutputStream outputStream = output.getDataOutputStream();
614                outputStream.writeInt(values.length);
615                if (LOG.isDebugEnabled()) {
616                    buf.append("length: ").append(values.length);
617                }
618
619                for (int i = 0; i < values.length; i++) {
620                    outputStream.writeChar(values[i]);
621                    if (LOG.isDebugEnabled()) {
622                        buf.append(i == 0 ? ": " : ", ");
623                        buf.append(values[i]);
624                    }
625                }
626                if (LOG.isDebugEnabled()) {
627                    log(this, buf);
628                }
629            } finally {
630                if (LOG.isDebugEnabled()) {
631                    unlog(this);
632                }
633            }
634        }
635
636        @Override
637        protected char[] doRead(final DataInputExtended input) throws IOException {
638            try {
639                final StringBuilder buf = new StringBuilder();
640                final DataInputStream inputStream = input.getDataInputStream();
641                final int length = inputStream.readInt();
642                if (LOG.isDebugEnabled()) {
643                    buf.append("length: ").append(length);
644                }
645
646                final char[] values = new char[length];
647                for (int i = 0; i < values.length; i++) {
648                    if (LOG.isDebugEnabled()) {
649                        buf.append(i == 0 ? ": " : ", ");
650                        buf.append(values[i]);
651                    }
652                    values[i] = inputStream.readChar();
653                }
654                if (LOG.isDebugEnabled()) {
655                    log(this, buf);
656                }
657                return values;
658            } finally {
659                if (LOG.isDebugEnabled()) {
660                    unlog(this);
661                }
662            }
663        }
664    };
665
666    public static FieldType<Float> FLOAT = new FieldType<Float>((byte) next++, Float.class, Indenting.INDENT_ONLY) {
667        @Override
668        protected void doWrite(final DataOutputExtended output, final Float value) throws IOException {
669            try {
670                if (LOG.isDebugEnabled()) {
671                    log(this, new StringBuilder().append(value));
672                }
673                final DataOutputStream outputStream = output.getDataOutputStream();
674                outputStream.writeFloat(value);
675            } finally {
676                if (LOG.isDebugEnabled()) {
677                    unlog(this);
678                }
679            }
680        }
681
682        @Override
683        protected Float doRead(final DataInputExtended input) throws IOException {
684            try {
685                final DataInputStream inputStream = input.getDataInputStream();
686                final float value = inputStream.readFloat();
687                if (LOG.isDebugEnabled()) {
688                    log(this, new StringBuilder().append(value));
689                }
690                return value;
691            } finally {
692                if (LOG.isDebugEnabled()) {
693                    unlog(this);
694                }
695            }
696        }
697    };
698
699    public static FieldType<float[]> FLOAT_ARRAY = new FieldType<float[]>((byte) next++, float[].class, Indenting.INDENT_AND_OUTDENT) {
700        @Override
701        protected void doWrite(final DataOutputExtended output, final float[] values) throws IOException {
702            try {
703                final StringBuilder buf = new StringBuilder();
704                final DataOutputStream outputStream = output.getDataOutputStream();
705                outputStream.writeInt(values.length);
706                if (LOG.isDebugEnabled()) {
707                    buf.append("length: ").append(values.length);
708                }
709
710                for (int i = 0; i < values.length; i++) {
711                    outputStream.writeFloat(values[i]);
712                    if (LOG.isDebugEnabled()) {
713                        buf.append(i == 0 ? ": " : ", ");
714                        buf.append(values[i]);
715                    }
716                }
717                if (LOG.isDebugEnabled()) {
718                    log(this, buf);
719                }
720            } finally {
721                if (LOG.isDebugEnabled()) {
722                    unlog(this);
723                }
724            }
725        }
726
727        @Override
728        protected float[] doRead(final DataInputExtended input) throws IOException {
729            try {
730                final StringBuilder buf = new StringBuilder();
731                final DataInputStream inputStream = input.getDataInputStream();
732                final int length = inputStream.readInt();
733                if (LOG.isDebugEnabled()) {
734                    buf.append("length: ").append(length);
735                }
736
737                final float[] values = new float[length];
738                for (int i = 0; i < values.length; i++) {
739                    values[i] = inputStream.readFloat();
740                    if (LOG.isDebugEnabled()) {
741                        buf.append(i == 0 ? ": " : ", ");
742                        buf.append(values[i]);
743                    }
744                }
745                if (LOG.isDebugEnabled()) {
746                    log(this, buf);
747                }
748                return values;
749            } finally {
750                if (LOG.isDebugEnabled()) {
751                    unlog(this);
752                }
753            }
754        }
755    };
756
757    public static FieldType<Double> DOUBLE = new FieldType<Double>((byte) next++, Double.class, Indenting.INDENT_ONLY) {
758        @Override
759        protected void doWrite(final DataOutputExtended output, final Double value) throws IOException {
760            try {
761                if (LOG.isDebugEnabled()) {
762                    log(this, new StringBuilder().append(value));
763                }
764                final DataOutputStream outputStream = output.getDataOutputStream();
765                outputStream.writeDouble(value);
766            } finally {
767                if (LOG.isDebugEnabled()) {
768                    unlog(this);
769                }
770            }
771        }
772
773        @Override
774        protected Double doRead(final DataInputExtended input) throws IOException {
775            try {
776                final DataInputStream inputStream = input.getDataInputStream();
777                final double value = inputStream.readDouble();
778                if (LOG.isDebugEnabled()) {
779                    log(this, new StringBuilder().append(value));
780                }
781                return value;
782            } finally {
783                if (LOG.isDebugEnabled()) {
784                    unlog(this);
785                }
786            }
787        }
788    };
789
790    public static FieldType<double[]> DOUBLE_ARRAY = new FieldType<double[]>((byte) next++, double[].class, Indenting.INDENT_AND_OUTDENT) {
791        @Override
792        protected void doWrite(final DataOutputExtended output, final double[] values) throws IOException {
793            try {
794                final StringBuilder buf = new StringBuilder();
795                final DataOutputStream outputStream = output.getDataOutputStream();
796                outputStream.writeInt(values.length);
797                if (LOG.isDebugEnabled()) {
798                    buf.append("length: ").append(values.length);
799                }
800
801                for (int i = 0; i < values.length; i++) {
802                    outputStream.writeDouble(values[i]);
803                    if (LOG.isDebugEnabled()) {
804                        buf.append(i == 0 ? ": " : ", ");
805                        buf.append(values[i]);
806                    }
807                }
808                if (LOG.isDebugEnabled()) {
809                    log(this, buf);
810                }
811            } finally {
812                if (LOG.isDebugEnabled()) {
813                    unlog(this);
814                }
815            }
816        }
817
818        @Override
819        protected double[] doRead(final DataInputExtended input) throws IOException {
820            try {
821                final StringBuilder buf = new StringBuilder();
822                final DataInputStream inputStream = input.getDataInputStream();
823                final int length = inputStream.readInt();
824                if (LOG.isDebugEnabled()) {
825                    buf.append("length: ").append(length);
826                }
827
828                final double[] values = new double[length];
829                for (int i = 0; i < values.length; i++) {
830                    values[i] = inputStream.readDouble();
831                    if (LOG.isDebugEnabled()) {
832                        buf.append(i == 0 ? ": " : ", ");
833                        buf.append(values[i]);
834                    }
835                }
836                if (LOG.isDebugEnabled()) {
837                    log(this, buf);
838                }
839                return values;
840            } finally {
841                if (LOG.isDebugEnabled()) {
842                    unlog(this);
843                }
844            }
845        }
846    };
847
848    public static FieldType<String> STRING = new FieldType<String>((byte) next++, String.class, Indenting.INDENT_ONLY) {
849        @Override
850        protected void doWrite(final DataOutputExtended output, final String value) throws IOException {
851            try {
852                if (LOG.isDebugEnabled()) {
853                    log(this, new StringBuilder().append(value));
854                }
855                final DataOutputStream outputStream = output.getDataOutputStream();
856                outputStream.writeUTF(value);
857            } finally {
858                if (LOG.isDebugEnabled()) {
859                    unlog(this);
860                }
861            }
862        }
863
864        @Override
865        protected String doRead(final DataInputExtended input) throws IOException {
866            try {
867                final DataInputStream inputStream = input.getDataInputStream();
868                final String value = inputStream.readUTF();
869                if (LOG.isDebugEnabled()) {
870                    log(this, new StringBuilder().append(value));
871                }
872                return value;
873            } finally {
874                if (LOG.isDebugEnabled()) {
875                    unlog(this);
876                }
877            }
878        }
879    };
880    public static FieldType<String[]> STRING_ARRAY = new FieldType<String[]>((byte) next++, String[].class, Indenting.INDENT_AND_OUTDENT) {
881        @Override
882        protected void doWrite(final DataOutputExtended output, final String[] values) throws IOException {
883            try {
884                final StringBuilder buf = new StringBuilder();
885                final DataOutputStream outputStream = output.getDataOutputStream();
886                outputStream.writeInt(values.length);
887                if (LOG.isDebugEnabled()) {
888                    buf.append("length: ").append(values.length);
889                }
890
891                for (int i = 0; i < values.length; i++) {
892                    // using FieldType to write out takes care of null handling
893                    FieldType.STRING.write(output, values[i]);
894                    if (LOG.isDebugEnabled()) {
895                        buf.append(i == 0 ? ": " : ", ");
896                        buf.append(values[i]);
897                    }
898                }
899                if (LOG.isDebugEnabled()) {
900                    log(this, buf);
901                }
902            } finally {
903                if (LOG.isDebugEnabled()) {
904                    unlog(this);
905                }
906            }
907        }
908
909        @Override
910        protected String[] doRead(final DataInputExtended input) throws IOException {
911            try {
912                final StringBuilder buf = new StringBuilder();
913                final DataInputStream inputStream = input.getDataInputStream();
914                final int length = inputStream.readInt();
915                if (LOG.isDebugEnabled()) {
916                    buf.append("length: ").append(length);
917                }
918
919                final String[] values = new String[length];
920                for (int i = 0; i < values.length; i++) {
921                    // using FieldType to read in takes care of null handling
922                    values[i] = FieldType.STRING.read(input);
923                    if (LOG.isDebugEnabled()) {
924                        buf.append(i == 0 ? ": " : ", ");
925                        buf.append(values[i]);
926                    }
927                }
928                if (LOG.isDebugEnabled()) {
929                    log(this, buf);
930                }
931                return values;
932            } finally {
933                if (LOG.isDebugEnabled()) {
934                    unlog(this);
935                }
936            }
937        }
938    };
939
940    public static FieldType<Encodable> ENCODABLE = new FieldType<Encodable>((byte) next++, Encodable.class, Indenting.INDENT_AND_OUTDENT) {
941        @Override
942        protected void doWrite(final DataOutputExtended output, final Encodable encodable) throws IOException {
943            try {
944                // write out class
945                final String className = encodable.getClass().getName();
946                if (LOG.isDebugEnabled()) {
947                    log(this, new StringBuilder().append(className));
948                }
949                output.writeUTF(className);
950
951                // recursively encode
952                encodable.encode(output);
953            } finally {
954                if (LOG.isDebugEnabled()) {
955                    unlog(this);
956                }
957            }
958        }
959
960        @Override
961        protected Encodable doRead(final DataInputExtended input) throws IOException {
962            try {
963                // read in class name ...
964                final String className = input.readUTF();
965                if (LOG.isDebugEnabled()) {
966                    log(this, new StringBuilder().append(className));
967                }
968
969                Class<?> cls;
970                try {
971                    // ...obtain constructor
972                    cls = Thread.currentThread().getContextClassLoader().loadClass(className);
973
974                    final Constructor<?> constructor = cls.getConstructor(new Class[] { DataInputExtended.class });
975
976                    // recursively decode
977                    return (Encodable) constructor.newInstance(new Object[] { input });
978                } catch (final ClassNotFoundException ex) {
979                    throw new FailedToDecodeException(ex);
980                } catch (final IllegalArgumentException ex) {
981                    throw new FailedToDecodeException(ex);
982                } catch (final InstantiationException ex) {
983                    throw new FailedToDecodeException(ex);
984                } catch (final IllegalAccessException ex) {
985                    throw new FailedToDecodeException(ex);
986                } catch (final InvocationTargetException ex) {
987                    throw new FailedToDecodeException(ex);
988                } catch (final SecurityException ex) {
989                    throw new FailedToDecodeException(ex);
990                } catch (final NoSuchMethodException ex) {
991                    throw new FailedToDecodeException(ex);
992                }
993
994            } finally {
995                if (LOG.isDebugEnabled()) {
996                    unlog(this);
997                }
998            }
999        }
1000
1001        @Override
1002        protected boolean checksStream() {
1003            return false;
1004        }
1005    };
1006
1007    public static FieldType<Encodable[]> ENCODABLE_ARRAY = new FieldType<Encodable[]>((byte) next++, Encodable[].class, Indenting.INDENT_AND_OUTDENT) {
1008        @Override
1009        protected void doWrite(final DataOutputExtended output, final Encodable[] values) throws IOException {
1010            try {
1011                final DataOutputStream outputStream = output.getDataOutputStream();
1012                outputStream.writeInt(values.length);
1013                if (LOG.isDebugEnabled()) {
1014                    log(this, new StringBuilder().append("length: ").append(values.length));
1015                }
1016                for (final Encodable encodable : values) {
1017                    // using FieldType to write out takes care of null handling
1018                    FieldType.ENCODABLE.write(output, encodable);
1019                }
1020            } finally {
1021                if (LOG.isDebugEnabled()) {
1022                    unlog(this);
1023                }
1024            }
1025        }
1026
1027        @SuppressWarnings("unchecked")
1028        @Override
1029        protected <Q> Q[] doReadArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
1030            try {
1031                final DataInputStream inputStream = input.getDataInputStream();
1032                final int length = inputStream.readInt();
1033                if (LOG.isDebugEnabled()) {
1034                    log(this, new StringBuilder().append("length: ").append(length));
1035                }
1036
1037                final Q[] values = (Q[]) Array.newInstance(elementType, length);
1038                for (int i = 0; i < values.length; i++) {
1039                    // using FieldType to read in takes care of null handling
1040                    values[i] = (Q) FieldType.ENCODABLE.read(input);
1041                }
1042                return values;
1043            } finally {
1044                if (LOG.isDebugEnabled()) {
1045                    unlog(this);
1046                }
1047            }
1048        }
1049
1050        @Override
1051        protected boolean checksStream() {
1052            return false;
1053        }
1054    };
1055
1056    public static FieldType<Serializable> SERIALIZABLE = new FieldType<Serializable>((byte) next++, Serializable.class, Indenting.INDENT_ONLY) {
1057        @Override
1058        protected void doWrite(final DataOutputExtended output, final Serializable value) throws IOException {
1059            try {
1060                if (LOG.isDebugEnabled()) {
1061                    log(this, new StringBuilder().append("[SERIALIZABLE]"));
1062                }
1063
1064                // write out as blob of bytes
1065                final ObjectOutputStream oos = new ObjectOutputStream(output.getDataOutputStream());
1066                oos.writeObject(value);
1067                oos.flush();
1068            } finally {
1069                if (LOG.isDebugEnabled()) {
1070                    unlog(this);
1071                }
1072            }
1073        }
1074
1075        @Override
1076        protected Serializable doRead(final DataInputExtended input) throws IOException {
1077            try {
1078                if (LOG.isDebugEnabled()) {
1079                    log(this, new StringBuilder().append("[SERIALIZABLE]"));
1080                }
1081
1082                // read in a blob of bytes
1083                final ObjectInputStream ois = new ObjectInputStream(input.getDataInputStream());
1084                try {
1085                    return (Serializable) ois.readObject();
1086                } catch (final ClassNotFoundException ex) {
1087                    throw new FailedToDeserializeException(ex);
1088                }
1089            } finally {
1090                if (LOG.isDebugEnabled()) {
1091                    unlog(this);
1092                }
1093            }
1094        }
1095
1096        @Override
1097        protected boolean checksStream() {
1098            return false;
1099        }
1100    };
1101
1102    public static FieldType<Serializable[]> SERIALIZABLE_ARRAY = new FieldType<Serializable[]>((byte) next++, Serializable[].class, Indenting.INDENT_AND_OUTDENT) {
1103        @Override
1104        protected void doWrite(final DataOutputExtended output, final Serializable[] values) throws IOException {
1105            try {
1106                final DataOutputStream outputStream = output.getDataOutputStream();
1107                outputStream.writeInt(values.length);
1108                if (LOG.isDebugEnabled()) {
1109                    log(this, new StringBuilder().append("length: ").append(values.length));
1110                }
1111
1112                for (final Serializable value : values) {
1113                    // using FieldType to write out takes care of null handling
1114                    FieldType.SERIALIZABLE.write(output, value);
1115                }
1116            } finally {
1117                if (LOG.isDebugEnabled()) {
1118                    unlog(this);
1119                }
1120            }
1121        }
1122
1123        @Override
1124        @SuppressWarnings("unchecked")
1125        protected <Q> Q[] doReadArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
1126            try {
1127                final DataInputStream inputStream = input.getDataInputStream();
1128                final int length = inputStream.readInt();
1129                if (LOG.isDebugEnabled()) {
1130                    log(this, new StringBuilder().append("length: ").append(length));
1131                }
1132
1133                final Q[] values = (Q[]) Array.newInstance(elementType, length);
1134                for (int i = 0; i < values.length; i++) {
1135                    // using FieldType to read in takes care of null handling
1136                    values[i] = (Q) FieldType.SERIALIZABLE.read(input);
1137                }
1138                return values;
1139            } finally {
1140                if (LOG.isDebugEnabled()) {
1141                    unlog(this);
1142                }
1143            }
1144        }
1145
1146        @Override
1147        protected boolean checksStream() {
1148            return false;
1149        }
1150    };
1151
1152    public static FieldType<?> get(final byte idx) {
1153        return cache.get(idx);
1154    }
1155
1156    private final byte idx;
1157    private final Class<T> cls;
1158    private final Indenting indenting;
1159
1160    private FieldType(final byte idx, final Class<T> cls, final Indenting indenting) {
1161        this.idx = idx;
1162        this.cls = cls;
1163        this.indenting = indenting;
1164        cache.put(idx, this);
1165    }
1166
1167    public byte getIdx() {
1168        return idx;
1169    }
1170
1171    public Class<T> getCls() {
1172        return cls;
1173    }
1174
1175    /**
1176     * Whether this implementation checks ordering in the stream.
1177     * 
1178     * <p>
1179     * Broadly, the type safe ones do, the {@link Encodable} and
1180     * {@link Serializable} ones do not.
1181     */
1182    protected boolean checksStream() {
1183        return true;
1184    }
1185
1186    public final T read(final DataInputExtended input) throws IOException {
1187        final DataInputStream inputStream = input.getDataInputStream();
1188        final byte fieldTypeIdxAndNullability = inputStream.readByte();
1189
1190        final boolean isNull = fieldTypeIdxAndNullability >= NULL_BIT;
1191        final byte fieldTypeIdx = (byte) (fieldTypeIdxAndNullability - (isNull ? NULL_BIT : 0));
1192        try {
1193            final FieldType<?> fieldType = FieldType.get(fieldTypeIdx);
1194            if (fieldType == null || (fieldType.checksStream() && fieldType != this)) {
1195                throw new IllegalStateException("Mismatch in stream: expected " + this + " but got " + fieldType + " (" + fieldTypeIdx + ")");
1196            }
1197
1198            if (isNull && LOG.isDebugEnabled()) {
1199                // only log if reading a null; otherwise actual value read
1200                // logged later
1201                log(this, new StringBuilder().append("(null)"));
1202            }
1203
1204            if (isNull) {
1205                return null;
1206            } else {
1207                return doRead(input);
1208            }
1209        } finally {
1210            if (isNull && LOG.isDebugEnabled()) {
1211                // only unlog if reading a null
1212                unlog(this);
1213            }
1214        }
1215    }
1216
1217    public final <Q> Q[] readArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
1218        final DataInputStream inputStream = input.getDataInputStream();
1219        final byte fieldTypeIdxAndNullability = inputStream.readByte();
1220
1221        final boolean isNull = fieldTypeIdxAndNullability >= NULL_BIT;
1222        final byte fieldTypeIdx = (byte) (fieldTypeIdxAndNullability - (isNull ? NULL_BIT : 0));
1223        try {
1224            final FieldType<?> fieldType = FieldType.get(fieldTypeIdx);
1225            if (fieldType.checksStream() && fieldType != this) {
1226                throw new IllegalStateException("Mismatch in stream: expected " + this + " but got " + fieldType);
1227            }
1228
1229            if (isNull && LOG.isDebugEnabled()) {
1230                // only log if reading a null; otherwise actual value read
1231                // logged later
1232                log(this, new StringBuilder().append("(null)"));
1233            }
1234
1235            if (isNull) {
1236                return null;
1237            } else {
1238                return doReadArray(input, elementType);
1239            }
1240
1241        } finally {
1242            if (isNull && LOG.isDebugEnabled()) {
1243                // only unlog if reading a null
1244                unlog(this);
1245            }
1246        }
1247
1248    }
1249
1250    public final void write(final DataOutputExtended output, final T value) throws IOException {
1251        byte fieldTypeIdxAndNullability = getIdx();
1252        final boolean isNull = value == null;
1253        if (isNull) {
1254            // set high order bit
1255            fieldTypeIdxAndNullability += NULL_BIT;
1256        }
1257        try {
1258
1259            final DataOutputStream outputStream = output.getDataOutputStream();
1260
1261            outputStream.write(fieldTypeIdxAndNullability);
1262            if (isNull && LOG.isDebugEnabled()) {
1263                // only log if writing a null; otherwise actual value logged
1264                // later
1265                log(this, new StringBuilder().append("(null)"));
1266            }
1267
1268            if (!isNull) {
1269                doWrite(output, value);
1270            }
1271        } finally {
1272            if (isNull && LOG.isDebugEnabled()) {
1273                // only unlog if writing a null
1274                unlog(this);
1275            }
1276        }
1277    }
1278
1279    protected T doRead(final DataInputExtended input) throws IOException {
1280        throw new UnsupportedOperationException("not supported for this field type");
1281    }
1282
1283    protected <Q> Q[] doReadArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
1284        throw new UnsupportedOperationException("not supported for this field type");
1285    }
1286
1287    protected abstract void doWrite(DataOutputExtended output, T value) throws IOException;
1288
1289    private boolean isIndentingAndOutdenting() {
1290        return indenting == Indenting.INDENT_AND_OUTDENT;
1291    }
1292
1293    // ///////////////////////////////////////////////////////
1294    // debugging
1295    // ///////////////////////////////////////////////////////
1296
1297    private static ThreadLocal<int[]> debugIndent = new ThreadLocal<int[]>();
1298
1299    private static void log(final FieldType<?> fieldType, final StringBuilder buf) {
1300        buf.insert(0, ": ");
1301        buf.insert(0, fieldType);
1302        if (fieldType.isIndentingAndOutdenting()) {
1303            buf.insert(0, "> ");
1304        }
1305        buf.insert(0, spaces(currentDebugLevel()));
1306        incrementDebugLevel();
1307        LOG.debug(buf.toString());
1308    }
1309
1310    private static void unlog(final FieldType<?> fieldType) {
1311        unlog(fieldType, new StringBuilder());
1312    }
1313
1314    private static void unlog(final FieldType<?> fieldType, final StringBuilder buf) {
1315        if (fieldType.isIndentingAndOutdenting()) {
1316            buf.insert(0, "< ");
1317        }
1318        decrementDebugLevel();
1319        if (fieldType.isIndentingAndOutdenting()) {
1320            buf.insert(0, spaces(currentDebugLevel()));
1321            LOG.debug(buf.toString());
1322        }
1323    }
1324
1325    private static String spaces(final int num) {
1326        return LOG_INDENT.substring(0, num);
1327    }
1328
1329    private static int currentDebugLevel() {
1330        return debugIndent()[0];
1331    }
1332
1333    private static void incrementDebugLevel() {
1334        final int[] indentLevel = debugIndent();
1335        indentLevel[0] += 2;
1336    }
1337
1338    private static void decrementDebugLevel() {
1339        final int[] indentLevel = debugIndent();
1340        indentLevel[0] -= 2;
1341    }
1342
1343    private static int[] debugIndent() {
1344        int[] indentLevel = debugIndent.get();
1345        if (indentLevel == null) {
1346            indentLevel = new int[1];
1347            debugIndent.set(indentLevel);
1348        }
1349        return indentLevel;
1350    }
1351
1352    // ///////////////////////////////////////////////////////
1353    // toString
1354    // ///////////////////////////////////////////////////////
1355
1356    @Override
1357    public String toString() {
1358        return getCls().getSimpleName();
1359    }
1360
1361}