View Javadoc
1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.ObjectOutputStream;
23  import java.io.ObjectStreamException;
24  import java.io.Reader;
25  import java.io.Serializable;
26  import java.math.BigDecimal;
27  import java.math.BigInteger;
28  import java.nio.ByteBuffer;
29  import java.nio.ByteOrder;
30  import java.nio.CharBuffer;
31  import java.nio.charset.Charset;
32  import java.time.DateTimeException;
33  import java.time.Duration;
34  import java.time.Instant;
35  import java.time.LocalDate;
36  import java.time.LocalDateTime;
37  import java.time.LocalTime;
38  import java.time.ZoneId;
39  import java.time.ZonedDateTime;
40  import java.time.temporal.ChronoUnit;
41  import java.time.temporal.TemporalAccessor;
42  import java.time.temporal.TemporalQueries;
43  import java.util.Calendar;
44  import java.util.Collection;
45  import java.util.Date;
46  import java.util.List;
47  import java.util.Map;
48  import java.util.TimeZone;
49  import java.util.UUID;
50  import java.util.regex.Matcher;
51  import java.util.regex.Pattern;
52  
53  import com.healthmarketscience.jackcess.Column;
54  import com.healthmarketscience.jackcess.ColumnBuilder;
55  import com.healthmarketscience.jackcess.DataType;
56  import com.healthmarketscience.jackcess.DateTimeType;
57  import com.healthmarketscience.jackcess.InvalidValueException;
58  import com.healthmarketscience.jackcess.PropertyMap;
59  import com.healthmarketscience.jackcess.Table;
60  import com.healthmarketscience.jackcess.complex.ComplexColumnInfo;
61  import com.healthmarketscience.jackcess.complex.ComplexValue;
62  import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
63  import com.healthmarketscience.jackcess.expr.Identifier;
64  import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl;
65  import com.healthmarketscience.jackcess.impl.expr.LocaleUtil;
66  import com.healthmarketscience.jackcess.impl.expr.NumberFormatter;
67  import com.healthmarketscience.jackcess.util.ColumnValidator;
68  import com.healthmarketscience.jackcess.util.SimpleColumnValidator;
69  import org.apache.commons.lang3.builder.ToStringBuilder;
70  import org.apache.commons.logging.Log;
71  import org.apache.commons.logging.LogFactory;
72  
73  /**
74   * Access database column definition
75   * @author Tim McCune
76   * @usage _intermediate_class_
77   */
78  public class ColumnImpl implements Column, Comparable<ColumnImpl>, DateTimeContext
79  {
80  
81    protected static final Log LOG = LogFactory.getLog(ColumnImpl.class);
82  
83    /**
84     * Placeholder object for adding rows which indicates that the caller wants
85     * the RowId of the new row.  Must be added as an extra value at the end of
86     * the row values array.
87     * @see TableImpl#asRowWithRowId
88     * @usage _intermediate_field_
89     */
90    public static final Object RETURN_ROW_ID = "<RETURN_ROW_ID>";
91  
92    /**
93     * Access stores numeric dates in days.  Java stores them in milliseconds.
94     */
95    private static final long MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L);
96    private static final long SECONDS_PER_DAY = (24L * 60L * 60L);
97    private static final long NANOS_PER_SECOND = 1_000_000_000L;
98    private static final long NANOS_PER_MILLI = 1_000_000L;
99    private static final long MILLIS_PER_SECOND = 1000L;
100 
101   /**
102    * Access starts counting dates at Dec 30, 1899 (note, this strange date
103    * seems to be caused by MS compatibility with Lotus-1-2-3 and incorrect
104    * leap years).  Java starts counting at Jan 1, 1970.  This is the # of
105    * millis between them for conversion.
106    */
107   static final long MILLIS_BETWEEN_EPOCH_AND_1900 =
108     25569L * MILLISECONDS_PER_DAY;
109 
110   public static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30);
111   public static final LocalTime BASE_LT = LocalTime.of(0, 0);
112   public static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT);
113 
114   private static final LocalDate BASE_EXT_LD = LocalDate.of(1, 1, 1);
115   private static final LocalTime BASE_EXT_LT = LocalTime.of(0, 0);
116   private static final LocalDateTime BASE_EXT_LDT =
117     LocalDateTime.of(BASE_EXT_LD, BASE_EXT_LT);
118   private static final byte[] EXT_LDT_TRAILER = {':', '7', 0x00};
119 
120   private static final DateTimeFactory DEF_DATE_TIME_FACTORY =
121     new DefaultDateTimeFactory();
122 
123   static final DateTimeFactory LDT_DATE_TIME_FACTORY =
124     new LDTDateTimeFactory();
125 
126   /**
127    * mask for the fixed len bit
128    * @usage _advanced_field_
129    */
130   public static final byte FIXED_LEN_FLAG_MASK = (byte)0x01;
131 
132   /**
133    * mask for the auto number bit
134    * @usage _advanced_field_
135    */
136   public static final byte AUTO_NUMBER_FLAG_MASK = (byte)0x04;
137 
138   /**
139    * mask for the auto number guid bit
140    * @usage _advanced_field_
141    */
142   public static final byte AUTO_NUMBER_GUID_FLAG_MASK = (byte)0x40;
143 
144   /**
145    * mask for the hyperlink bit (on memo types)
146    * @usage _advanced_field_
147    */
148   public static final byte HYPERLINK_FLAG_MASK = (byte)0x80;
149 
150   /**
151    * mask for the "is updatable" field bit
152    * @usage _advanced_field_
153    */
154   public static final byte UPDATABLE_FLAG_MASK = (byte)0x02;
155 
156   // some other flags?
157   // 0x10: replication related field (or hidden?)
158 
159   protected static final byte COMPRESSED_UNICODE_EXT_FLAG_MASK = (byte)0x01;
160   private static final byte CALCULATED_EXT_FLAG_MASK = (byte)0xC0;
161 
162   static final byte NUMERIC_NEGATIVE_BYTE = (byte)0x80;
163 
164   /** the value for the "general" sort order */
165   private static final short GENERAL_SORT_ORDER_VALUE = 1033;
166 
167   /**
168    * the "general" text sort order, version (access 1997)
169    * @usage _intermediate_field_
170    */
171   public static final SortOrder GENERAL_97_SORT_ORDER =
172     new SortOrder(GENERAL_SORT_ORDER_VALUE, (short)-1);
173 
174   /**
175    * the "general" text sort order, legacy version (access 2000-2007)
176    * @usage _intermediate_field_
177    */
178   public static final SortOrder GENERAL_LEGACY_SORT_ORDER =
179     new SortOrder(GENERAL_SORT_ORDER_VALUE, (short)0);
180 
181   /**
182    * the "general" text sort order, latest version (access 2010+)
183    * @usage _intermediate_field_
184    */
185   public static final SortOrder GENERAL_SORT_ORDER =
186     new SortOrder(GENERAL_SORT_ORDER_VALUE, (short)1);
187 
188   /** pattern matching textual guid strings (allows for optional surrounding
189       '{' and '}') */
190   private static final Pattern GUID_PATTERN = Pattern.compile("\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*");
191 
192   /** header used to indicate unicode text compression */
193   private static final byte[] TEXT_COMPRESSION_HEADER =
194   { (byte)0xFF, (byte)0XFE };
195   private static final char MIN_COMPRESS_CHAR = 1;
196   private static final char MAX_COMPRESS_CHAR = 0xFF;
197 
198   /** auto numbers must be > 0 */
199   static final int INVALID_AUTO_NUMBER = 0;
200 
201   static final int INVALID_LENGTH = -1;
202 
203 
204   /** owning table */
205   private final TableImpl _table;
206   /** Whether or not the column is of variable length */
207   private final boolean _variableLength;
208   /** Whether or not the column is an autonumber column */
209   private final boolean _autoNumber;
210   /** Whether or not the column is a calculated column */
211   private final boolean _calculated;
212   /** Data type */
213   private final DataType _type;
214   /** Maximum column length */
215   private final short _columnLength;
216   /** 0-based column number */
217   private final short _columnNumber;
218   /** index of the data for this column within a list of row data */
219   private int _columnIndex;
220   /** display index of the data for this column */
221   private final int _displayIndex;
222   /** Column name */
223   private final String _name;
224   /** the offset of the fixed data in the row */
225   private final int _fixedDataOffset;
226   /** the index of the variable length data in the var len offset table */
227   private final int _varLenTableIndex;
228   /** the auto number generator for this column (if autonumber column) */
229   private final AutoNumberGenerator _autoNumberGenerator;
230   /** properties for this column, if any */
231   private PropertyMap _props;
232   /** Validator for writing new values */
233   private ColumnValidator _validator = SimpleColumnValidator.INSTANCE;
234   /** default value generator */
235   private ColDefaultValueEvalContext _defValue;
236   /** length of the column in units, lazily computed */
237   private int _lengthInUnits = INVALID_LENGTH;
238 
239   /**
240    * @usage _advanced_method_
241    */
242   protected ColumnImpl(TableImpl table, String name, DataType type,
243                        int colNumber, int fixedOffset, int varLenIndex) {
244     _table = table;
245     _name = name;
246     _type = type;
247 
248     if(!_type.isVariableLength()) {
249       _columnLength = (short)type.getFixedSize();
250     } else {
251       _columnLength = (short)type.getMaxSize();
252     }
253     _variableLength = type.isVariableLength();
254     _autoNumber = false;
255     _calculated = false;
256     _autoNumberGenerator = null;
257     _columnNumber = (short)colNumber;
258     _columnIndex = colNumber;
259     _displayIndex = colNumber;
260     _fixedDataOffset = fixedOffset;
261     _varLenTableIndex = varLenIndex;
262   }
263 
264   /**
265    * Read a column definition in from a buffer
266    * @usage _advanced_method_
267    */
268   ColumnImpl(InitArgs args)
269   {
270     _table = args.table;
271     _name = args.name;
272     _displayIndex = args.displayIndex;
273     _type = args.type;
274 
275     _columnNumber = args.buffer.getShort(
276         args.offset + getFormat().OFFSET_COLUMN_NUMBER);
277     _columnLength = args.buffer.getShort(
278         args.offset + getFormat().OFFSET_COLUMN_LENGTH);
279 
280     _variableLength = ((args.flags & FIXED_LEN_FLAG_MASK) == 0);
281     _autoNumber = ((args.flags &
282                     (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) != 0);
283     _calculated = ((args.extFlags & CALCULATED_EXT_FLAG_MASK) != 0);
284 
285     _autoNumberGenerator = createAutoNumberGenerator();
286 
287     _varLenTableIndex = args.buffer.getShort(
288         args.offset + getFormat().OFFSET_COLUMN_VARIABLE_TABLE_INDEX);
289     _fixedDataOffset = args.buffer.getShort(
290           args.offset + getFormat().OFFSET_COLUMN_FIXED_DATA_OFFSET);
291   }
292 
293   /**
294    * Creates the appropriate ColumnImpl class and reads a column definition in
295    * from a buffer
296    * @param table owning table
297    * @param buffer Buffer containing column definition
298    * @param offset Offset in the buffer at which the column definition starts
299    * @usage _advanced_method_
300    */
301   public static ColumnImpl create(TableImpl table, ByteBuffer buffer,
302                                   int offset, String name, int displayIndex)
303     throws IOException
304   {
305     InitArgs args = new InitArgs(table, buffer, offset, name, displayIndex);
306 
307     boolean calculated = ((args.extFlags & CALCULATED_EXT_FLAG_MASK) != 0);
308     byte colType = args.colType;
309     if(calculated) {
310       // "real" data type is in the "result type" property
311       PropertyMap colProps = table.getPropertyMaps().get(name);
312       Byte resultType = (Byte)colProps.getValue(PropertyMap.RESULT_TYPE_PROP);
313       if(resultType != null) {
314         colType = resultType;
315       }
316     }
317 
318     try {
319       args.type = DataType.fromByte(colType);
320     } catch(IOException e) {
321       LOG.warn(withErrorContext("Unsupported column type " + colType,
322                                 table.getDatabase(), table.getName(), name));
323       boolean variableLength = ((args.flags & FIXED_LEN_FLAG_MASK) == 0);
324       args.type = (variableLength ? DataType.UNSUPPORTED_VARLEN :
325                    DataType.UNSUPPORTED_FIXEDLEN);
326       return new UnsupportedColumnImpl(args);
327     }
328 
329     if(calculated) {
330       return CalculatedColumnUtil.create(args);
331     }
332 
333     switch(args.type) {
334     case TEXT:
335       return new TextColumnImpl(args);
336     case MEMO:
337       return new MemoColumnImpl(args);
338     case COMPLEX_TYPE:
339       return new ComplexColumnImpl(args);
340     default:
341       // fall through
342     }
343 
344     if(args.type.getHasScalePrecision()) {
345       return new NumericColumnImpl(args);
346     }
347     if(args.type.isLongValue()) {
348       return new LongValueColumnImpl(args);
349     }
350 
351     return new ColumnImpl(args);
352   }
353 
354   /**
355    * Sets the usage maps for this column.
356    */
357   void setUsageMaps(UsageMap./../../com/healthmarketscience/jackcess/impl/UsageMap.html#UsageMap">UsageMap ownedPages, UsageMap freeSpacePages) {
358     // base does nothing
359   }
360 
361   void collectUsageMapPages(Collection<Integer> pages) {
362     // base does nothing
363   }
364 
365   /**
366    * Secondary column initialization after the table is fully loaded.
367    */
368   void postTableLoadInit() throws IOException {
369     // base does nothing
370   }
371 
372   @Override
373   public TableImpl getTable() {
374     return _table;
375   }
376 
377   @Override
378   public DatabaseImpl getDatabase() {
379     return getTable().getDatabase();
380   }
381 
382   /**
383    * @usage _advanced_method_
384    */
385   public JetFormat getFormat() {
386     return getDatabase().getFormat();
387   }
388 
389   /**
390    * @usage _advanced_method_
391    */
392   public PageChannel getPageChannel() {
393     return getDatabase().getPageChannel();
394   }
395 
396   @Override
397   public String getName() {
398     return _name;
399   }
400 
401   @Override
402   public boolean isVariableLength() {
403     return _variableLength;
404   }
405 
406   @Override
407   public boolean isAutoNumber() {
408     return _autoNumber;
409   }
410 
411   /**
412    * @usage _advanced_method_
413    */
414   public short getColumnNumber() {
415     return _columnNumber;
416   }
417 
418   @Override
419   public int getColumnIndex() {
420     return _columnIndex;
421   }
422 
423   /**
424    * @usage _advanced_method_
425    */
426   public void setColumnIndex(int newColumnIndex) {
427     _columnIndex = newColumnIndex;
428   }
429 
430   /**
431    * @usage _advanced_method_
432    */
433   public int getDisplayIndex() {
434     return _displayIndex;
435   }
436 
437   @Override
438   public DataType getType() {
439     return _type;
440   }
441 
442   @Override
443   public int getSQLType() throws IOException {
444     return _type.getSQLType();
445   }
446 
447   @Override
448   public boolean isCompressedUnicode() {
449     return false;
450   }
451 
452   @Override
453   public byte getPrecision() {
454     return (byte)getType().getDefaultPrecision();
455   }
456 
457   @Override
458   public byte getScale() {
459     return (byte)getType().getDefaultScale();
460   }
461 
462   /**
463    * @usage _intermediate_method_
464    */
465   public SortOrder getTextSortOrder() {
466     return null;
467   }
468 
469   /**
470    * @usage _intermediate_method_
471    */
472   public short getTextCodePage() {
473     return 0;
474   }
475 
476   @Override
477   public short getLength() {
478     return _columnLength;
479   }
480 
481   @Override
482   public final short getLengthInUnits() {
483     if(_lengthInUnits == INVALID_LENGTH) {
484       _lengthInUnits = calcLengthInUnits();
485     }
486     return (short)_lengthInUnits;
487   }
488 
489   protected int calcLengthInUnits() {
490     return getType().toUnitSize(getLength(), getFormat());
491   }
492 
493   @Override
494   public boolean isCalculated() {
495     return _calculated;
496   }
497 
498   /**
499    * @usage _advanced_method_
500    */
501   public int getVarLenTableIndex() {
502     return _varLenTableIndex;
503   }
504 
505   /**
506    * @usage _advanced_method_
507    */
508   public int getFixedDataOffset() {
509     return _fixedDataOffset;
510   }
511 
512   protected int getFixedDataSize() {
513     return _type.getFixedSize(_columnLength);
514   }
515 
516   protected Charset getCharset() {
517     return getDatabase().getCharset();
518   }
519 
520   @Override
521   public TimeZone getTimeZone() {
522     return getDatabase().getTimeZone();
523   }
524 
525   @Override
526   public ZoneId getZoneId() {
527     return getDatabase().getZoneId();
528   }
529 
530   @Override
531   public DateTimeFactory getDateTimeFactory() {
532     return getDatabase().getDateTimeFactory();
533   }
534 
535   @Override
536   public boolean isAppendOnly() {
537     return (getVersionHistoryColumn() != null);
538   }
539 
540   @Override
541   public ColumnImpl getVersionHistoryColumn() {
542     return null;
543   }
544 
545   /**
546    * Returns the number of database pages owned by this column.
547    * @usage _intermediate_method_
548    */
549   public int getOwnedPageCount() {
550     return 0;
551   }
552 
553   /**
554    * @usage _advanced_method_
555    */
556   public void setVersionHistoryColumn(ColumnImpl versionHistoryCol) {
557     throw new UnsupportedOperationException();
558   }
559 
560   @Override
561   public boolean isHyperlink() {
562     return false;
563   }
564 
565   @Override
566   public ComplexColumnInfo<? extends ComplexValue> getComplexInfo() {
567     return null;
568   }
569 
570   void initColumnValidator() throws IOException {
571 
572     if(getDatabase().isReadOnly()) {
573       // validators are irrelevant for read-only databases
574       return;
575     }
576 
577     // first initialize any "external" (user-defined) validator
578     setColumnValidator(null);
579 
580     // next, initialize any "internal" (property defined) validators
581     reloadPropertiesValidators();
582   }
583 
584   void reloadPropertiesValidators() throws IOException {
585 
586     if(isAutoNumber()) {
587       // none of the props stuff applies to autonumber columns
588       return;
589     }
590 
591     if(isCalculated()) {
592 
593       CalcColEvalContext calcCol = null;
594 
595       if(getDatabase().isEvaluateExpressions()) {
596 
597         // init calc col expression evaluator
598         PropertyMap props = getProperties();
599         String calcExpr = (String)props.getValue(PropertyMap.EXPRESSION_PROP);
600         calcCol = new CalcColEvalContext(this).setExpr(calcExpr);
601       }
602 
603       setCalcColEvalContext(calcCol);
604 
605       // none of the remaining props stuff applies to calculated columns
606       return;
607     }
608 
609     // discard any existing internal validators and re-compute them
610     // (essentially unwrap the external validator)
611     _validator = getColumnValidator();
612     _defValue = null;
613 
614     PropertyMap props = getProperties();
615 
616     // if the "required" property is enabled, add appropriate validator
617     boolean required = (Boolean)props.getValue(PropertyMap.REQUIRED_PROP,
618                                                Boolean.FALSE);
619     if(required) {
620       _validator = new RequiredColValidator(_validator);
621     }
622 
623     // if the "allow zero len" property is disabled (textual columns only),
624     // add appropriate validator
625     boolean allowZeroLen =
626       !getType().isTextual() ||
627       (Boolean)props.getValue(PropertyMap.ALLOW_ZERO_LEN_PROP,
628                               Boolean.TRUE);
629     if(!allowZeroLen) {
630       _validator = new NoZeroLenColValidator(_validator);
631     }
632 
633     // only check for props based exprs if this is enabled
634     if(!getDatabase().isEvaluateExpressions()) {
635       return;
636     }
637 
638     String exprStr = PropertyMaps.getTrimmedStringProperty(
639         props, PropertyMap.VALIDATION_RULE_PROP);
640 
641     if(exprStr != null) {
642       String helpStr = PropertyMaps.getTrimmedStringProperty(
643           props, PropertyMap.VALIDATION_TEXT_PROP);
644 
645       _validator = new ColValidatorEvalContext(this)
646         .setExpr(exprStr, helpStr)
647         .toColumnValidator(_validator);
648     }
649 
650     String defValueStr = PropertyMaps.getTrimmedStringProperty(
651         props, PropertyMap.DEFAULT_VALUE_PROP);
652     if(defValueStr != null) {
653       _defValue = new ColDefaultValueEvalContext(this)
654         .setExpr(defValueStr);
655     }
656   }
657 
658   void propertiesUpdated() throws IOException {
659     reloadPropertiesValidators();
660   }
661 
662   @Override
663   public ColumnValidator getColumnValidator() {
664     // unwrap any "internal" validator
665     return ((_validator instanceof InternalColumnValidator) ?
666             ((InternalColumnValidator)_validator).getExternal() : _validator);
667   }
668 
669   @Override
670   public void setColumnValidator(ColumnValidator newValidator) {
671 
672     if(isAutoNumber()) {
673       // cannot set autonumber validator (autonumber values are controlled
674       // internally)
675       if(newValidator != null) {
676         throw new IllegalArgumentException(withErrorContext(
677                 "Cannot set ColumnValidator for autonumber columns"));
678       }
679       // just leave default validator instance alone
680       return;
681     }
682 
683     if(newValidator == null) {
684       newValidator = getDatabase().getColumnValidatorFactory()
685         .createValidator(this);
686       if(newValidator == null) {
687         newValidator = SimpleColumnValidator.INSTANCE;
688       }
689     }
690 
691     // handle delegation if "internal" validator in use
692     if(_validator instanceof InternalColumnValidator) {
693       ((InternalColumnValidator)_validator).setExternal(newValidator);
694     } else {
695       _validator = newValidator;
696     }
697   }
698 
699   byte getOriginalDataType() {
700     return _type.getValue();
701   }
702 
703   private AutoNumberGenerator createAutoNumberGenerator() {
704     if(!_autoNumber || (_type == null)) {
705       return null;
706     }
707 
708     switch(_type) {
709     case LONG:
710       return new LongAutoNumberGenerator();
711     case GUID:
712       return new GuidAutoNumberGenerator();
713     case COMPLEX_TYPE:
714       return new ComplexTypeAutoNumberGenerator();
715     default:
716       LOG.warn(withErrorContext("Unknown auto number column type " + _type));
717       return new UnsupportedAutoNumberGenerator(_type);
718     }
719   }
720 
721   /**
722    * Returns the AutoNumberGenerator for this column if this is an autonumber
723    * column, {@code null} otherwise.
724    * @usage _advanced_method_
725    */
726   public AutoNumberGenerator getAutoNumberGenerator() {
727     return _autoNumberGenerator;
728   }
729 
730   @Override
731   public PropertyMap getProperties() throws IOException {
732     if(_props == null) {
733       _props = getTable().getPropertyMaps().get(getName());
734     }
735     return _props;
736   }
737 
738   @Override
739   public Object setRowValue(Object[] rowArray, Object value) {
740     rowArray[_columnIndex] = value;
741     return value;
742   }
743 
744   @Override
745   public Object setRowValue(Map<String,Object> rowMap, Object value) {
746     rowMap.put(_name, value);
747     return value;
748   }
749 
750   @Override
751   public Object getRowValue(Object[] rowArray) {
752     return rowArray[_columnIndex];
753   }
754 
755   @Override
756   public Object getRowValue(Map<String,?> rowMap) {
757     return rowMap.get(_name);
758   }
759 
760   public boolean storeInNullMask() {
761     return (getType() == DataType.BOOLEAN);
762   }
763 
764   public boolean writeToNullMask(Object value) {
765     return toBooleanValue(value);
766   }
767 
768   public Object readFromNullMask(boolean isNull) {
769     return Boolean.valueOf(!isNull);
770   }
771 
772   /**
773    * Deserialize a raw byte value for this column into an Object
774    * @param data The raw byte value
775    * @return The deserialized Object
776    * @usage _advanced_method_
777    */
778   public Object read(byte[] data) throws IOException {
779     return read(data, PageChannel.DEFAULT_BYTE_ORDER);
780   }
781 
782   /**
783    * Deserialize a raw byte value for this column into an Object
784    * @param data The raw byte value
785    * @param order Byte order in which the raw value is stored
786    * @return The deserialized Object
787    * @usage _advanced_method_
788    */
789   public Object read(byte[] data, ByteOrder order) throws IOException {
790     ByteBuffer buffer = ByteBuffer.wrap(data).order(order);
791 
792     switch(getType()) {
793     case BOOLEAN:
794       throw new IOException(withErrorContext("Tried to read a boolean from data instead of null mask."));
795     case BYTE:
796       return Byte.valueOf(buffer.get());
797     case INT:
798       return Short.valueOf(buffer.getShort());
799     case LONG:
800       return Integer.valueOf(buffer.getInt());
801     case DOUBLE:
802       return Double.valueOf(buffer.getDouble());
803     case FLOAT:
804       return Float.valueOf(buffer.getFloat());
805     case SHORT_DATE_TIME:
806       return readDateValue(buffer);
807     case BINARY:
808       return data;
809     case TEXT:
810       return decodeTextValue(data);
811     case MONEY:
812       return readCurrencyValue(buffer);
813     case NUMERIC:
814       return readNumericValue(buffer);
815     case GUID:
816       return readGUIDValue(buffer, order);
817     case EXT_DATE_TIME:
818       return readExtendedDateValue(buffer);
819     case UNKNOWN_0D:
820     case UNKNOWN_11:
821       // treat like "binary" data
822       return data;
823     case COMPLEX_TYPE:
824       return new ComplexValueForeignKeyImpl(this, buffer.getInt());
825     case BIG_INT:
826       return Long.valueOf(buffer.getLong());
827     default:
828       throw new IOException(withErrorContext("Unrecognized data type: " + _type));
829     }
830   }
831 
832   /**
833    * Decodes "Currency" values.
834    *
835    * @param buffer Column value that points to currency data
836    * @return BigDecimal representing the monetary value
837    * @throws IOException if the value cannot be parsed
838    */
839   private BigDecimal readCurrencyValue(ByteBuffer buffer)
840     throws IOException
841   {
842     if(buffer.remaining() != 8) {
843       throw new IOException(withErrorContext("Invalid money value"));
844     }
845 
846     return new BigDecimal(BigInteger.valueOf(buffer.getLong(0)), 4);
847   }
848 
849   /**
850    * Writes "Currency" values.
851    */
852   private void writeCurrencyValue(ByteBuffer buffer, Object value)
853     throws IOException
854   {
855     Object inValue = value;
856     try {
857       BigDecimal decVal = toBigDecimal(value);
858       inValue = decVal;
859 
860       // adjust scale (will cause the an ArithmeticException if number has too
861       // many decimal places)
862       decVal = decVal.setScale(4);
863 
864       // now, remove scale and convert to long (this will throw if the value is
865       // too big)
866       buffer.putLong(decVal.movePointRight(4).longValueExact());
867     } catch(ArithmeticException e) {
868       throw new IOException(
869           withErrorContext("Currency value '" + inValue + "' out of range"), e);
870     }
871   }
872 
873   /**
874    * Decodes a NUMERIC field.
875    */
876   private BigDecimal readNumericValue(ByteBuffer buffer)
877   {
878     boolean negate = (buffer.get() != 0);
879 
880     byte[] tmpArr = ByteUtil.getBytes(buffer, 16);
881 
882     if(buffer.order() != ByteOrder.BIG_ENDIAN) {
883       fixNumericByteOrder(tmpArr);
884     }
885 
886     return toBigDecimal(tmpArr, negate, getScale());
887   }
888 
889   static BigDecimal toBigDecimal(byte[] bytes, boolean negate, int scale)
890   {
891     if((bytes[0] & 0x80) != 0) {
892       // the data is effectively unsigned, but the BigInteger handles it as
893       // signed twos complement.  we need to add an extra byte to the input so
894       // that it will be treated as unsigned
895       bytes = ByteUtil.copyOf(bytes, 0, bytes.length + 1, 1);
896     }
897     BigInteger intVal = new BigInteger(bytes);
898     if(negate) {
899       intVal = intVal.negate();
900     }
901     return new BigDecimal(intVal, scale);
902   }
903 
904   /**
905    * Writes a numeric value.
906    */
907   private void writeNumericValue(ByteBuffer buffer, Object value)
908     throws IOException
909   {
910     Object inValue = value;
911     try {
912       BigDecimal decVal = toBigDecimal(value);
913       inValue = decVal;
914 
915       int signum = decVal.signum();
916       if(signum < 0) {
917         decVal = decVal.negate();
918       }
919 
920       // write sign byte
921       buffer.put((signum < 0) ? NUMERIC_NEGATIVE_BYTE : 0);
922 
923       // adjust scale according to this column type (will cause the an
924       // ArithmeticException if number has too many decimal places)
925       decVal = decVal.setScale(getScale());
926 
927       // check precision
928       if(decVal.precision() > getPrecision()) {
929         throw new InvalidValueException(withErrorContext(
930             "Numeric value is too big for specified precision "
931             + getPrecision() + ": " + decVal));
932       }
933 
934       // convert to unscaled BigInteger, big-endian bytes
935       byte[] intValBytes = toUnscaledByteArray(
936           decVal, getType().getFixedSize() - 1);
937       if(buffer.order() != ByteOrder.BIG_ENDIAN) {
938         fixNumericByteOrder(intValBytes);
939       }
940       buffer.put(intValBytes);
941     } catch(ArithmeticException e) {
942       throw new IOException(
943           withErrorContext("Numeric value '" + inValue + "' out of range"), e);
944     }
945   }
946 
947   byte[] toUnscaledByteArray(BigDecimal decVal, int maxByteLen)
948     throws IOException
949   {
950     // convert to unscaled BigInteger, big-endian bytes
951     byte[] intValBytes = decVal.unscaledValue().toByteArray();
952     if(intValBytes.length > maxByteLen) {
953       if((intValBytes[0] == 0) && ((intValBytes.length - 1) == maxByteLen)) {
954         // in order to not return a negative two's complement value,
955         // toByteArray() may return an extra leading 0 byte.  we are working
956         // with unsigned values, so we can drop the extra leading 0
957         intValBytes = ByteUtil.copyOf(intValBytes, 1, maxByteLen);
958       } else {
959         throw new InvalidValueException(withErrorContext(
960                                   "Too many bytes for valid BigInteger?"));
961       }
962     } else if(intValBytes.length < maxByteLen) {
963       intValBytes = ByteUtil.copyOf(intValBytes, 0, maxByteLen,
964                                     (maxByteLen - intValBytes.length));
965     }
966     return intValBytes;
967   }
968 
969   /**
970    * Decodes a date value.
971    */
972   private Object readDateValue(ByteBuffer buffer) {
973     long dateBits = buffer.getLong();
974     return getDateTimeFactory().fromDateBits(this, dateBits);
975   }
976 
977   /**
978    * Decodes an "extended" date/time value.
979    */
980   private static Object readExtendedDateValue(ByteBuffer buffer) {
981     // format: <19digits>:<19digits>:7 0x00
982     long numDays = readExtDateLong(buffer, 19);
983     buffer.get();
984     long seconds = readExtDateLong(buffer, 12);
985     // there are 7 fractional digits
986     long nanos = readExtDateLong(buffer, 7) * 100L;
987     ByteUtil.forward(buffer, EXT_LDT_TRAILER.length);
988 
989     return BASE_EXT_LDT
990       .plusDays(numDays)
991       .plusSeconds(seconds)
992       .plusNanos(nanos);
993   }
994 
995   /**
996    * Reads the given number of ascii encoded characters as a long value.
997    */
998   private static long readExtDateLong(ByteBuffer buffer, int numChars) {
999     long val = 0L;
1000     for(int i = 0; i < numChars; ++i) {
1001       char digit = (char)buffer.get();
1002       long inc = digit - '0';
1003       val = (val * 10L) + inc;
1004     }
1005     return val;
1006   }
1007 
1008   /**
1009    * Returns a java long time value converted from an access date double.
1010    * @usage _advanced_method_
1011    */
1012   public long fromDateDouble(double value) {
1013     return fromDateDouble(value, getTimeZone());
1014   }
1015 
1016   private static long fromDateDouble(double value, TimeZone tz) {
1017     long localTime = fromLocalDateDouble(value);
1018     return localTime - getFromLocalTimeZoneOffset(localTime, tz);
1019   }
1020 
1021   static long fromLocalDateDouble(double value) {
1022     long datePart = ((long)value) * MILLISECONDS_PER_DAY;
1023 
1024     // the fractional part of the double represents the time.  it is always
1025     // a positive fraction of the day (even if the double is negative),
1026     // _not_ the time distance from zero (as one would expect with "normal"
1027     // numbers).  therefore, we need to do a little number logic to convert
1028     // the absolute time fraction into a normal distance from zero number.
1029     long timePart = Math.round((Math.abs(value) % 1.0d) *
1030                                MILLISECONDS_PER_DAY);
1031 
1032     long time = datePart + timePart;
1033     return time - MILLIS_BETWEEN_EPOCH_AND_1900;
1034   }
1035 
1036   public static LocalDateTime ldtFromLocalDateDouble(double value) {
1037     Duration dateTimeOffset = durationFromLocalDateDouble(value);
1038     return BASE_LDT.plus(dateTimeOffset);
1039   }
1040 
1041   private static Duration durationFromLocalDateDouble(double value) {
1042     long dateSeconds = ((long)value) * SECONDS_PER_DAY;
1043 
1044     // the fractional part of the double represents the time.  it is always
1045     // a positive fraction of the day (even if the double is negative),
1046     // _not_ the time distance from zero (as one would expect with "normal"
1047     // numbers).  therefore, we need to do a little number logic to convert
1048     // the absolute time fraction into a normal distance from zero number.
1049 
1050     double secondsDouble = (Math.abs(value) % 1.0d) * SECONDS_PER_DAY;
1051     long timeSeconds = (long)secondsDouble;
1052     long timeMillis = (long)(roundToMillis(secondsDouble % 1.0d) *
1053                              MILLIS_PER_SECOND);
1054 
1055     return Duration.ofSeconds(dateSeconds + timeSeconds,
1056                               timeMillis * NANOS_PER_MILLI);
1057   }
1058 
1059   /**
1060    * Writes a date value.
1061    */
1062   private void writeDateValue(ByteBuffer buffer, Object value)
1063     throws InvalidValueException
1064   {
1065     if(value == null) {
1066       buffer.putDouble(0d);
1067     } else if(value instanceof DateExt) {
1068       // this is a Date value previously read from readDateValue().  use the
1069       // original bits to store the value so we don't lose any precision
1070       buffer.putLong(((DateExt)value).getDateBits());
1071     } else {
1072       buffer.putDouble(toDateDouble(value));
1073     }
1074   }
1075 
1076   /**
1077    * Writes an "extended" date/time value.
1078    */
1079   private void writeExtendedDateValue(ByteBuffer buffer, Object value)
1080   {
1081     LocalDateTime ldt = BASE_EXT_LDT;
1082     if(value != null) {
1083       ldt = toLocalDateTime(value, this);
1084     }
1085 
1086     LocalDate ld = ldt.toLocalDate();
1087     LocalTime lt = ldt.toLocalTime();
1088 
1089     long numDays = BASE_EXT_LD.until(ld, ChronoUnit.DAYS);
1090     long numSeconds = BASE_EXT_LT.until(lt, ChronoUnit.SECONDS);
1091     long nanos = lt.getNano();
1092 
1093     // format: <19digits>:<19digits>:7 0x00
1094     writeExtDateLong(buffer, numDays, 19);
1095     buffer.put((byte)':');
1096     writeExtDateLong(buffer, numSeconds, 12);
1097     // there are 7 fractional digits
1098     writeExtDateLong(buffer, (nanos / 100L), 7);
1099 
1100     buffer.put(EXT_LDT_TRAILER);
1101   }
1102 
1103   /**
1104    * Writes the given long value as the given number of ascii encoded
1105    * characters.
1106    */
1107   private static void writeExtDateLong(
1108       ByteBuffer buffer, long val, int numChars) {
1109     // we write the desired number of digits in reverse order
1110     int end = buffer.position();
1111     int start = end + numChars - 1;
1112     for(int i = start; i >= end; --i) {
1113       char digit = (char)('0' + (char)(val % 10L));
1114       buffer.put(i, (byte)digit);
1115       val /= 10L;
1116     }
1117     ByteUtil.forward(buffer, numChars);
1118   }
1119 
1120   /**
1121    * Returns an access date double converted from a java Date/Calendar/Number
1122    * time value.
1123    * @usage _advanced_method_
1124    */
1125   public double toDateDouble(Object value)
1126     throws InvalidValueException
1127   {
1128     try {
1129       return toDateDouble(value, this);
1130     } catch(IllegalArgumentException iae) {
1131       throw new InvalidValueException(withErrorContext(iae.getMessage()), iae);
1132     }
1133   }
1134 
1135   /**
1136    * Returns an access date double converted from a java
1137    * Date/Calendar/Number/Temporal time value.
1138    * @usage _advanced_method_
1139    */
1140   private static double toDateDouble(Object value, DateTimeContext dtc) {
1141     return dtc.getDateTimeFactory().toDateDouble(value, dtc);
1142   }
1143 
1144   static LocalDateTime toLocalDateTime(
1145       Object value, DateTimeContext dtc) {
1146     if(value instanceof TemporalAccessor) {
1147       return temporalToLocalDateTime((TemporalAccessor)value, dtc);
1148     }
1149     Instant inst = Instant.ofEpochMilli(toDateLong(value));
1150     return LocalDateTime.ofInstant(inst, dtc.getZoneId());
1151   }
1152 
1153   private static LocalDateTime temporalToLocalDateTime(
1154       TemporalAccessor value, DateTimeContext dtc) {
1155 
1156     // handle some common Temporal types
1157     if(value instanceof LocalDateTime) {
1158       return (LocalDateTime)value;
1159     }
1160     if(value instanceof ZonedDateTime) {
1161       // if the temporal value has a timezone, convert it to this db's timezone
1162       return ((ZonedDateTime)value).withZoneSameInstant(
1163           dtc.getZoneId()).toLocalDateTime();
1164     }
1165     if(value instanceof Instant) {
1166       return LocalDateTime.ofInstant((Instant)value, dtc.getZoneId());
1167     }
1168     if(value instanceof LocalDate) {
1169       return ((LocalDate)value).atTime(BASE_LT);
1170     }
1171     if(value instanceof LocalTime) {
1172       return ((LocalTime)value).atDate(BASE_LD);
1173     }
1174 
1175     // generic handling for many other Temporal types
1176     try {
1177 
1178       LocalDate ld = value.query(TemporalQueries.localDate());
1179       if(ld == null) {
1180         ld = BASE_LD;
1181       }
1182       LocalTime lt = value.query(TemporalQueries.localTime());
1183       if(lt == null) {
1184         lt = BASE_LT;
1185       }
1186       ZoneId zone = value.query(TemporalQueries.zone());
1187       if(zone != null) {
1188         // the Temporal has a zone, see if it is the right zone.  if not,
1189         // adjust it
1190         ZoneId zoneId = dtc.getZoneId();
1191         if(!zoneId.equals(zone)) {
1192           return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId)
1193             .toLocalDateTime();
1194         }
1195       }
1196 
1197       return LocalDateTime.of(ld, lt);
1198 
1199     } catch(DateTimeException | ArithmeticException e) {
1200       throw new IllegalArgumentException(
1201           "Unsupported temporal type " + value.getClass(), e);
1202     }
1203   }
1204 
1205   private static Instant toInstant(TemporalAccessor value, DateTimeContext dtc) {
1206     if(value instanceof ZonedDateTime) {
1207       return ((ZonedDateTime)value).toInstant();
1208     }
1209     if(value instanceof Instant) {
1210       return (Instant)value;
1211     }
1212     return temporalToLocalDateTime(value, dtc).atZone(dtc.getZoneId())
1213       .toInstant();
1214   }
1215 
1216   static double toLocalDateDouble(long time) {
1217     time += MILLIS_BETWEEN_EPOCH_AND_1900;
1218 
1219     if(time < 0L) {
1220       // reverse the crazy math described in fromLocalDateDouble
1221       long timePart = -time % MILLISECONDS_PER_DAY;
1222       if(timePart > 0) {
1223         time -= (2 * (MILLISECONDS_PER_DAY - timePart));
1224       }
1225     }
1226 
1227     return time / (double)MILLISECONDS_PER_DAY;
1228   }
1229 
1230   public static double toDateDouble(LocalDateTime ldt) {
1231     Duration dateTimeOffset = Duration.between(BASE_LDT, ldt);
1232     return toLocalDateDouble(dateTimeOffset);
1233   }
1234 
1235   private static double toLocalDateDouble(Duration time) {
1236     long dateTimeSeconds = time.getSeconds();
1237     long timeSeconds = dateTimeSeconds % SECONDS_PER_DAY;
1238     if(timeSeconds < 0) {
1239       timeSeconds += SECONDS_PER_DAY;
1240     }
1241     long dateSeconds = dateTimeSeconds - timeSeconds;
1242     long timeNanos = time.getNano();
1243 
1244     // we have a difficult choice to make here between keeping a value which
1245     // most accurately represents the bits saved and rounding to a value that
1246     // would match what the user would expect too see.  since we do a double
1247     // to long conversion, we end up in a situation where the value might be
1248     // 19.9999 seconds.  access will display this as 20 seconds (access seems
1249     // to only record times to second precision).  if we return 19.9999, then
1250     // when the value is written back out it will be exactly the same double
1251     // (good), but will display as 19 seconds (bad because it looks wrong to
1252     // the user).  on the flip side, if we round, the value will display
1253     // "correctly" to the user, but if the value is written back out, it will
1254     // be a slightly different double value.  this may not be a problem for
1255     // most situations, but may result in incorrect index based lookups.  in
1256     // the old date time handling we use DateExt to store the original bits.
1257     // in jdk8, we cannot extend LocalDateTime.  for now, we will try
1258     // returning the value rounded to milliseconds (technically still more
1259     // precision than access uses but more likely to round trip to the same
1260     // value).
1261     double timeDouble = ((roundToMillis((double)timeNanos / NANOS_PER_SECOND) +
1262                           timeSeconds) / SECONDS_PER_DAY);
1263 
1264     double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY);
1265 
1266     if(dateSeconds < 0) {
1267       timeDouble = -timeDouble;
1268     }
1269 
1270     return dateDouble + timeDouble;
1271   }
1272 
1273   /**
1274    * Rounds the given decimal to milliseconds (3 decimal places) using the
1275    * standard access rounding mode.
1276    */
1277   private static double roundToMillis(double dbl) {
1278     return ((dbl == 0d) ? dbl :
1279             new BigDecimal(dbl).setScale(3, NumberFormatter.ROUND_MODE)
1280             .doubleValue());
1281   }
1282 
1283   /**
1284    * @return an appropriate Date long value for the given object
1285    */
1286   private static long toDateLong(Object value) {
1287     return ((value instanceof Date) ?
1288             ((Date)value).getTime() :
1289             ((value instanceof Calendar) ?
1290              ((Calendar)value).getTimeInMillis() :
1291              ((Number)value).longValue()));
1292   }
1293 
1294   /**
1295    * Gets the timezone offset from UTC to local time for the given time
1296    * (including DST).
1297    */
1298   private static long getToLocalTimeZoneOffset(long time, TimeZone tz) {
1299     return tz.getOffset(time);
1300   }
1301 
1302   /**
1303    * Gets the timezone offset from local time to UTC for the given time
1304    * (including DST).
1305    */
1306   private static long getFromLocalTimeZoneOffset(long time, TimeZone tz) {
1307     // getting from local time back to UTC is a little wonky (and not
1308     // guaranteed to get you back to where you started).  apply the zone
1309     // offset first to get us closer to the original time
1310     return tz.getOffset(time - tz.getRawOffset());
1311   }
1312 
1313   /**
1314    * Decodes a GUID value.
1315    */
1316   private static String readGUIDValue(ByteBuffer buffer, ByteOrder order)
1317   {
1318     if(order != ByteOrder.BIG_ENDIAN) {
1319       byte[] tmpArr = ByteUtil.getBytes(buffer, 16);
1320 
1321         // the first 3 guid components are integer components which need to
1322         // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int
1323       ByteUtil.swap4Bytes(tmpArr, 0);
1324       ByteUtil.swap2Bytes(tmpArr, 4);
1325       ByteUtil.swap2Bytes(tmpArr, 6);
1326       buffer = ByteBuffer.wrap(tmpArr);
1327     }
1328 
1329     StringBuilder sb = new StringBuilder(22);
1330     sb.append("{");
1331     sb.append(ByteUtil.toHexString(buffer, 0, 4,
1332                                    false));
1333     sb.append("-");
1334     sb.append(ByteUtil.toHexString(buffer, 4, 2,
1335                                    false));
1336     sb.append("-");
1337     sb.append(ByteUtil.toHexString(buffer, 6, 2,
1338                                    false));
1339     sb.append("-");
1340     sb.append(ByteUtil.toHexString(buffer, 8, 2,
1341                                    false));
1342     sb.append("-");
1343     sb.append(ByteUtil.toHexString(buffer, 10, 6,
1344                                    false));
1345     sb.append("}");
1346     return (sb.toString());
1347   }
1348 
1349   /**
1350    * Writes a GUID value.
1351    */
1352   private void writeGUIDValue(ByteBuffer buffer, Object value)
1353     throws IOException
1354   {
1355     Matcher m = GUID_PATTERN.matcher(toCharSequence(value));
1356     if(!m.matches()) {
1357       throw new InvalidValueException(
1358           withErrorContext("Invalid GUID: " + value));
1359     }
1360 
1361     ByteBuffer origBuffer = null;
1362     byte[] tmpBuf = null;
1363     if(buffer.order() != ByteOrder.BIG_ENDIAN) {
1364       // write to a temp buf so we can do some swapping below
1365       origBuffer = buffer;
1366       tmpBuf = new byte[16];
1367       buffer = ByteBuffer.wrap(tmpBuf);
1368     }
1369 
1370     ByteUtil.writeHexString(buffer, m.group(1));
1371     ByteUtil.writeHexString(buffer, m.group(2));
1372     ByteUtil.writeHexString(buffer, m.group(3));
1373     ByteUtil.writeHexString(buffer, m.group(4));
1374     ByteUtil.writeHexString(buffer, m.group(5));
1375 
1376     if(tmpBuf != null) {
1377       // the first 3 guid components are integer components which need to
1378       // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int
1379       ByteUtil.swap4Bytes(tmpBuf, 0);
1380       ByteUtil.swap2Bytes(tmpBuf, 4);
1381       ByteUtil.swap2Bytes(tmpBuf, 6);
1382       origBuffer.put(tmpBuf);
1383     }
1384   }
1385 
1386   /**
1387    * Returns {@code true} if the given value is a "guid" value.
1388    */
1389   static boolean isGUIDValue(Object value) throws IOException {
1390     return GUID_PATTERN.matcher(toCharSequence(value)).matches();
1391   }
1392 
1393   /**
1394    * Returns a default value for this column
1395    */
1396   public Object generateDefaultValue() throws IOException {
1397     return ((_defValue != null) ? _defValue.eval() : null);
1398   }
1399 
1400   /**
1401    * Passes the given obj through the currently configured validator for this
1402    * column and returns the result.
1403    */
1404   public Object validate(Object obj) throws IOException {
1405     return _validator.validate(this, obj);
1406   }
1407 
1408   /**
1409    * Returns the context used to manage calculated column values.
1410    */
1411   protected CalcColEvalContext getCalculationContext() {
1412     throw new UnsupportedOperationException();
1413   }
1414 
1415   protected void setCalcColEvalContext(CalcColEvalContext calcCol) {
1416     throw new UnsupportedOperationException();
1417   }
1418 
1419   /**
1420    * Serialize an Object into a raw byte value for this column in little
1421    * endian order
1422    * @param obj Object to serialize
1423    * @return A buffer containing the bytes
1424    * @usage _advanced_method_
1425    */
1426   public ByteBuffer write(Object obj, int remainingRowLength)
1427     throws IOException
1428   {
1429     return write(obj, remainingRowLength, PageChannel.DEFAULT_BYTE_ORDER);
1430   }
1431 
1432   /**
1433    * Serialize an Object into a raw byte value for this column
1434    * @param obj Object to serialize
1435    * @param order Order in which to serialize
1436    * @return A buffer containing the bytes
1437    * @usage _advanced_method_
1438    */
1439   public ByteBuffer write(Object obj, int remainingRowLength, ByteOrder order)
1440     throws IOException
1441   {
1442     if(isRawData(obj)) {
1443       // just slap it right in (not for the faint of heart!)
1444       return ByteBuffer.wrap(((RawData)obj).getBytes());
1445     }
1446 
1447     return writeRealData(obj, remainingRowLength, order);
1448   }
1449 
1450   protected ByteBuffer writeRealData(Object obj, int remainingRowLength,
1451                                      ByteOrder order)
1452     throws IOException
1453   {
1454     if(!isVariableLength() || !getType().isVariableLength()) {
1455       return writeFixedLengthField(obj, order);
1456     }
1457 
1458     // this is an "inline" var length field
1459     switch(getType()) {
1460     case NUMERIC:
1461       // don't ask me why numerics are "var length" columns...
1462       ByteBuffer buffer = PageChannel.createBuffer(
1463           getType().getFixedSize(), order);
1464       writeNumericValue(buffer, obj);
1465       buffer.flip();
1466       return buffer;
1467 
1468     case TEXT:
1469       return encodeTextValue(
1470           obj, 0, getLengthInUnits(), false).order(order);
1471 
1472     case BINARY:
1473     case UNKNOWN_0D:
1474     case UNSUPPORTED_VARLEN:
1475       // should already be "encoded"
1476       break;
1477     default:
1478       throw new RuntimeException(withErrorContext(
1479               "unexpected inline var length type: " + getType()));
1480     }
1481 
1482     return ByteBuffer.wrap(toByteArray(obj)).order(order);
1483   }
1484 
1485   /**
1486    * Serialize an Object into a raw byte value for this column
1487    * @param obj Object to serialize
1488    * @param order Order in which to serialize
1489    * @return A buffer containing the bytes
1490    * @usage _advanced_method_
1491    */
1492   protected ByteBuffer writeFixedLengthField(Object obj, ByteOrder order)
1493     throws IOException
1494   {
1495     int size = getFixedDataSize();
1496 
1497     ByteBuffer buffer = writeFixedLengthField(
1498         obj, PageChannel.createBuffer(size, order));
1499     buffer.flip();
1500     return buffer;
1501   }
1502 
1503   protected ByteBuffer writeFixedLengthField(Object obj, ByteBuffer buffer)
1504     throws IOException
1505   {
1506     // since booleans are not written by this method, it's safe to convert any
1507     // incoming boolean into an integer.
1508     obj = booleanToInteger(obj);
1509 
1510     switch(getType()) {
1511     case BOOLEAN:
1512       //Do nothing
1513       break;
1514     case  BYTE:
1515       buffer.put(toNumber(obj).byteValue());
1516       break;
1517     case INT:
1518       buffer.putShort(toNumber(obj).shortValue());
1519       break;
1520     case LONG:
1521       buffer.putInt(toNumber(obj).intValue());
1522       break;
1523     case MONEY:
1524       writeCurrencyValue(buffer, obj);
1525       break;
1526     case FLOAT:
1527       buffer.putFloat(toNumber(obj).floatValue());
1528       break;
1529     case DOUBLE:
1530       buffer.putDouble(toNumber(obj).doubleValue());
1531       break;
1532     case SHORT_DATE_TIME:
1533       writeDateValue(buffer, obj);
1534       break;
1535     case TEXT:
1536       // apparently text numeric values are also occasionally written as fixed
1537       // length...
1538       int numChars = getLengthInUnits();
1539       // force uncompressed encoding for fixed length text
1540       buffer.put(encodeTextValue(obj, numChars, numChars, true));
1541       break;
1542     case GUID:
1543       writeGUIDValue(buffer, obj);
1544       break;
1545     case NUMERIC:
1546       // yes, that's right, occasionally numeric values are written as fixed
1547       // length...
1548       writeNumericValue(buffer, obj);
1549       break;
1550     case BINARY:
1551     case UNKNOWN_0D:
1552     case UNKNOWN_11:
1553     case COMPLEX_TYPE:
1554       buffer.putInt(toNumber(obj).intValue());
1555       break;
1556     case BIG_INT:
1557       buffer.putLong(toNumber(obj).longValue());
1558       break;
1559     case EXT_DATE_TIME:
1560       writeExtendedDateValue(buffer, obj);
1561       break;
1562     case UNSUPPORTED_FIXEDLEN:
1563       byte[] bytes = toByteArray(obj);
1564       if(bytes.length != getLength()) {
1565         throw new InvalidValueException(withErrorContext(
1566                                   "Invalid fixed size binary data, size "
1567                                   + getLength() + ", got " + bytes.length));
1568       }
1569       buffer.put(bytes);
1570       break;
1571     default:
1572       throw new IOException(withErrorContext(
1573                                 "Unsupported data type: " + getType()));
1574     }
1575     return buffer;
1576   }
1577 
1578   /**
1579    * Decodes a compressed or uncompressed text value.
1580    */
1581   String decodeTextValue(byte[] data)
1582   {
1583     // see if data is compressed.  the 0xFF, 0xFE sequence indicates that
1584     // compression is used (sort of, see algorithm below)
1585     boolean isCompressed = ((data.length > 1) &&
1586                             (data[0] == TEXT_COMPRESSION_HEADER[0]) &&
1587                             (data[1] == TEXT_COMPRESSION_HEADER[1]));
1588 
1589     if(isCompressed) {
1590 
1591       // this is a whacky compression combo that switches back and forth
1592       // between compressed/uncompressed using a 0x00 byte (starting in
1593       // compressed mode)
1594       StringBuilder textBuf = new StringBuilder(data.length);
1595       // start after two bytes indicating compression use
1596       int dataStart = TEXT_COMPRESSION_HEADER.length;
1597       int dataEnd = dataStart;
1598       boolean inCompressedMode = true;
1599       while(dataEnd < data.length) {
1600         if(data[dataEnd] == (byte)0x00) {
1601 
1602           // handle current segment
1603           decodeTextSegment(data, dataStart, dataEnd, inCompressedMode,
1604                             textBuf);
1605           inCompressedMode = !inCompressedMode;
1606           ++dataEnd;
1607           dataStart = dataEnd;
1608 
1609         } else {
1610           ++dataEnd;
1611         }
1612       }
1613       // handle last segment
1614       decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, textBuf);
1615 
1616       return textBuf.toString();
1617 
1618     }
1619 
1620     return decodeUncompressedText(data, getCharset());
1621   }
1622 
1623   /**
1624    * Decodes a segnment of a text value into the given buffer according to the
1625    * given status of the segment (compressed/uncompressed).
1626    */
1627   private void decodeTextSegment(byte[] data, int dataStart, int dataEnd,
1628                                  boolean inCompressedMode,
1629                                  StringBuilder textBuf)
1630   {
1631     if(dataEnd <= dataStart) {
1632       // no data
1633       return;
1634     }
1635     int dataLength = dataEnd - dataStart;
1636 
1637     if(inCompressedMode) {
1638       byte[] tmpData = new byte[dataLength * 2];
1639       int tmpIdx = 0;
1640       for(int i = dataStart; i < dataEnd; ++i) {
1641         tmpData[tmpIdx] = data[i];
1642         tmpIdx += 2;
1643       }
1644       data = tmpData;
1645       dataStart = 0;
1646       dataLength = data.length;
1647     }
1648 
1649     textBuf.append(decodeUncompressedText(data, dataStart, dataLength,
1650                                           getCharset()));
1651   }
1652 
1653   /**
1654    * @param textBytes bytes of text to decode
1655    * @return the decoded string
1656    */
1657   private static CharBuffer decodeUncompressedText(
1658       byte[] textBytes, int startPos, int length, Charset charset)
1659   {
1660     return charset.decode(ByteBuffer.wrap(textBytes, startPos, length));
1661   }
1662 
1663   /**
1664    * Encodes a text value, possibly compressing.
1665    */
1666   ByteBuffer encodeTextValue(Object obj, int minChars, int maxChars,
1667                              boolean forceUncompressed)
1668     throws IOException
1669   {
1670     CharSequence text = toCharSequence(obj);
1671     if((text.length() > maxChars) || (text.length() < minChars)) {
1672       throw new InvalidValueException(withErrorContext(
1673                             "Text is wrong length for " + getType() +
1674                             " column, max " + maxChars
1675                             + ", min " + minChars + ", got " + text.length()));
1676     }
1677 
1678     // may only compress if column type allows it
1679     if(!forceUncompressed && isCompressedUnicode() &&
1680        (text.length() <= getFormat().MAX_COMPRESSED_UNICODE_SIZE) &&
1681        isUnicodeCompressible(text)) {
1682 
1683       byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length +
1684                                      text.length()];
1685       encodedChars[0] = TEXT_COMPRESSION_HEADER[0];
1686       encodedChars[1] = TEXT_COMPRESSION_HEADER[1];
1687       for(int i = 0; i < text.length(); ++i) {
1688         encodedChars[i + TEXT_COMPRESSION_HEADER.length] =
1689           (byte)text.charAt(i);
1690       }
1691       return ByteBuffer.wrap(encodedChars);
1692     }
1693 
1694     return encodeUncompressedText(text, getCharset());
1695   }
1696 
1697   /**
1698    * Returns {@code true} if the given text can be compressed using compressed
1699    * unicode, {@code false} otherwise.
1700    */
1701   private static boolean isUnicodeCompressible(CharSequence text) {
1702     // only attempt to compress > 2 chars (compressing less than 3 chars would
1703     // not result in a space savings due to the 2 byte compression header)
1704     if(text.length() <= TEXT_COMPRESSION_HEADER.length) {
1705       return false;
1706     }
1707     // now, see if it is all compressible characters
1708     for(int i = 0; i < text.length(); ++i) {
1709       char c = text.charAt(i);
1710       if((c < MIN_COMPRESS_CHAR) || (c > MAX_COMPRESS_CHAR)) {
1711         return false;
1712       }
1713     }
1714     return true;
1715   }
1716 
1717   /**
1718    * Constructs a byte containing the flags for this column.
1719    */
1720   private static byte getColumnBitFlags(ColumnBuilder col) {
1721     byte flags = UPDATABLE_FLAG_MASK;
1722     if(!col.isVariableLength()) {
1723       flags |= FIXED_LEN_FLAG_MASK;
1724     }
1725     if(col.isAutoNumber()) {
1726       byte autoNumFlags = 0;
1727       switch(col.getType()) {
1728       case LONG:
1729       case COMPLEX_TYPE:
1730         autoNumFlags = AUTO_NUMBER_FLAG_MASK;
1731         break;
1732       case GUID:
1733         autoNumFlags = AUTO_NUMBER_GUID_FLAG_MASK;
1734         break;
1735       default:
1736         // unknown autonum type
1737       }
1738       flags |= autoNumFlags;
1739     }
1740     if(col.isHyperlink()) {
1741       flags |= HYPERLINK_FLAG_MASK;
1742     }
1743     return flags;
1744   }
1745 
1746   @Override
1747   public String toString() {
1748     ToStringBuilder sb = CustomToStringStyle.builder(this)
1749       .append("name", "(" + _table.getName() + ") " + _name);
1750     byte typeValue = getOriginalDataType();
1751     sb.append("type", "0x" + Integer.toHexString(typeValue) +
1752               " (" + _type + ")")
1753       .append("number", _columnNumber)
1754       .append("length", _columnLength)
1755       .append("variableLength", _variableLength);
1756     if(_calculated) {
1757       sb.append("calculated", _calculated)
1758         .append("expression",
1759                 CustomToStringStyle.ignoreNull(getCalculationContext()));
1760     }
1761     if(_type.isTextual()) {
1762       sb.append("compressedUnicode", isCompressedUnicode())
1763         .append("textSortOrder", getTextSortOrder());
1764       if(getTextCodePage() > 0) {
1765         sb.append("textCodePage", getTextCodePage());
1766       }
1767       if(isAppendOnly()) {
1768         sb.append("appendOnly", isAppendOnly());
1769       }
1770       if(isHyperlink()) {
1771         sb.append("hyperlink", isHyperlink());
1772       }
1773     }
1774     if(_type.getHasScalePrecision()) {
1775       sb.append("precision", getPrecision())
1776         .append("scale", getScale());
1777     }
1778     if(_autoNumber) {
1779       sb.append("lastAutoNumber", _autoNumberGenerator.getLast());
1780     }
1781     sb.append("complexInfo", CustomToStringStyle.ignoreNull(getComplexInfo()))
1782       .append("validator", CustomToStringStyle.ignoreNull(
1783                   ((_validator != SimpleColumnValidator.INSTANCE) ?
1784                    _validator : null)))
1785       .append("defaultValue", CustomToStringStyle.ignoreNull(_defValue));
1786     return sb.toString();
1787   }
1788 
1789   /**
1790    * @param textBytes bytes of text to decode
1791    * @param charset relevant charset
1792    * @return the decoded string
1793    * @usage _advanced_method_
1794    */
1795   public static String decodeUncompressedText(byte[] textBytes,
1796                                               Charset charset)
1797   {
1798     return decodeUncompressedText(textBytes, 0, textBytes.length, charset)
1799       .toString();
1800   }
1801 
1802   /**
1803    * @param text Text to encode
1804    * @param charset database charset
1805    * @return A buffer with the text encoded
1806    * @usage _advanced_method_
1807    */
1808   public static ByteBuffer encodeUncompressedText(CharSequence text,
1809                                                   Charset charset)
1810   {
1811     CharBuffer cb = ((text instanceof CharBuffer) ?
1812                      (CharBuffer)text : CharBuffer.wrap(text));
1813     return charset.encode(cb);
1814   }
1815 
1816 
1817   /**
1818    * Orders Columns by column number.
1819    * @usage _general_method_
1820    */
1821   @Override
1822   public int compareTo(ColumnImpl other) {
1823     if (_columnNumber > other.getColumnNumber()) {
1824       return 1;
1825     } else if (_columnNumber < other.getColumnNumber()) {
1826       return -1;
1827     } else {
1828       return 0;
1829     }
1830   }
1831 
1832   /**
1833    * @param columns A list of columns in a table definition
1834    * @return The number of variable length columns found in the list
1835    * @usage _advanced_method_
1836    */
1837   public static short countVariableLength(List<ColumnBuilder> columns) {
1838     short rtn = 0;
1839     for (ColumnBuilder col : columns) {
1840       if (col.isVariableLength()) {
1841         rtn++;
1842       }
1843     }
1844     return rtn;
1845   }
1846 
1847   /**
1848    * @return an appropriate BigDecimal representation of the given object.
1849    *         <code>null</code> is returned as 0 and Numbers are converted
1850    *         using their double representation.
1851    */
1852   BigDecimal toBigDecimal(Object value)
1853   {
1854     return toBigDecimal(value, getDatabase());
1855   }
1856 
1857   /**
1858    * @return an appropriate BigDecimal representation of the given object.
1859    *         <code>null</code> is returned as 0 and Numbers are converted
1860    *         using their double representation.
1861    */
1862   static BigDecimal toBigDecimal(Object value, DatabaseImpl db)
1863   {
1864     if(value == null) {
1865       return BigDecimal.ZERO;
1866     } else if(value instanceof BigDecimal) {
1867       return (BigDecimal)value;
1868     } else if(value instanceof BigInteger) {
1869       return new BigDecimal((BigInteger)value);
1870     } else if(value instanceof Number) {
1871       return new BigDecimal(((Number)value).doubleValue());
1872     } else if(value instanceof Boolean) {
1873       // access seems to like -1 for true and 0 for false
1874       return ((Boolean)value) ? BigDecimal.valueOf(-1) : BigDecimal.ZERO;
1875     } else if(value instanceof Date) {
1876       return new BigDecimal(toDateDouble(value, db));
1877     } else if(value instanceof LocalDateTime) {
1878       return new BigDecimal(toDateDouble((LocalDateTime)value));
1879     }
1880     return new BigDecimal(value.toString());
1881   }
1882 
1883   /**
1884    * @return an appropriate Number representation of the given object.
1885    *         <code>null</code> is returned as 0 and Strings are parsed as
1886    *         Doubles.
1887    */
1888   private Number toNumber(Object value)
1889   {
1890     return toNumber(value, getDatabase());
1891   }
1892 
1893   /**
1894    * @return an appropriate Number representation of the given object.
1895    *         <code>null</code> is returned as 0 and Strings are parsed as
1896    *         Doubles.
1897    */
1898   private static Number toNumber(Object value, DatabaseImpl db)
1899   {
1900     if(value == null) {
1901       return BigDecimal.ZERO;
1902     } else if(value instanceof Number) {
1903       return (Number)value;
1904     } else if(value instanceof Boolean) {
1905       // access seems to like -1 for true and 0 for false
1906       return ((Boolean)value) ? -1 : 0;
1907     } else if(value instanceof Date) {
1908       return toDateDouble(value, db);
1909     } else if(value instanceof LocalDateTime) {
1910       return toDateDouble((LocalDateTime)value);
1911     }
1912     return Double.valueOf(value.toString());
1913   }
1914 
1915   /**
1916    * @return an appropriate CharSequence representation of the given object.
1917    * @usage _advanced_method_
1918    */
1919   public static CharSequence toCharSequence(Object value)
1920     throws IOException
1921   {
1922     if(value == null) {
1923       return null;
1924     } else if(value instanceof CharSequence) {
1925       return (CharSequence)value;
1926     } else if(SqlHelper.INSTANCE.isClob(value)) {
1927       return SqlHelper.INSTANCE.getClobString(value);
1928     } else if(value instanceof Reader) {
1929       char[] buf = new char[8 * 1024];
1930       StringBuilder sout = new StringBuilder();
1931       Reader in = (Reader)value;
1932       int read = 0;
1933       while((read = in.read(buf)) != -1) {
1934         sout.append(buf, 0, read);
1935       }
1936       return sout;
1937     }
1938 
1939     return value.toString();
1940   }
1941 
1942   /**
1943    * @return an appropriate byte[] representation of the given object.
1944    * @usage _advanced_method_
1945    */
1946   public static byte[] toByteArray(Object value)
1947     throws IOException
1948   {
1949     if(value == null) {
1950       return null;
1951     } else if(value instanceof byte[]) {
1952       return (byte[])value;
1953     } else if(value instanceof InMemoryBlob) {
1954       return ((InMemoryBlob)value).getBytes();
1955     } else if(SqlHelper.INSTANCE.isBlob(value)) {
1956       return SqlHelper.INSTANCE.getBlobBytes(value);
1957     }
1958 
1959     ByteArrayOutputStream bout = new ByteArrayOutputStream();
1960 
1961     if(value instanceof InputStream) {
1962       ByteUtil.copy((InputStream)value, bout);
1963     } else {
1964       // if all else fails, serialize it
1965       try(ObjectOutputStream oos = new ObjectOutputStream(bout)){
1966         oos.writeObject(value);
1967       }
1968     }
1969 
1970     return bout.toByteArray();
1971   }
1972 
1973   /**
1974    * Interpret a boolean value (null == false)
1975    * @usage _advanced_method_
1976    */
1977   public static boolean toBooleanValue(Object obj) {
1978     if(obj == null) {
1979       return false;
1980     } else if(obj instanceof Boolean) {
1981       return ((Boolean)obj).booleanValue();
1982     } else if(obj instanceof Number) {
1983       // Access considers 0 as "false"
1984       if(obj instanceof BigDecimal) {
1985         return (((BigDecimal)obj).compareTo(BigDecimal.ZERO) != 0);
1986       }
1987       if(obj instanceof BigInteger) {
1988         return (((BigInteger)obj).compareTo(BigInteger.ZERO) != 0);
1989       }
1990       return (((Number)obj).doubleValue() != 0.0d);
1991     }
1992     return Boolean.parseBoolean(obj.toString());
1993   }
1994 
1995   /**
1996    * Swaps the bytes of the given numeric in place.
1997    */
1998   private static void fixNumericByteOrder(byte[] bytes)
1999   {
2000     // fix endianness of each 4 byte segment
2001     for(int i = 0; i < bytes.length; i+=4) {
2002       ByteUtil.swap4Bytes(bytes, i);
2003     }
2004   }
2005 
2006   /**
2007    * Treat booleans as integers (access-style).
2008    */
2009   protected static Object booleanToInteger(Object obj) {
2010     if (obj instanceof Boolean) {
2011       obj = ((Boolean) obj) ? -1 : 0;
2012     }
2013     return obj;
2014   }
2015 
2016   /**
2017    * Returns a wrapper for raw column data that can be written without
2018    * understanding the data.  Useful for wrapping unparseable data for
2019    * re-writing.
2020    */
2021   public static RawData rawDataWrapper(byte[] bytes) {
2022     return new RawData(bytes);
2023   }
2024 
2025   /**
2026    * Returns {@code true} if the given value is "raw" column data,
2027    * {@code false} otherwise.
2028    * @usage _advanced_method_
2029    */
2030   public static boolean isRawData(Object value) {
2031     return(value instanceof RawData);
2032   }
2033 
2034   /**
2035    * Writes the column definitions into a table definition buffer.
2036    * @param buffer Buffer to write to
2037    */
2038   protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer)
2039   {
2040     // we specifically put the "long variable" values after the normal
2041     // variable length values so that we have a better chance of fitting it
2042     // all (because "long variable" values can go in separate pages)
2043     int longVariableOffset = creator.countNonLongVariableLength();
2044     creator.setColumnOffsets(0, 0, longVariableOffset);
2045 
2046     for (ColumnBuilder col : creator.getColumns()) {
2047       writeDefinition(creator, col, buffer);
2048     }
2049 
2050     for (ColumnBuilder col : creator.getColumns()) {
2051       TableImpl.writeName(buffer, col.getName(), creator.getCharset());
2052     }
2053   }
2054 
2055   protected static void writeDefinition(
2056       TableMutator mutator, ColumnBuilder col, ByteBuffer buffer)
2057   {
2058     TableMutator.ColumnOffsets colOffsets = mutator.getColumnOffsets();
2059 
2060     buffer.put(col.getType().getValue());
2061     buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER);  //constant magic number
2062     buffer.putShort(col.getColumnNumber());  //Column Number
2063 
2064     buffer.putShort(colOffsets.getNextVariableOffset(col));
2065 
2066     buffer.putShort(col.getColumnNumber()); //Column Number again
2067 
2068     if(col.getType().isTextual()) {
2069       // this will write 4 bytes (note we don't support writing dbs which
2070       // use the text code page)
2071       writeSortOrder(buffer, col.getTextSortOrder(), mutator.getFormat());
2072     } else {
2073       // note scale/precision not stored for calculated numeric fields
2074       if(col.getType().getHasScalePrecision() && !col.isCalculated()) {
2075         buffer.put(col.getPrecision());  // numeric precision
2076         buffer.put(col.getScale());  // numeric scale
2077       } else {
2078         buffer.put((byte) 0x00); //unused
2079         buffer.put((byte) 0x00); //unused
2080       }
2081       buffer.putShort((short) 0); //Unknown
2082     }
2083 
2084     buffer.put(getColumnBitFlags(col)); // misc col flags
2085 
2086     // note access doesn't seem to allow unicode compression for calced fields
2087     if(col.isCalculated()) {
2088       buffer.put(CALCULATED_EXT_FLAG_MASK);
2089     } else if (col.isCompressedUnicode()) {  //Compressed
2090       buffer.put(COMPRESSED_UNICODE_EXT_FLAG_MASK);
2091     } else {
2092       buffer.put((byte)0);
2093     }
2094 
2095     buffer.putInt(0); //Unknown, but always 0.
2096 
2097     //Offset for fixed length columns
2098     if(col.isVariableLength()) {
2099       buffer.putShort((short) 0);
2100     } else {
2101       buffer.putShort(colOffsets.getNextFixedOffset(col));
2102     }
2103 
2104     if(!col.getType().isLongValue()) {
2105       short length = col.getLength();
2106       if(col.isCalculated()) {
2107         // calced columns have additional value overhead
2108         if(!col.getType().isVariableLength() ||
2109            col.getType().getHasScalePrecision()) {
2110           length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN;
2111         } else {
2112           length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN;
2113         }
2114       }
2115       buffer.putShort(length); //Column length
2116     } else {
2117       buffer.putShort((short)0x0000); // unused
2118     }
2119   }
2120 
2121   protected static void writeColUsageMapDefinitions(
2122       TableCreator creator, ByteBuffer buffer)
2123   {
2124     // write long value column usage map references
2125     for(ColumnBuilder lvalCol : creator.getLongValueColumns()) {
2126       writeColUsageMapDefinition(creator, lvalCol, buffer);
2127     }
2128   }
2129 
2130   protected static void writeColUsageMapDefinition(
2131       TableMutator creator, ColumnBuilder lvalCol, ByteBuffer buffer)
2132   {
2133     TableMutator.ColumnState colState = creator.getColumnState(lvalCol);
2134 
2135     buffer.putShort(lvalCol.getColumnNumber());
2136 
2137     // owned pages umap (both are on same page)
2138     buffer.put(colState.getUmapOwnedRowNumber());
2139     ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
2140     // free space pages umap
2141     buffer.put(colState.getUmapFreeRowNumber());
2142     ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
2143   }
2144 
2145   /**
2146    * Reads the sort order info from the given buffer from the given position.
2147    */
2148   static SortOrder readSortOrder(ByteBuffer buffer, int position,
2149                                  JetFormat format)
2150   {
2151     short value = buffer.getShort(position);
2152 
2153     if(value == 0) {
2154       // probably a file we wrote, before handling sort order
2155       return format.DEFAULT_SORT_ORDER;
2156     }
2157 
2158     short version = format.DEFAULT_SORT_ORDER.getVersion();
2159     if(format.SIZE_SORT_ORDER == 4) {
2160       version = buffer.get(position + 3);
2161     }
2162 
2163     if(value == GENERAL_SORT_ORDER_VALUE) {
2164       if(version == GENERAL_SORT_ORDER.getVersion()) {
2165         return GENERAL_SORT_ORDER;
2166       }
2167       if(version == GENERAL_LEGACY_SORT_ORDER.getVersion()) {
2168         return GENERAL_LEGACY_SORT_ORDER;
2169       }
2170       if(version == GENERAL_97_SORT_ORDER.getVersion()) {
2171         return GENERAL_97_SORT_ORDER;
2172       }
2173     }
2174     return new SortOrder(value, version);
2175   }
2176 
2177   /**
2178    * Reads the column cade page info from the given buffer, if supported for
2179    * this db.
2180    */
2181   static short readCodePage(ByteBuffer buffer, int offset, JetFormat format)
2182   {
2183       int cpOffset = format.OFFSET_COLUMN_CODE_PAGE;
2184       return ((cpOffset >= 0) ? buffer.getShort(offset + cpOffset) : 0);
2185   }
2186 
2187   /**
2188    * Read the extra flags field for a column definition.
2189    */
2190   static byte readExtraFlags(ByteBuffer buffer, int offset, JetFormat format)
2191   {
2192     int extFlagsOffset = format.OFFSET_COLUMN_EXT_FLAGS;
2193     return ((extFlagsOffset >= 0) ? buffer.get(offset + extFlagsOffset) : 0);
2194   }
2195 
2196   /**
2197    * Writes the sort order info to the given buffer at the current position.
2198    */
2199   private static void writeSortOrder(ByteBuffer buffer, SortOrder sortOrder,
2200                                      JetFormat format) {
2201     if(sortOrder == null) {
2202       sortOrder = format.DEFAULT_SORT_ORDER;
2203     }
2204     buffer.putShort(sortOrder.getValue());
2205     if(format.SIZE_SORT_ORDER == 4) {
2206       buffer.put((byte)0x00); // unknown
2207       buffer.put((byte)sortOrder.getVersion());
2208     }
2209   }
2210 
2211   /**
2212    * Returns {@code true} if the value is immutable, {@code false} otherwise.
2213    * This only handles values that are returned from the {@link #read} method.
2214    */
2215   static boolean isImmutableValue(Object value) {
2216     // for now, the only mutable value this class returns is byte[]
2217     return !(value instanceof byte[]);
2218   }
2219 
2220   /**
2221    * Converts the given value to the "internal" representation for the given
2222    * data type.
2223    */
2224   public static Object toInternalValue(DataType dataType, Object value,
2225                                        DatabaseImpl db)
2226     throws IOException
2227   {
2228     return toInternalValue(dataType, value, db, null);
2229   }
2230 
2231   static Object toInternalValue(DataType dataType, Object value,
2232                                 DatabaseImpl db,
2233                                 ColumnImpl.DateTimeFactory factory)
2234     throws IOException
2235   {
2236     if(value == null) {
2237       return null;
2238     }
2239 
2240     switch(dataType) {
2241     case BOOLEAN:
2242       return ((value instanceof Boolean) ? value : toBooleanValue(value));
2243     case BYTE:
2244       return ((value instanceof Byte) ? value : toNumber(value, db).byteValue());
2245     case INT:
2246       return ((value instanceof Short) ? value :
2247               toNumber(value, db).shortValue());
2248     case LONG:
2249       return ((value instanceof Integer) ? value :
2250               toNumber(value, db).intValue());
2251     case MONEY:
2252       return toBigDecimal(value, db);
2253     case FLOAT:
2254       return ((value instanceof Float) ? value :
2255               toNumber(value, db).floatValue());
2256     case DOUBLE:
2257       return ((value instanceof Double) ? value :
2258               toNumber(value, db).doubleValue());
2259     case SHORT_DATE_TIME:
2260       if(factory == null) {
2261         factory = db.getDateTimeFactory();
2262       }
2263       return factory.toInternalValue(db, value);
2264     case TEXT:
2265     case MEMO:
2266     case GUID:
2267       return ((value instanceof String) ? value :
2268               toCharSequence(value).toString());
2269     case NUMERIC:
2270       return toBigDecimal(value, db);
2271     case COMPLEX_TYPE:
2272       // leave alone for now?
2273       return value;
2274     case BIG_INT:
2275       return ((value instanceof Long) ? value :
2276               toNumber(value, db).longValue());
2277     case EXT_DATE_TIME:
2278       return toLocalDateTime(value, db);
2279     default:
2280       // some variation of binary data
2281       return toByteArray(value);
2282     }
2283   }
2284 
2285   protected static DateTimeFactory getDateTimeFactory(DateTimeType type) {
2286     return ((type == DateTimeType.LOCAL_DATE_TIME) ?
2287             LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY);
2288   }
2289 
2290   String withErrorContext(String msg) {
2291     return withErrorContext(msg, getDatabase(), getTable().getName(), getName());
2292   }
2293 
2294   boolean isThisColumn(Identifier identifier) {
2295     return(getTable().isThisTable(identifier) &&
2296            getName().equalsIgnoreCase(identifier.getObjectName()));
2297   }
2298 
2299   private static String withErrorContext(
2300       String msg, DatabaseImpl db, String tableName, String colName) {
2301     return msg + " (Db=" + db.getName() + ";Table=" + tableName + ";Column=" +
2302       colName + ")";
2303   }
2304 
2305   /**
2306    * Date subclass which stashes the original date bits, in case we attempt to
2307    * re-write the value (will not lose precision).  Also, this implementation
2308    * is immutable.
2309    */
2310   @SuppressWarnings("deprecation")
2311   private static final class DateExt extends Date
2312   {
2313     private static final long serialVersionUID = 0L;
2314 
2315     /** cached bits of the original date value */
2316     private transient final long _dateBits;
2317 
2318     private DateExt(long time, long dateBits) {
2319       super(time);
2320       _dateBits = dateBits;
2321     }
2322 
2323     public long getDateBits() {
2324       return _dateBits;
2325     }
2326 
2327     @Override
2328     public void setDate(int time) {
2329       throw new UnsupportedOperationException();
2330     }
2331 
2332     @Override
2333     public void setHours(int time) {
2334       throw new UnsupportedOperationException();
2335     }
2336 
2337     @Override
2338     public void setMinutes(int time) {
2339       throw new UnsupportedOperationException();
2340     }
2341 
2342     @Override
2343     public void setMonth(int time) {
2344       throw new UnsupportedOperationException();
2345     }
2346 
2347     @Override
2348     public void setSeconds(int time) {
2349       throw new UnsupportedOperationException();
2350     }
2351 
2352     @Override
2353     public void setYear(int time) {
2354       throw new UnsupportedOperationException();
2355     }
2356 
2357     @Override
2358     public void setTime(long time) {
2359       throw new UnsupportedOperationException();
2360     }
2361 
2362     private Object writeReplace() throws ObjectStreamException {
2363       // if we are going to serialize this Date, convert it back to a normal
2364       // Date (in case it is restored outside of the context of jackcess)
2365       return new Date(super.getTime());
2366     }
2367   }
2368 
2369   /**
2370    * Wrapper for raw column data which can be re-written.
2371    */
2372   private static final class RawData implements Serializable, InMemoryBlob
2373   {
2374     private static final long serialVersionUID = 0L;
2375 
2376     private final byte[] _bytes;
2377 
2378     private RawData(byte[] bytes) {
2379       _bytes = bytes;
2380     }
2381 
2382     @Override
2383     public byte[] getBytes() {
2384       return _bytes;
2385     }
2386 
2387     @Override
2388     public String toString() {
2389       return CustomToStringStyle.valueBuilder(this)
2390         .append(null, getBytes())
2391         .toString();
2392     }
2393 
2394     private Object writeReplace() throws ObjectStreamException {
2395       // if we are going to serialize this, convert it back to a normal
2396       // byte[] (in case it is restored outside of the context of jackcess)
2397       return getBytes();
2398     }
2399   }
2400 
2401   /**
2402    * Base class for the supported autonumber types.
2403    * @usage _advanced_class_
2404    */
2405   public abstract class AutoNumberGenerator
2406   {
2407     protected AutoNumberGenerator() {}
2408 
2409     /**
2410      * Returns the last autonumber generated by this generator.  Only valid
2411      * after a call to {@link Table#addRow}, otherwise undefined.
2412      */
2413     public abstract Object getLast();
2414 
2415     /**
2416      * Returns the next autonumber for this generator.
2417      * <p>
2418      * <i>Warning, calling this externally will result in this value being
2419      * "lost" for the table.</i>
2420      */
2421     public abstract Object getNext(TableImpl.WriteRowState writeRowState);
2422 
2423     /**
2424      * Returns a valid autonumber for this generator.
2425      * <p>
2426      * <i>Warning, calling this externally may result in this value being
2427      * "lost" for the table.</i>
2428      */
2429     public abstract Object handleInsert(
2430         TableImpl.WriteRowState writeRowState, Object inRowValue)
2431       throws IOException;
2432 
2433     /**
2434      * Restores a previous autonumber generated by this generator.
2435      */
2436     public abstract void restoreLast(Object last);
2437 
2438     /**
2439      * Returns the type of values generated by this generator.
2440      */
2441     public abstract DataType getType();
2442   }
2443 
2444   private final class LongAutoNumberGenerator extends AutoNumberGenerator
2445   {
2446     private LongAutoNumberGenerator() {}
2447 
2448     @Override
2449     public Object getLast() {
2450       // the table stores the last long autonumber used
2451       return getTable().getLastLongAutoNumber();
2452     }
2453 
2454     @Override
2455     public Object getNext(TableImpl.WriteRowState writeRowState) {
2456       // the table stores the last long autonumber used
2457       return getTable().getNextLongAutoNumber();
2458     }
2459 
2460     @Override
2461     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2462                                Object inRowValue)
2463       throws IOException
2464     {
2465       int inAutoNum = toNumber(inRowValue).intValue();
2466       if(inAutoNum <= INVALID_AUTO_NUMBER &&
2467          !getTable().isAllowAutoNumberInsert()) {
2468         throw new InvalidValueException(withErrorContext(
2469                 "Invalid auto number value " + inAutoNum));
2470       }
2471       // the table stores the last long autonumber used
2472       getTable().adjustLongAutoNumber(inAutoNum);
2473       return inAutoNum;
2474     }
2475 
2476     @Override
2477     public void restoreLast(Object last) {
2478       if(last instanceof Integer) {
2479         getTable().restoreLastLongAutoNumber((Integer)last);
2480       }
2481     }
2482 
2483     @Override
2484     public DataType getType() {
2485       return DataType.LONG;
2486     }
2487   }
2488 
2489   private final class GuidAutoNumberGenerator extends AutoNumberGenerator
2490   {
2491     private Object _lastAutoNumber;
2492 
2493     private GuidAutoNumberGenerator() {}
2494 
2495     @Override
2496     public Object getLast() {
2497       return _lastAutoNumber;
2498     }
2499 
2500     @Override
2501     public Object getNext(TableImpl.WriteRowState writeRowState) {
2502       // format guids consistently w/ Column.readGUIDValue()
2503       _lastAutoNumber = "{" + UUID.randomUUID() + "}";
2504       return _lastAutoNumber;
2505     }
2506 
2507     @Override
2508     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2509                                Object inRowValue)
2510       throws IOException
2511     {
2512       _lastAutoNumber = toCharSequence(inRowValue);
2513       return _lastAutoNumber;
2514     }
2515 
2516     @Override
2517     public void restoreLast(Object last) {
2518       _lastAutoNumber = null;
2519     }
2520 
2521     @Override
2522     public DataType getType() {
2523       return DataType.GUID;
2524     }
2525   }
2526 
2527   private final class ComplexTypeAutoNumberGenerator extends AutoNumberGenerator
2528   {
2529     private ComplexTypeAutoNumberGenerator() {}
2530 
2531     @Override
2532     public Object getLast() {
2533       // the table stores the last ComplexType autonumber used
2534       return getTable().getLastComplexTypeAutoNumber();
2535     }
2536 
2537     @Override
2538     public Object getNext(TableImpl.WriteRowState writeRowState) {
2539       // same value is shared across all ComplexType values in a row
2540       int nextComplexAutoNum = writeRowState.getComplexAutoNumber();
2541       if(nextComplexAutoNum <= INVALID_AUTO_NUMBER) {
2542         // the table stores the last ComplexType autonumber used
2543         nextComplexAutoNum = getTable().getNextComplexTypeAutoNumber();
2544         writeRowState.setComplexAutoNumber(nextComplexAutoNum);
2545       }
2546       return new ComplexValueForeignKeyImpl(ColumnImpl.this,
2547                                             nextComplexAutoNum);
2548     }
2549 
2550     @Override
2551     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2552                                Object inRowValue)
2553       throws IOException
2554     {
2555       ComplexValueForeignKey inComplexFK = null;
2556       if(inRowValue instanceof ComplexValueForeignKey) {
2557         inComplexFK = (ComplexValueForeignKey)inRowValue;
2558       } else {
2559         inComplexFK = new ComplexValueForeignKeyImpl(
2560             ColumnImpl.this, toNumber(inRowValue).intValue());
2561       }
2562 
2563       if(inComplexFK.getColumn() != ColumnImpl.this) {
2564         throw new InvalidValueException(withErrorContext(
2565                 "Wrong column for complex value foreign key, found " +
2566                 inComplexFK.getColumn().getName()));
2567       }
2568       if(inComplexFK.get() < 1) {
2569         throw new InvalidValueException(withErrorContext(
2570                 "Invalid complex value foreign key value " + inComplexFK.get()));
2571       }
2572       // same value is shared across all ComplexType values in a row
2573       int prevRowValue = writeRowState.getComplexAutoNumber();
2574       if(prevRowValue <= INVALID_AUTO_NUMBER) {
2575         writeRowState.setComplexAutoNumber(inComplexFK.get());
2576       } else if(prevRowValue != inComplexFK.get()) {
2577         throw new InvalidValueException(withErrorContext(
2578                 "Inconsistent complex value foreign key values: found " +
2579                 prevRowValue + ", given " + inComplexFK));
2580       }
2581 
2582       // the table stores the last ComplexType autonumber used
2583       getTable().adjustComplexTypeAutoNumber(inComplexFK.get());
2584 
2585       return inComplexFK;
2586     }
2587 
2588     @Override
2589     public void restoreLast(Object last) {
2590       if(last instanceof ComplexValueForeignKey) {
2591         getTable().restoreLastComplexTypeAutoNumber(
2592             ((ComplexValueForeignKey)last).get());
2593       }
2594     }
2595 
2596     @Override
2597     public DataType getType() {
2598       return DataType.COMPLEX_TYPE;
2599     }
2600   }
2601 
2602   private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator
2603   {
2604     private final DataType _genType;
2605 
2606     private UnsupportedAutoNumberGenerator(DataType genType) {
2607       _genType = genType;
2608     }
2609 
2610     @Override
2611     public Object getLast() {
2612       return null;
2613     }
2614 
2615     @Override
2616     public Object getNext(TableImpl.WriteRowState writeRowState) {
2617       throw new UnsupportedOperationException();
2618     }
2619 
2620     @Override
2621     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2622                                Object inRowValue) {
2623       throw new UnsupportedOperationException();
2624     }
2625 
2626     @Override
2627     public void restoreLast(Object last) {
2628       throw new UnsupportedOperationException();
2629     }
2630 
2631     @Override
2632     public DataType getType() {
2633       return _genType;
2634     }
2635   }
2636 
2637 
2638   /**
2639    * Information about the sort order (collation) for a textual column.
2640    * @usage _intermediate_class_
2641    */
2642   public static final class SortOrder
2643   {
2644     private final short _value;
2645     private final short _version;
2646 
2647     public SortOrder(short value, short version) {
2648       _value = value;
2649       _version = version;
2650     }
2651 
2652     public short getValue() {
2653       return _value;
2654     }
2655 
2656     public short getVersion() {
2657       return _version;
2658     }
2659 
2660     @Override
2661     public int hashCode() {
2662       return _value;
2663     }
2664 
2665     @Override
2666     public boolean equals(Object o) {
2667       return ((this == o) ||
2668               ((o != null) && (getClass() == o.getClass()) &&
2669                (_value == ((SortOrder)o)._value) &&
2670                (_version == ((SortOrder)o)._version)));
2671     }
2672 
2673     @Override
2674     public String toString() {
2675       LocaleUtil.LcidInfo info = LocaleUtil.getInfo(_value);
2676       String valueStr = _value + "(" + _version + ")";
2677       return CustomToStringStyle.valueBuilder(this)
2678         .append(null, (info != null) ? (valueStr + ", " + info) : valueStr)
2679         .toString();
2680     }
2681   }
2682 
2683   /**
2684    * Utility struct for passing params through ColumnImpl constructors.
2685    */
2686   static final class InitArgs
2687   {
2688     public final TableImpl table;
2689     public final ByteBuffer buffer;
2690     public final int offset;
2691     public final String name;
2692     public final int displayIndex;
2693     public final byte colType;
2694     public final byte flags;
2695     public final byte extFlags;
2696     public DataType type;
2697 
2698     InitArgs(TableImpl newTable, ByteBuffer newBuffer, int newOffset,
2699              String newName, int newDisplayIndex) {
2700       this.table = newTable;
2701       this.buffer = newBuffer;
2702       this.offset = newOffset;
2703       this.name = newName;
2704       this.displayIndex = newDisplayIndex;
2705 
2706       this.colType = buffer.get(offset + table.getFormat().OFFSET_COLUMN_TYPE);
2707       this.flags = buffer.get(offset + table.getFormat().OFFSET_COLUMN_FLAGS);
2708       this.extFlags = readExtraFlags(buffer, offset, table.getFormat());
2709     }
2710   }
2711 
2712   /**
2713    * "Internal" column validator for columns with the "required" property
2714    * enabled.
2715    */
2716   private static final class RequiredColValidator extends InternalColumnValidator
2717   {
2718     private RequiredColValidator(ColumnValidator delegate) {
2719       super(delegate);
2720     }
2721 
2722     @Override
2723     protected Object internalValidate(Column col, Object val)
2724       throws IOException
2725     {
2726       if(val == null) {
2727         throw new InvalidValueException(
2728             ((ColumnImpl)col).withErrorContext(
2729                 "Missing value for required column"));
2730       }
2731       return val;
2732     }
2733 
2734     @Override
2735     protected void appendToString(StringBuilder sb) {
2736       sb.append("required=true");
2737     }
2738   }
2739 
2740   /**
2741    * "Internal" column validator for text columns with the "allow zero len"
2742    * property disabled.
2743    */
2744   private static final class NoZeroLenColValidator extends InternalColumnValidator
2745   {
2746     private NoZeroLenColValidator(ColumnValidator delegate) {
2747       super(delegate);
2748     }
2749 
2750     @Override
2751     protected Object internalValidate(Column col, Object val)
2752       throws IOException
2753     {
2754       CharSequence valStr = toCharSequence(val);
2755       // oddly enough null is allowed for non-zero len strings
2756       if((valStr != null) && valStr.length() == 0) {
2757         throw new InvalidValueException(
2758             ((ColumnImpl)col).withErrorContext(
2759                 "Zero length string is not allowed"));
2760       }
2761       return valStr;
2762     }
2763 
2764     @Override
2765     protected void appendToString(StringBuilder sb) {
2766       sb.append("allowZeroLength=false");
2767     }
2768   }
2769 
2770   /**
2771    * Factory which handles date/time values appropriately for a DateTimeType.
2772    */
2773   protected static abstract class DateTimeFactory
2774   {
2775     public abstract DateTimeType getType();
2776 
2777     public abstract Object fromDateBits(ColumnImpl col, long dateBits);
2778 
2779     public abstract double toDateDouble(Object value, DateTimeContext dtc);
2780 
2781     public abstract Object toInternalValue(DatabaseImpl db, Object value);
2782   }
2783 
2784   /**
2785    * Factory impl for legacy Date handling.
2786    */
2787   private static final class DefaultDateTimeFactory extends DateTimeFactory
2788   {
2789     @Override
2790     public DateTimeType getType() {
2791       return DateTimeType.DATE;
2792     }
2793 
2794     @Override
2795     public Object fromDateBits(ColumnImpl col, long dateBits) {
2796       long time = col.fromDateDouble(
2797           Double.longBitsToDouble(dateBits));
2798       return new DateExt(time, dateBits);
2799     }
2800 
2801     @Override
2802     public double toDateDouble(Object value, DateTimeContext dtc) {
2803       // ZoneId and TimeZone have different rules for older timezones, so we
2804       // need to consistently use one or the other depending on the date/time
2805       // type
2806       long time = 0L;
2807       if(value instanceof TemporalAccessor) {
2808         time = toInstant((TemporalAccessor)value, dtc).toEpochMilli();
2809       } else {
2810         time = toDateLong(value);
2811       }
2812       // seems access stores dates in the local timezone.  guess you just
2813       // hope you read it in the same timezone in which it was written!
2814       time += getToLocalTimeZoneOffset(time, dtc.getTimeZone());
2815       return toLocalDateDouble(time);
2816     }
2817 
2818     @Override
2819     public Object toInternalValue(DatabaseImpl db, Object value) {
2820       return ((value instanceof Date) ? value :
2821               new Date(toDateLong(value)));
2822     }
2823   }
2824 
2825   /**
2826    * Factory impl for LocalDateTime handling.
2827    */
2828   private static final class LDTDateTimeFactory extends DateTimeFactory
2829   {
2830     @Override
2831     public DateTimeType getType() {
2832       return DateTimeType.LOCAL_DATE_TIME;
2833     }
2834 
2835     @Override
2836     public Object fromDateBits(ColumnImpl col, long dateBits) {
2837       return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits));
2838     }
2839 
2840     @Override
2841     public double toDateDouble(Object value, DateTimeContext dtc) {
2842       // ZoneId and TimeZone have different rules for older timezones, so we
2843       // need to consistently use one or the other depending on the date/time
2844       // type
2845       if(!(value instanceof TemporalAccessor)) {
2846         value = Instant.ofEpochMilli(toDateLong(value));
2847       }
2848       return ColumnImpl.toDateDouble(
2849           temporalToLocalDateTime((TemporalAccessor)value, dtc));
2850     }
2851 
2852     @Override
2853     public Object toInternalValue(DatabaseImpl db, Object value) {
2854       return toLocalDateTime(value, db);
2855     }
2856   }
2857 
2858   /** internal interface for types which hold bytes in memory */
2859   static interface InMemoryBlob {
2860     public byte[] getBytes() throws IOException;
2861   }
2862 }