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