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).  a format which
1691     // holds text in a code page has no compressed form, and the same two
1692     // bytes are two characters of the code page there
1693     boolean isCompressed = (!getFormat().CODE_PAGE_CHARSET &&
1694                             (data.length > 1) &&
1695                             (data[0] == TEXT_COMPRESSION_HEADER[0]) &&
1696                             (data[1] == TEXT_COMPRESSION_HEADER[1]));
1697 
1698     if(isCompressed) {
1699 
1700       // this is a whacky compression combo that switches back and forth
1701       // between compressed/uncompressed using a 0x00 byte (starting in
1702       // compressed mode)
1703       StringBuilder textBuf = new StringBuilder(data.length);
1704       // start after two bytes indicating compression use
1705       int dataStart = TEXT_COMPRESSION_HEADER.length;
1706       int dataEnd = dataStart;
1707       boolean inCompressedMode = true;
1708       while(dataEnd < data.length) {
1709         if(data[dataEnd] == (byte)0x00) {
1710 
1711           // handle current segment
1712           decodeTextSegment(data, dataStart, dataEnd, inCompressedMode,
1713                             textBuf);
1714           inCompressedMode = !inCompressedMode;
1715           ++dataEnd;
1716           dataStart = dataEnd;
1717 
1718         } else {
1719           // an uncompressed character is two bytes wide and the byte which
1720           // marks the change back is one, so the 00 of a character such as
1721           // "d", which is 64 00, is not one.  stepping over the whole
1722           // character is what tells the two apart
1723           dataEnd += (inCompressedMode ? 1 : 2);
1724         }
1725       }
1726       // handle last segment
1727       decodeTextSegment(data, dataStart, data.length, inCompressedMode,
1728                         textBuf);
1729 
1730       return textBuf.toString();
1731 
1732     }
1733 
1734     return decodeUncompressedText(data, getCharset());
1735   }
1736 
1737   /**
1738    * Decodes a segnment of a text value into the given buffer according to the
1739    * given status of the segment (compressed/uncompressed).
1740    */
1741   private void decodeTextSegment(byte[] data, int dataStart, int dataEnd,
1742                                  boolean inCompressedMode,
1743                                  StringBuilder textBuf)
1744   {
1745     if(dataEnd <= dataStart) {
1746       // no data
1747       return;
1748     }
1749     int dataLength = dataEnd - dataStart;
1750 
1751     if(inCompressedMode) {
1752       byte[] tmpData = new byte[dataLength * 2];
1753       int tmpIdx = 0;
1754       for(int i = dataStart; i < dataEnd; ++i) {
1755         tmpData[tmpIdx] = data[i];
1756         tmpIdx += 2;
1757       }
1758       data = tmpData;
1759       dataStart = 0;
1760       dataLength = data.length;
1761     }
1762 
1763     textBuf.append(decodeUncompressedText(data, dataStart, dataLength,
1764                                           getCharset()));
1765   }
1766 
1767   /**
1768    * @param textBytes bytes of text to decode
1769    * @return the decoded string
1770    */
1771   private static CharBuffer decodeUncompressedText(
1772       byte[] textBytes, int startPos, int length, Charset charset)
1773   {
1774     return charset.decode(ByteBuffer.wrap(textBytes, startPos, length));
1775   }
1776 
1777   /**
1778    * Encodes a text value, possibly compressing.
1779    */
1780   ByteBuffer encodeTextValue(Object obj, int minChars, int maxChars,
1781                              boolean forceUncompressed)
1782     throws IOException
1783   {
1784     CharSequence text = toCharSequence(obj);
1785     if((text.length() > maxChars) || (text.length() < minChars)) {
1786       throw new InvalidValueException(withErrorContext(
1787                             "Text is wrong length for " + getType() +
1788                             " column, max " + maxChars
1789                             + ", min " + minChars + ", got " + text.length()));
1790     }
1791 
1792     // may only compress if column type allows it
1793     if(!forceUncompressed && isCompressedUnicode() &&
1794        (text.length() <= getFormat().MAX_COMPRESSED_UNICODE_SIZE) &&
1795        isUnicodeCompressible(text)) {
1796 
1797       byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length +
1798                                      text.length()];
1799       encodedChars[0] = TEXT_COMPRESSION_HEADER[0];
1800       encodedChars[1] = TEXT_COMPRESSION_HEADER[1];
1801       for(int i = 0; i < text.length(); ++i) {
1802         encodedChars[i + TEXT_COMPRESSION_HEADER.length] =
1803           (byte)text.charAt(i);
1804       }
1805       return ByteBuffer.wrap(encodedChars);
1806     }
1807 
1808     return encodeUncompressedText(text, getCharset());
1809   }
1810 
1811   /**
1812    * Returns {@code true} if the given text can be compressed using compressed
1813    * unicode, {@code false} otherwise.
1814    */
1815   private static boolean isUnicodeCompressible(CharSequence text) {
1816     // only attempt to compress > 2 chars (compressing less than 3 chars would
1817     // not result in a space savings due to the 2 byte compression header)
1818     if(text.length() <= TEXT_COMPRESSION_HEADER.length) {
1819       return false;
1820     }
1821     // now, see if it is all compressible characters
1822     for(int i = 0; i < text.length(); ++i) {
1823       char c = text.charAt(i);
1824       if((c < MIN_COMPRESS_CHAR) || (c > MAX_COMPRESS_CHAR)) {
1825         return false;
1826       }
1827     }
1828     return true;
1829   }
1830 
1831   /**
1832    * Constructs a byte containing the flags for this column.
1833    */
1834   private static byte getColumnBitFlags(ColumnBuilder col) {
1835     byte flags = UPDATABLE_FLAG_MASK;
1836     if(!col.isVariableLength()) {
1837       flags |= FIXED_LEN_FLAG_MASK;
1838     }
1839     if(col.isAutoNumber()) {
1840       byte autoNumFlags = 0;
1841       switch(col.getType()) {
1842       case LONG:
1843       case COMPLEX_TYPE:
1844         autoNumFlags = AUTO_NUMBER_FLAG_MASK;
1845         break;
1846       case GUID:
1847         autoNumFlags = AUTO_NUMBER_GUID_FLAG_MASK;
1848         break;
1849       default:
1850         // unknown autonum type
1851       }
1852       flags |= autoNumFlags;
1853     }
1854     if(col.isHyperlink()) {
1855       flags |= HYPERLINK_FLAG_MASK;
1856     }
1857     return flags;
1858   }
1859 
1860   @Override
1861   public String toString() {
1862     ToStringBuilder sb = ToStringBuilder.builder(this)
1863       .append("name", "(" + _table.getName() + ") " + _name);
1864     byte typeValue = getOriginalDataType();
1865     sb.append("type", "0x" + Integer.toHexString(typeValue) +
1866               " (" + _type + ")")
1867       .append("number", _columnNumber)
1868       .append("id", _columnId)
1869       .append("length", _columnLength)
1870       .append("variableLength", _variableLength);
1871     if(_hidden) {
1872       sb.append("hidden", _hidden);
1873     }
1874     if(_securityIdentifier) {
1875       sb.append("securityIdentifier", _securityIdentifier);
1876     }
1877     if(_calculated) {
1878       sb.append("calculated", _calculated)
1879         .appendIfNotNull("expression", getCalculationContext());
1880     }
1881     if(_type.isTextual()) {
1882       sb.append("compressedUnicode", isCompressedUnicode())
1883         .append("textSortOrder", getTextSortOrder());
1884       if(getTextCodePage() > 0) {
1885         sb.append("textCodePage", getTextCodePage());
1886       }
1887       if(isAppendOnly()) {
1888         sb.append("appendOnly", isAppendOnly());
1889       }
1890       if(isHyperlink()) {
1891         sb.append("hyperlink", isHyperlink());
1892       }
1893     }
1894     if(_type.getHasScalePrecision()) {
1895       sb.append("precision", getPrecision())
1896         .append("scale", getScale());
1897     }
1898     if(_autoNumber) {
1899       sb.append("lastAutoNumber", _autoNumberGenerator.getLast());
1900     }
1901     sb.appendIfNotNull("complexInfo", getComplexInfo())
1902       .appendIfNotNull("validator",
1903                        ((_validator != SimpleColumnValidator.INSTANCE) ?
1904                         _validator : null))
1905       .appendIfNotNull("defaultValue", _defValue);
1906     return sb.toString();
1907   }
1908 
1909   /**
1910    * @param textBytes bytes of text to decode
1911    * @param charset relevant charset
1912    * @return the decoded string
1913    * @usage _advanced_method_
1914    */
1915   public static String decodeUncompressedText(byte[] textBytes,
1916                                               Charset charset)
1917   {
1918     return decodeUncompressedText(textBytes, 0, textBytes.length, charset)
1919       .toString();
1920   }
1921 
1922   /**
1923    * @param text Text to encode
1924    * @param charset database charset
1925    * @return A buffer with the text encoded
1926    * @usage _advanced_method_
1927    */
1928   public static ByteBuffer encodeUncompressedText(CharSequence text,
1929                                                   Charset charset)
1930   {
1931     CharBuffer cb = ((text instanceof CharBuffer) ?
1932                      (CharBuffer)text : CharBuffer.wrap(text));
1933     return charset.encode(cb);
1934   }
1935 
1936   /**
1937    * @param columns A list of columns in a table definition
1938    * @return The number of variable length columns found in the list
1939    * @usage _advanced_method_
1940    */
1941   public static short countVariableLength(List<ColumnBuilder> columns) {
1942     short rtn = 0;
1943     for (ColumnBuilder col : columns) {
1944       if (col.isVariableLength()) {
1945         rtn++;
1946       }
1947     }
1948     return rtn;
1949   }
1950 
1951   /**
1952    * @return an appropriate BigDecimal representation of the given object.
1953    *         <code>null</code> is returned as 0 and Numbers are converted
1954    *         using their double representation.
1955    */
1956   BigDecimal toBigDecimal(Object value)
1957   {
1958     return toBigDecimal(value, getDatabase());
1959   }
1960 
1961   /**
1962    * @return an appropriate BigDecimal representation of the given object.
1963    *         <code>null</code> is returned as 0 and Numbers are converted
1964    *         using their double representation.
1965    */
1966   static BigDecimal toBigDecimal(Object value, DatabaseImpl db)
1967   {
1968     if(value == null) {
1969       return BigDecimal.ZERO;
1970     } else if(value instanceof BigDecimal) {
1971       return (BigDecimal)value;
1972     } else if(value instanceof BigInteger) {
1973       return new BigDecimal((BigInteger)value);
1974     } else if(value instanceof Number) {
1975       return new BigDecimal(((Number)value).doubleValue());
1976     } else if(value instanceof Boolean) {
1977       // access seems to like -1 for true and 0 for false
1978       return ((Boolean)value) ? BigDecimal.valueOf(-1) : BigDecimal.ZERO;
1979     } else if(value instanceof Date) {
1980       return new BigDecimal(toDateDouble(value, db));
1981     } else if(value instanceof LocalDateTime) {
1982       return new BigDecimal(toDateDouble((LocalDateTime)value));
1983     }
1984     return new BigDecimal(value.toString());
1985   }
1986 
1987   /**
1988    * @return an appropriate Number representation of the given object.
1989    *         <code>null</code> is returned as 0 and Strings are parsed as
1990    *         Doubles.
1991    */
1992   private Number toNumber(Object value)
1993   {
1994     return toNumber(value, getDatabase());
1995   }
1996 
1997   /**
1998    * @return an appropriate Number representation of the given object.
1999    *         <code>null</code> is returned as 0 and Strings are parsed as
2000    *         Doubles.
2001    */
2002   private static Number toNumber(Object value, DatabaseImpl db)
2003   {
2004     if(value == null) {
2005       return BigDecimal.ZERO;
2006     } else if(value instanceof Number) {
2007       return (Number)value;
2008     } else if(value instanceof Boolean) {
2009       // access seems to like -1 for true and 0 for false
2010       return ((Boolean)value) ? -1 : 0;
2011     } else if(value instanceof Date) {
2012       return toDateDouble(value, db);
2013     } else if(value instanceof LocalDateTime) {
2014       return toDateDouble((LocalDateTime)value);
2015     }
2016     return Double.valueOf(value.toString());
2017   }
2018 
2019   /**
2020    * @return an appropriate CharSequence representation of the given object.
2021    * @usage _advanced_method_
2022    */
2023   public static CharSequence toCharSequence(Object value)
2024     throws IOException
2025   {
2026     if(value == null) {
2027       return null;
2028     } else if(value instanceof CharSequence) {
2029       return (CharSequence)value;
2030     } else if(SqlHelper.INSTANCE.isClob(value)) {
2031       return SqlHelper.INSTANCE.getClobString(value);
2032     } else if(value instanceof Reader) {
2033       char[] buf = new char[8 * 1024];
2034       StringBuilder sout = new StringBuilder();
2035       Reader in = (Reader)value;
2036       int read = 0;
2037       while((read = in.read(buf)) != -1) {
2038         sout.append(buf, 0, read);
2039       }
2040       return sout;
2041     }
2042 
2043     return value.toString();
2044   }
2045 
2046   /**
2047    * @return an appropriate byte[] representation of the given object.
2048    * @usage _advanced_method_
2049    */
2050   public static byte[] toByteArray(Object value)
2051     throws IOException
2052   {
2053     if(value == null) {
2054       return null;
2055     } else if(value instanceof byte[]) {
2056       return (byte[])value;
2057     } else if(value instanceof InMemoryBlob) {
2058       return ((InMemoryBlob)value).getBytes();
2059     } else if(SqlHelper.INSTANCE.isBlob(value)) {
2060       return SqlHelper.INSTANCE.getBlobBytes(value);
2061     }
2062 
2063     ByteArrayOutputStream bout = new ByteArrayOutputStream();
2064 
2065     if(value instanceof InputStream) {
2066       ByteUtil.copy((InputStream)value, bout);
2067     } else {
2068       // if all else fails, serialize it
2069       try(ObjectOutputStream oos = new ObjectOutputStream(bout)){
2070         oos.writeObject(value);
2071       }
2072     }
2073 
2074     return bout.toByteArray();
2075   }
2076 
2077   /**
2078    * Interpret a boolean value (null == false)
2079    * @usage _advanced_method_
2080    */
2081   public static boolean toBooleanValue(Object obj) {
2082     if(obj == null) {
2083       return false;
2084     } else if(obj instanceof Boolean) {
2085       return ((Boolean)obj).booleanValue();
2086     } else if(obj instanceof Number) {
2087       // Access considers 0 as "false"
2088       if(obj instanceof BigDecimal) {
2089         return (((BigDecimal)obj).compareTo(BigDecimal.ZERO) != 0);
2090       }
2091       if(obj instanceof BigInteger) {
2092         return (((BigInteger)obj).compareTo(BigInteger.ZERO) != 0);
2093       }
2094       return (((Number)obj).doubleValue() != 0.0d);
2095     }
2096     return Boolean.parseBoolean(obj.toString());
2097   }
2098 
2099   /**
2100    * Swaps the bytes of the given numeric in place.
2101    */
2102   private static void fixNumericByteOrder(byte[] bytes)
2103   {
2104     // fix endianness of each 4 byte segment
2105     for(int i = 0; i < bytes.length; i+=4) {
2106       ByteUtil.swap4Bytes(bytes, i);
2107     }
2108   }
2109 
2110   /**
2111    * Treat booleans as integers (access-style).
2112    */
2113   protected static Object booleanToInteger(Object obj) {
2114     if (obj instanceof Boolean) {
2115       obj = ((Boolean) obj) ? -1 : 0;
2116     }
2117     return obj;
2118   }
2119 
2120   /**
2121    * Returns a wrapper for raw column data that can be written without
2122    * understanding the data.  Useful for wrapping unparseable data for
2123    * re-writing.
2124    */
2125   public static RawData rawDataWrapper(byte[] bytes) {
2126     return new RawData(bytes);
2127   }
2128 
2129   /**
2130    * Returns {@code true} if the given value is "raw" column data,
2131    * {@code false} otherwise.
2132    * @usage _advanced_method_
2133    */
2134   public static boolean isRawData(Object value) {
2135     return(value instanceof RawData);
2136   }
2137 
2138   /**
2139    * Writes the column definitions into a table definition buffer.
2140    * @param buffer Buffer to write to
2141    */
2142   protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer)
2143   {
2144     // we specifically put the "long variable" values after the normal
2145     // variable length values so that we have a better chance of fitting it
2146     // all (because "long variable" values can go in separate pages)
2147     int longVariableOffset = creator.countNonLongVariableLength();
2148     creator.setColumnOffsets(0, 0, longVariableOffset);
2149 
2150     for (ColumnBuilder col : creator.getColumns()) {
2151       writeDefinition(creator, col, buffer);
2152     }
2153 
2154     for (ColumnBuilder col : creator.getColumns()) {
2155       TableImpl.writeName(buffer, col.getName(), creator.getCharset());
2156     }
2157   }
2158 
2159   protected static void writeDefinition(
2160       TableMutator mutator, ColumnBuilder col, ByteBuffer buffer)
2161   {
2162     TableMutator.ColumnOffsets colOffsets = mutator.getColumnOffsets();
2163     // only a column jackcess works something out for has a state
2164     TableMutator.ColumnState colState = mutator.getColumnState(col);
2165 
2166     buffer.put(col.getType().getValue());
2167     buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER);  //constant magic number
2168     buffer.putShort(col.getColumnNumber());  //Column Number
2169 
2170     buffer.putShort(colOffsets.getNextVariableOffset(col));
2171 
2172     buffer.putShort(col.getColumnId()); //Column Id
2173 
2174     if(col.getType().isTextual()) {
2175       // this will write 4 bytes (note we don't support writing dbs which
2176       // use the text code page)
2177       writeSortOrder(buffer, col.getTextSortOrder(), mutator.getFormat());
2178     } else if(col.getType() == DataType.COMPLEX_TYPE) {
2179       // the four bytes which hold the sort order of a text column hold the
2180       // id of the MSysComplexColumns row of a complex one
2181       buffer.putInt(colState.getComplexId());
2182     } else {
2183       // note scale/precision not stored for calculated numeric fields
2184       if(col.getType().getHasScalePrecision() && !col.isCalculated()) {
2185         buffer.put(col.getPrecision());  // numeric precision
2186         buffer.put(col.getScale());  // numeric scale
2187       } else {
2188         buffer.put((byte) 0x00); //unused
2189         buffer.put((byte) 0x00); //unused
2190       }
2191       // the collation variant, which is always zero, and the sort order
2192       // version, which access writes on a non-text column only in jet 14
2193       buffer.putShort((short) 0);
2194     }
2195 
2196     buffer.put(getColumnBitFlags(col)); // misc col flags
2197 
2198     // note access doesn't seem to allow unicode compression for calced fields
2199     byte extFlags = ((colState != null) ? colState.getExtraFlags() : 0);
2200     ComplexColumnDesc complexDesc = col.getComplexDesc();
2201     if((complexDesc != null) &&
2202        (complexDesc.getType() == ComplexDataType.VERSION_HISTORY)) {
2203       // this bit tells ms access the column keeps the history of a memo column
2204       // rather than holding values of its own
2205       extFlags |= VERSION_HISTORY_EXT_FLAG_MASK;
2206     }
2207     if(col.isCalculated()) {
2208       extFlags |= CALCULATED_EXT_FLAG_MASK;
2209     } else if (col.isCompressedUnicode()) {  //Compressed
2210       extFlags |= COMPRESSED_UNICODE_EXT_FLAG_MASK;
2211     }
2212     buffer.put(extFlags);
2213 
2214     buffer.putInt(0); // always 0
2215 
2216     //Offset for fixed length columns
2217     if(col.isVariableLength()) {
2218       buffer.putShort((short) 0);
2219     } else {
2220       buffer.putShort(colOffsets.getNextFixedOffset(col));
2221     }
2222 
2223     if(!col.getType().isLongValue()) {
2224       short length = col.getLength();
2225       if(col.isCalculated()) {
2226         // calced columns have additional value overhead
2227         if(!col.getType().isVariableLength() ||
2228            col.getType().getHasScalePrecision()) {
2229           length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN;
2230         } else {
2231           length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN;
2232         }
2233       }
2234       buffer.putShort(length); //Column length
2235     } else {
2236       buffer.putShort((short)0x0000); // unused
2237     }
2238   }
2239 
2240   protected static void writeColUsageMapDefinitions(
2241       TableCreator creator, ByteBuffer buffer)
2242   {
2243     // write long value column usage map references
2244     for(ColumnBuilder lvalCol : creator.getLongValueColumns()) {
2245       writeColUsageMapDefinition(creator, lvalCol, buffer);
2246     }
2247   }
2248 
2249   protected static void writeColUsageMapDefinition(
2250       TableMutator creator, ColumnBuilder lvalCol, ByteBuffer buffer)
2251   {
2252     TableMutator.ColumnState colState = creator.getColumnState(lvalCol);
2253 
2254     buffer.putShort(lvalCol.getColumnNumber());
2255 
2256     // owned pages umap (both are on same page)
2257     buffer.put(colState.getUmapOwnedRowNumber());
2258     ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
2259     // free space pages umap
2260     buffer.put(colState.getUmapFreeRowNumber());
2261     ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
2262   }
2263 
2264   /**
2265    * Reads the sort order info from the given buffer from the given position.
2266    */
2267   static SortOrder readSortOrder(ByteBuffer buffer, int position,
2268                                  JetFormat format)
2269   {
2270     short value = buffer.getShort(position);
2271 
2272     if(value == 0) {
2273       // probably a file we wrote, before handling sort order
2274       return format.DEFAULT_SORT_ORDER;
2275     }
2276 
2277     short variant = 0;
2278     short version = format.DEFAULT_SORT_ORDER.getVersion();
2279     if(format.SIZE_SORT_ORDER == 4) {
2280       // the four bytes are the ms access sort id: the lcid read above, then
2281       // the variant within the collation and the weight table family
2282       variant = (short)ByteUtil.getUnsignedByte(buffer, position + 2);
2283       version = buffer.get(position + 3);
2284     }
2285 
2286     if((value == GENERAL_SORT_ORDER_VALUE) && (variant == 0)) {
2287       if(version == GENERAL_SORT_ORDER.getVersion()) {
2288         return GENERAL_SORT_ORDER;
2289       }
2290       if(version == GENERAL_LEGACY_SORT_ORDER.getVersion()) {
2291         return GENERAL_LEGACY_SORT_ORDER;
2292       }
2293       if(version == GENERAL_97_SORT_ORDER.getVersion()) {
2294         return GENERAL_97_SORT_ORDER;
2295       }
2296     }
2297     return new SortOrder(value, variant, version);
2298   }
2299 
2300   /**
2301    * Reads the column cade page info from the given buffer, if supported for
2302    * this db.
2303    */
2304   static short readCodePage(ByteBuffer buffer, int offset, JetFormat format)
2305   {
2306       int cpOffset = format.OFFSET_COLUMN_CODE_PAGE;
2307       return ((cpOffset >= 0) ? buffer.getShort(offset + cpOffset) : 0);
2308   }
2309 
2310   /**
2311    * Read the extra flags field for a column definition.
2312    */
2313   static byte readExtraFlags(ByteBuffer buffer, int offset, JetFormat format)
2314   {
2315     int extFlagsOffset = format.OFFSET_COLUMN_EXT_FLAGS;
2316     return ((extFlagsOffset >= 0) ? buffer.get(offset + extFlagsOffset) : 0);
2317   }
2318 
2319   /**
2320    * Writes the sort order info to the given buffer at the current position.
2321    */
2322   static void writeSortOrder(ByteBuffer buffer, SortOrder sortOrder,
2323                              JetFormat format) {
2324     if(sortOrder == null) {
2325       sortOrder = format.DEFAULT_SORT_ORDER;
2326     }
2327     buffer.putShort(sortOrder.getValue());
2328     if(format.SIZE_SORT_ORDER == 4) {
2329       buffer.put((byte)sortOrder.getVariant());
2330       buffer.put((byte)sortOrder.getVersion());
2331     }
2332   }
2333 
2334   /**
2335    * Returns {@code true} if the value is immutable, {@code false} otherwise.
2336    * This only handles values that are returned from the {@link #read} method.
2337    */
2338   static boolean isImmutableValue(Object value) {
2339     // for now, the only mutable value this class returns is byte[]
2340     return !(value instanceof byte[]);
2341   }
2342 
2343   /**
2344    * Converts the given value to the "internal" representation for the given
2345    * data type.
2346    */
2347   public static Object toInternalValue(DataType dataType, Object value,
2348                                        DatabaseImpl db)
2349     throws IOException
2350   {
2351     return toInternalValue(dataType, value, db, null);
2352   }
2353 
2354   static Object toInternalValue(DataType dataType, Object value,
2355                                 DatabaseImpl db,
2356                                 ColumnImpl.DateTimeFactory factory)
2357     throws IOException
2358   {
2359     if(value == null) {
2360       return null;
2361     }
2362 
2363     switch(dataType) {
2364     case BOOLEAN:
2365       return ((value instanceof Boolean) ? value : toBooleanValue(value));
2366     case BYTE:
2367       return ((value instanceof Byte) ? value : toNumber(value, db).byteValue());
2368     case INT:
2369       return ((value instanceof Short) ? value :
2370               toNumber(value, db).shortValue());
2371     case LONG:
2372       return ((value instanceof Integer) ? value :
2373               toNumber(value, db).intValue());
2374     case MONEY:
2375       return toBigDecimal(value, db);
2376     case FLOAT:
2377       return ((value instanceof Float) ? value :
2378               toNumber(value, db).floatValue());
2379     case DOUBLE:
2380       return ((value instanceof Double) ? value :
2381               toNumber(value, db).doubleValue());
2382     case SHORT_DATE_TIME:
2383       if(factory == null) {
2384         factory = db.getDateTimeFactory();
2385       }
2386       return factory.toInternalValue(db, value);
2387     case TEXT:
2388     case MEMO:
2389     case GUID:
2390       return ((value instanceof String) ? value :
2391               toCharSequence(value).toString());
2392     case NUMERIC:
2393       return toBigDecimal(value, db);
2394     case COMPLEX_TYPE:
2395       // leave alone for now?
2396       return value;
2397     case BIG_INT:
2398       return ((value instanceof Long) ? value :
2399               toNumber(value, db).longValue());
2400     case EXT_DATE_TIME:
2401       return toLocalDateTime(value, db);
2402     default:
2403       // some variation of binary data
2404       return toByteArray(value);
2405     }
2406   }
2407 
2408   protected static DateTimeFactory getDateTimeFactory(DateTimeType type) {
2409     return ((type == DateTimeType.LOCAL_DATE_TIME) ?
2410             LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY);
2411   }
2412 
2413   String withErrorContext(String msg) {
2414     return withErrorContext(msg, getDatabase(), getTable().getName(), getName());
2415   }
2416 
2417   boolean isThisColumn(Identifier identifier) {
2418     return(getTable().isThisTable(identifier) &&
2419            getName().equalsIgnoreCase(identifier.getObjectName()));
2420   }
2421 
2422   private static String withErrorContext(
2423       String msg, DatabaseImpl db, String tableName, String colName) {
2424     return msg + " (Db=" + db.getName() + ";Table=" + tableName + ";Column=" +
2425       colName + ")";
2426   }
2427 
2428   /**
2429    * Date subclass which stashes the original date bits, in case we attempt to
2430    * re-write the value (will not lose precision).  Also, this implementation
2431    * is immutable.
2432    */
2433   @SuppressWarnings("deprecation")
2434   private static final class DateExt extends Date
2435   {
2436     private static final long serialVersionUID = 0L;
2437 
2438     /** cached bits of the original date value */
2439     private transient final long _dateBits;
2440 
2441     private DateExt(long time, long dateBits) {
2442       super(time);
2443       _dateBits = dateBits;
2444     }
2445 
2446     public long getDateBits() {
2447       return _dateBits;
2448     }
2449 
2450     @Override
2451     public void setDate(int time) {
2452       throw new UnsupportedOperationException();
2453     }
2454 
2455     @Override
2456     public void setHours(int time) {
2457       throw new UnsupportedOperationException();
2458     }
2459 
2460     @Override
2461     public void setMinutes(int time) {
2462       throw new UnsupportedOperationException();
2463     }
2464 
2465     @Override
2466     public void setMonth(int time) {
2467       throw new UnsupportedOperationException();
2468     }
2469 
2470     @Override
2471     public void setSeconds(int time) {
2472       throw new UnsupportedOperationException();
2473     }
2474 
2475     @Override
2476     public void setYear(int time) {
2477       throw new UnsupportedOperationException();
2478     }
2479 
2480     @Override
2481     public void setTime(long time) {
2482       throw new UnsupportedOperationException();
2483     }
2484 
2485     private Object writeReplace() throws ObjectStreamException {
2486       // if we are going to serialize this Date, convert it back to a normal
2487       // Date (in case it is restored outside of the context of jackcess)
2488       return new Date(super.getTime());
2489     }
2490   }
2491 
2492   /**
2493    * Wrapper for raw column data which can be re-written.
2494    */
2495   private static final class RawData implements Serializable, InMemoryBlob
2496   {
2497     private static final long serialVersionUID = 0L;
2498 
2499     private final byte[] _bytes;
2500 
2501     private RawData(byte[] bytes) {
2502       _bytes = bytes;
2503     }
2504 
2505     @Override
2506     public byte[] getBytes() {
2507       return _bytes;
2508     }
2509 
2510     @Override
2511     public String toString() {
2512       return ToStringBuilder.valueBuilder(this)
2513         .append(null, getBytes())
2514         .toString();
2515     }
2516 
2517     private Object writeReplace() throws ObjectStreamException {
2518       // if we are going to serialize this, convert it back to a normal
2519       // byte[] (in case it is restored outside of the context of jackcess)
2520       return getBytes();
2521     }
2522   }
2523 
2524   /**
2525    * Base class for the supported autonumber types.
2526    * @usage _advanced_class_
2527    */
2528   public abstract class AutoNumberGenerator
2529   {
2530     protected AutoNumberGenerator() {}
2531 
2532     /**
2533      * Returns the last autonumber generated by this generator.  Only valid
2534      * after a call to {@link Table#addRow}, otherwise undefined.
2535      */
2536     public abstract Object getLast();
2537 
2538     /**
2539      * Returns the next autonumber for this generator.
2540      * <p>
2541      * <i>Warning, calling this externally will result in this value being
2542      * "lost" for the table.</i>
2543      */
2544     public abstract Object getNext(TableImpl.WriteRowState writeRowState);
2545 
2546     /**
2547      * Returns a valid autonumber for this generator.
2548      * <p>
2549      * <i>Warning, calling this externally may result in this value being
2550      * "lost" for the table.</i>
2551      */
2552     public abstract Object handleInsert(
2553         TableImpl.WriteRowState writeRowState, Object inRowValue)
2554       throws IOException;
2555 
2556     /**
2557      * Restores a previous autonumber generated by this generator.
2558      */
2559     public abstract void restoreLast(Object last);
2560 
2561     /**
2562      * Returns the type of values generated by this generator.
2563      */
2564     public abstract DataType getType();
2565   }
2566 
2567   private final class LongAutoNumberGenerator extends AutoNumberGenerator
2568   {
2569     private LongAutoNumberGenerator() {}
2570 
2571     @Override
2572     public Object getLast() {
2573       // the table stores the last long autonumber used
2574       return getTable().getLastLongAutoNumber();
2575     }
2576 
2577     @Override
2578     public Object getNext(TableImpl.WriteRowState writeRowState) {
2579       // the table stores the last long autonumber used
2580       return getTable().getNextLongAutoNumber();
2581     }
2582 
2583     @Override
2584     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2585                                Object inRowValue)
2586       throws IOException
2587     {
2588       int inAutoNum = toNumber(inRowValue).intValue();
2589       if(inAutoNum <= INVALID_AUTO_NUMBER &&
2590          !getTable().isAllowAutoNumberInsert()) {
2591         throw new InvalidValueException(withErrorContext(
2592                 "Invalid auto number value " + inAutoNum));
2593       }
2594       // the table stores the last long autonumber used
2595       getTable().adjustLongAutoNumber(inAutoNum);
2596       return inAutoNum;
2597     }
2598 
2599     @Override
2600     public void restoreLast(Object last) {
2601       if(last instanceof Integer) {
2602         getTable().restoreLastLongAutoNumber((Integer)last);
2603       }
2604     }
2605 
2606     @Override
2607     public DataType getType() {
2608       return DataType.LONG;
2609     }
2610   }
2611 
2612   private final class GuidAutoNumberGenerator extends AutoNumberGenerator
2613   {
2614     private Object _lastAutoNumber;
2615 
2616     private GuidAutoNumberGenerator() {}
2617 
2618     @Override
2619     public Object getLast() {
2620       return _lastAutoNumber;
2621     }
2622 
2623     @Override
2624     public Object getNext(TableImpl.WriteRowState writeRowState) {
2625       _lastAutoNumber = toGUIDString(UUID.randomUUID());
2626       return _lastAutoNumber;
2627     }
2628 
2629     @Override
2630     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2631                                Object inRowValue)
2632       throws IOException
2633     {
2634       _lastAutoNumber = toCharSequence(inRowValue);
2635       return _lastAutoNumber;
2636     }
2637 
2638     @Override
2639     public void restoreLast(Object last) {
2640       _lastAutoNumber = null;
2641     }
2642 
2643     @Override
2644     public DataType getType() {
2645       return DataType.GUID;
2646     }
2647   }
2648 
2649   private final class ComplexTypeAutoNumberGenerator extends AutoNumberGenerator
2650   {
2651     private ComplexTypeAutoNumberGenerator() {}
2652 
2653     @Override
2654     public Object getLast() {
2655       // the table stores the last ComplexType autonumber used
2656       return getTable().getLastComplexTypeAutoNumber();
2657     }
2658 
2659     @Override
2660     public Object getNext(TableImpl.WriteRowState writeRowState) {
2661       // same value is shared across all ComplexType values in a row
2662       int nextComplexAutoNum = writeRowState.getComplexAutoNumber();
2663       if(nextComplexAutoNum <= INVALID_AUTO_NUMBER) {
2664         // the table stores the last ComplexType autonumber used
2665         nextComplexAutoNum = getTable().getNextComplexTypeAutoNumber();
2666         writeRowState.setComplexAutoNumber(nextComplexAutoNum);
2667       }
2668       return new ComplexValueForeignKeyImpl(ColumnImpl.this,
2669                                             nextComplexAutoNum);
2670     }
2671 
2672     @Override
2673     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2674                                Object inRowValue)
2675       throws IOException
2676     {
2677       ComplexValueForeignKey inComplexFK = null;
2678       if(inRowValue instanceof ComplexValueForeignKey) {
2679         inComplexFK = (ComplexValueForeignKey)inRowValue;
2680       } else {
2681         inComplexFK = new ComplexValueForeignKeyImpl(
2682             ColumnImpl.this, toNumber(inRowValue).intValue());
2683       }
2684 
2685       if(inComplexFK.getColumn() != ColumnImpl.this) {
2686         throw new InvalidValueException(withErrorContext(
2687                 "Wrong column for complex value foreign key, found " +
2688                 inComplexFK.getColumn().getName()));
2689       }
2690       if(inComplexFK.get() < 1) {
2691         throw new InvalidValueException(withErrorContext(
2692                 "Invalid complex value foreign key value " + inComplexFK.get()));
2693       }
2694       // same value is shared across all ComplexType values in a row
2695       int prevRowValue = writeRowState.getComplexAutoNumber();
2696       if(prevRowValue <= INVALID_AUTO_NUMBER) {
2697         writeRowState.setComplexAutoNumber(inComplexFK.get());
2698       } else if(prevRowValue != inComplexFK.get()) {
2699         throw new InvalidValueException(withErrorContext(
2700                 "Inconsistent complex value foreign key values: found " +
2701                 prevRowValue + ", given " + inComplexFK));
2702       }
2703 
2704       // the table stores the last ComplexType autonumber used
2705       getTable().adjustComplexTypeAutoNumber(inComplexFK.get());
2706 
2707       return inComplexFK;
2708     }
2709 
2710     @Override
2711     public void restoreLast(Object last) {
2712       if(last instanceof ComplexValueForeignKey) {
2713         getTable().restoreLastComplexTypeAutoNumber(
2714             ((ComplexValueForeignKey)last).get());
2715       }
2716     }
2717 
2718     @Override
2719     public DataType getType() {
2720       return DataType.COMPLEX_TYPE;
2721     }
2722   }
2723 
2724   private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator
2725   {
2726     private final DataType _genType;
2727 
2728     private UnsupportedAutoNumberGenerator(DataType genType) {
2729       _genType = genType;
2730     }
2731 
2732     @Override
2733     public Object getLast() {
2734       return null;
2735     }
2736 
2737     @Override
2738     public Object getNext(TableImpl.WriteRowState writeRowState) {
2739       throw new UnsupportedOperationException();
2740     }
2741 
2742     @Override
2743     public Object handleInsert(TableImpl.WriteRowState writeRowState,
2744                                Object inRowValue) {
2745       throw new UnsupportedOperationException();
2746     }
2747 
2748     @Override
2749     public void restoreLast(Object last) {
2750       throw new UnsupportedOperationException();
2751     }
2752 
2753     @Override
2754     public DataType getType() {
2755       return _genType;
2756     }
2757   }
2758 
2759 
2760   /**
2761    * Information about the sort order (collation) for a textual column.
2762    * <p>
2763    * The three parts together are the ms access sort id, a 4 byte value built
2764    * as {@code (version &lt;&lt; 24) | (variant &lt;&lt; 16) | value}: the LCID
2765    * in the low half, the variant within a collation in byte 2, and which of
2766    * the two weight tables the collation uses in byte 3.  A locale
2767    * with more than one collation separates them with the variant, so German
2768    * 1031 is plain German at variant 0 and German Phone Book at variant 1,
2769    * which order differently.
2770    * @usage _intermediate_class_
2771    */
2772   public static final class SortOrder
2773   {
2774     private final short _value;
2775     private final short _variant;
2776     private final short _version;
2777 
2778     public SortOrder(short value, short version) {
2779       this(value, (short)0, version);
2780     }
2781 
2782     public SortOrder(short value, short variant, short version) {
2783       _value = value;
2784       _variant = variant;
2785       _version = version;
2786     }
2787 
2788     public short getValue() {
2789       return _value;
2790     }
2791 
2792     /**
2793      * @return which collation of the locale this is, 0 for the plain one
2794      */
2795     public short getVariant() {
2796       return _variant;
2797     }
2798 
2799     public short getVersion() {
2800       return _version;
2801     }
2802 
2803     /**
2804      * @return the ms access sort id, which names the collation on its own.
2805      *         A jet 3 sort order has no version byte and reports -1, so it has
2806      *         no sort id
2807      */
2808     public int getSortId() {
2809       if(_version < 0) {
2810         throw new IllegalStateException(
2811             "sort order " + this + " has no sort id");
2812       }
2813       return ((_version << 24) | ((_variant & 0xFF) << 16) |
2814               (_value & 0xFFFF));
2815     }
2816 
2817     @Override
2818     public int hashCode() {
2819       return _value;
2820     }
2821 
2822     @Override
2823     public boolean equals(Object o) {
2824       return ((this == o) ||
2825               ((o != null) && (getClass() == o.getClass()) &&
2826                (_value == ((SortOrder)o)._value) &&
2827                (_variant == ((SortOrder)o)._variant) &&
2828                (_version == ((SortOrder)o)._version)));
2829     }
2830 
2831     @Override
2832     public String toString() {
2833       LocaleUtil.LcidInfo info = LocaleUtil.getInfo(_value);
2834       String valueStr = ((_version >= 0) ?
2835                          String.format("0x%08X", getSortId()) :
2836                          (_value + "(" + _variant + "," + _version + ")"));
2837       return ToStringBuilder.valueBuilder(this)
2838         .append(null, (info != null) ? (valueStr + ", " + info) : valueStr)
2839         .toString();
2840     }
2841   }
2842 
2843   /**
2844    * Utility struct for passing params through ColumnImpl constructors.
2845    */
2846   static final class InitArgs
2847   {
2848     public final TableImpl table;
2849     public final ByteBuffer buffer;
2850     public final int offset;
2851     public final String name;
2852     public final int displayIndex;
2853     public final byte colType;
2854     public final byte flags;
2855     public final byte extFlags;
2856     public DataType type;
2857 
2858     InitArgs(TableImpl newTable, ByteBuffer newBuffer, int newOffset,
2859              String newName, int newDisplayIndex) {
2860       this.table = newTable;
2861       this.buffer = newBuffer;
2862       this.offset = newOffset;
2863       this.name = newName;
2864       this.displayIndex = newDisplayIndex;
2865 
2866       this.colType = buffer.get(offset + table.getFormat().OFFSET_COLUMN_TYPE);
2867       this.flags = buffer.get(offset + table.getFormat().OFFSET_COLUMN_FLAGS);
2868       this.extFlags = readExtraFlags(buffer, offset, table.getFormat());
2869     }
2870   }
2871 
2872   /**
2873    * "Internal" column validator for columns with the "required" property
2874    * enabled.
2875    */
2876   private static final class RequiredColValidator extends InternalColumnValidator
2877   {
2878     private RequiredColValidator(ColumnValidator delegate) {
2879       super(delegate);
2880     }
2881 
2882     @Override
2883     protected Object internalValidate(Column col, Object val)
2884       throws IOException
2885     {
2886       if(val == null) {
2887         throw new InvalidValueException(
2888             ((ColumnImpl)col).withErrorContext(
2889                 "Missing value for required column"));
2890       }
2891       return val;
2892     }
2893 
2894     @Override
2895     protected void appendToString(StringBuilder sb) {
2896       sb.append("required=true");
2897     }
2898   }
2899 
2900   /**
2901    * "Internal" column validator for text columns with the "allow zero len"
2902    * property disabled.
2903    */
2904   private static final class NoZeroLenColValidator extends InternalColumnValidator
2905   {
2906     private NoZeroLenColValidator(ColumnValidator delegate) {
2907       super(delegate);
2908     }
2909 
2910     @Override
2911     protected Object internalValidate(Column col, Object val)
2912       throws IOException
2913     {
2914       CharSequence valStr = toCharSequence(val);
2915       // oddly enough null is allowed for non-zero len strings
2916       if((valStr != null) && valStr.length() == 0) {
2917         throw new InvalidValueException(
2918             ((ColumnImpl)col).withErrorContext(
2919                 "Zero length string is not allowed"));
2920       }
2921       return valStr;
2922     }
2923 
2924     @Override
2925     protected void appendToString(StringBuilder sb) {
2926       sb.append("allowZeroLength=false");
2927     }
2928   }
2929 
2930   /**
2931    * Factory which handles date/time values appropriately for a DateTimeType.
2932    */
2933   protected static abstract class DateTimeFactory
2934   {
2935     public abstract DateTimeType getType();
2936 
2937     public abstract Object fromDateBits(ColumnImpl col, long dateBits);
2938 
2939     public abstract double toDateDouble(Object value, DateTimeContext dtc);
2940 
2941     public abstract Object toInternalValue(DatabaseImpl db, Object value);
2942   }
2943 
2944   /**
2945    * Factory impl for legacy Date handling.
2946    */
2947   private static final class DefaultDateTimeFactory extends DateTimeFactory
2948   {
2949     @Override
2950     public DateTimeType getType() {
2951       return DateTimeType.DATE;
2952     }
2953 
2954     @Override
2955     public Object fromDateBits(ColumnImpl col, long dateBits) {
2956       long time = col.fromDateDouble(
2957           Double.longBitsToDouble(dateBits));
2958       return new DateExt(time, dateBits);
2959     }
2960 
2961     @Override
2962     public double toDateDouble(Object value, DateTimeContext dtc) {
2963       // ZoneId and TimeZone have different rules for older timezones, so we
2964       // need to consistently use one or the other depending on the date/time
2965       // type
2966       long time = 0L;
2967       if(value instanceof TemporalAccessor) {
2968         time = toInstant((TemporalAccessor)value, dtc).toEpochMilli();
2969       } else {
2970         time = toDateLong(value);
2971       }
2972       // seems access stores dates in the local timezone.  guess you just
2973       // hope you read it in the same timezone in which it was written!
2974       time += getToLocalTimeZoneOffset(time, dtc.getTimeZone());
2975       return toLocalDateDouble(time);
2976     }
2977 
2978     @Override
2979     public Object toInternalValue(DatabaseImpl db, Object value) {
2980       return ((value instanceof Date) ? value :
2981               new Date(toDateLong(value)));
2982     }
2983   }
2984 
2985   /**
2986    * Factory impl for LocalDateTime handling.
2987    */
2988   private static final class LDTDateTimeFactory extends DateTimeFactory
2989   {
2990     @Override
2991     public DateTimeType getType() {
2992       return DateTimeType.LOCAL_DATE_TIME;
2993     }
2994 
2995     @Override
2996     public Object fromDateBits(ColumnImpl col, long dateBits) {
2997       return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits));
2998     }
2999 
3000     @Override
3001     public double toDateDouble(Object value, DateTimeContext dtc) {
3002       // ZoneId and TimeZone have different rules for older timezones, so we
3003       // need to consistently use one or the other depending on the date/time
3004       // type
3005       if(!(value instanceof TemporalAccessor)) {
3006         value = Instant.ofEpochMilli(toDateLong(value));
3007       }
3008       return ColumnImpl.toDateDouble(
3009           temporalToLocalDateTime((TemporalAccessor)value, dtc));
3010     }
3011 
3012     @Override
3013     public Object toInternalValue(DatabaseImpl db, Object value) {
3014       return toLocalDateTime(value, db);
3015     }
3016   }
3017 
3018   /** internal interface for types which hold bytes in memory */
3019   static interface InMemoryBlob {
3020     public byte[] getBytes() throws IOException;
3021   }
3022 }