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