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