View Javadoc
1   /*
2   Copyright (c) 2014 James Ahlborn
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.IOException;
20  import java.math.BigDecimal;
21  import java.nio.ByteBuffer;
22  import java.nio.ByteOrder;
23  
24  
25  /**
26   * Utility code for dealing with calculated columns.
27   * <p/>
28   * These are the currently possible calculated types: FLOAT, DOUBLE, INT,
29   * LONG, GUID, SHORT_DATE_TIME, MONEY, BOOLEAN, NUMERIC, TEXT, MEMO.
30   *
31   * @author James Ahlborn
32   */
33  class CalculatedColumnUtil 
34  {
35    // offset to the int which specifies the length of the actual data
36    private static final int CALC_DATA_LEN_OFFSET = 16;
37    // offset to the actual data
38    private static final int CALC_DATA_OFFSET = CALC_DATA_LEN_OFFSET + 4;
39    // total amount of extra bytes added for calculated values
40    static final int CALC_EXTRA_DATA_LEN = 23;
41    // ms access seems to define all fixed-len calc fields as this length
42    static final short CALC_FIXED_FIELD_LEN = 39;
43  
44    // fully encode calculated BOOLEAN "true" value
45    private static final byte[] CALC_BOOL_TRUE = wrapCalculatedValue(
46        new byte[]{(byte)0xFF});
47    // fully encode calculated BOOLEAN "false" value
48    private static final byte[] CALC_BOOL_FALSE = wrapCalculatedValue(
49        new byte[]{0});
50  
51    /**
52     * Creates the appropriate ColumnImpl class for a calculated column and
53     * reads a column definition in from a buffer
54     * 
55     * @param args column construction info
56     * @usage _advanced_method_
57     */
58    static ColumnImpl create(ColumnImpl.InitArgs args) throws IOException
59    {    
60      switch(args.type) {
61      case BOOLEAN:
62        return new CalcBooleanColImpl(args);
63      case TEXT:
64        return new CalcTextColImpl(args);
65      case MEMO:
66        return new CalcMemoColImpl(args);
67      default:
68        // fall through
69      }
70  
71      if(args.type.getHasScalePrecision()) {
72        return new CalcNumericColImpl(args);
73      }
74      
75      return new CalcColImpl(args);
76    }
77  
78    /**
79     * Grabs the real data bytes from a calculated value.
80     */
81    private static byte[] unwrapCalculatedValue(byte[] data) {
82      if(data.length < CALC_DATA_OFFSET) {
83        return data;
84      }
85      
86      ByteBuffer buffer = PageChannel.wrap(data);
87      buffer.position(CALC_DATA_LEN_OFFSET);
88      int dataLen = buffer.getInt();
89      byte[] newData = new byte[Math.min(buffer.remaining(), dataLen)];
90      buffer.get(newData);
91      return newData;
92    }
93  
94    /**
95     * Wraps the given data bytes with the extra calculated value data and
96     * returns a new ByteBuffer containing the final data.
97     */
98    private static ByteBuffer wrapCalculatedValue(ByteBuffer buffer) {
99      ByteBuffer newBuf = prepareWrappedCalcValue(
100         buffer.remaining(), buffer.order());
101     newBuf.put(buffer);
102     newBuf.rewind();
103     return newBuf;
104   }
105 
106   /**
107    * Wraps the given data bytes with the extra calculated value data and
108    * returns a new byte[] containing the final data.
109    */
110   private static byte[] wrapCalculatedValue(byte[] data) {
111     int dataLen = data.length;
112     data = ByteUtil.copyOf(data, 0, dataLen + CALC_EXTRA_DATA_LEN, 
113                            CALC_DATA_OFFSET);
114     PageChannel.wrap(data).putInt(CALC_DATA_LEN_OFFSET, dataLen);
115     return data;
116   }
117 
118   /**
119    * Prepares a calculated value buffer for data of the given length.
120    */
121   private static ByteBuffer prepareWrappedCalcValue(int dataLen, ByteOrder order)
122   {
123     ByteBuffer buffer = ByteBuffer.allocate(
124         dataLen + CALC_EXTRA_DATA_LEN).order(order);
125     buffer.putInt(CALC_DATA_LEN_OFFSET, dataLen);
126     buffer.position(CALC_DATA_OFFSET);
127     return buffer;
128   }
129   
130 
131   /**
132    * General calculated column implementation.
133    */
134   private static class CalcColImpl extends ColumnImpl
135   {
136     CalcColImpl(InitArgs args) throws IOException {
137       super(args);
138     }
139 
140     @Override
141     public Object read(byte[] data, ByteOrder order) throws IOException {
142       data = unwrapCalculatedValue(data);
143       if((data.length == 0) && !getType().isVariableLength()) {
144         // apparently "null" values can be written as actual data
145         return null;
146       }
147       return super.read(data, order);
148     }
149 
150     @Override
151     protected ByteBuffer writeRealData(Object obj, int remainingRowLength, 
152                                        ByteOrder order)
153       throws IOException
154     {
155       // we should only be working with fixed length types
156       ByteBuffer buffer = writeFixedLengthField(
157           obj, prepareWrappedCalcValue(getType().getFixedSize(), order));
158       buffer.rewind();
159       return buffer;
160     }
161   }
162 
163   /**
164    * Calculated BOOLEAN column implementation.
165    */
166   private static class CalcBooleanColImpl extends ColumnImpl
167   {
168     CalcBooleanColImpl(InitArgs args) throws IOException {
169       super(args);
170     }
171 
172     @Override
173     public boolean storeInNullMask() {
174       // calculated booleans are _not_ stored in null mask
175       return false;
176     }
177 
178     @Override
179     public Object read(byte[] data, ByteOrder order) throws IOException {
180       data = unwrapCalculatedValue(data);
181       if(data.length == 0) {
182         return Boolean.FALSE;
183       }
184       return ((data[0] != 0) ? Boolean.TRUE : Boolean.FALSE);
185     }
186 
187     @Override
188     protected ByteBuffer writeRealData(Object obj, int remainingRowLength, 
189                                        ByteOrder order)
190       throws IOException
191     {
192       return ByteBuffer.wrap(
193           toBooleanValue(obj) ? CALC_BOOL_TRUE : CALC_BOOL_FALSE).order(order);
194     }
195   }
196 
197   /**
198    * Calculated TEXT column implementation.
199    */
200   private static class CalcTextColImpl extends TextColumnImpl
201   {
202     CalcTextColImpl(InitArgs args) throws IOException {
203       super(args);
204     }
205 
206     @Override
207     public short getLengthInUnits() {
208       // the byte "length" includes the calculated field overhead.  remove
209       // that to get the _actual_ data length (in units)
210       return (short)getType().toUnitSize(getLength() - CALC_EXTRA_DATA_LEN);
211     }
212 
213     @Override
214     public Object read(byte[] data, ByteOrder order) throws IOException {
215       return decodeTextValue(unwrapCalculatedValue(data));
216     }
217 
218     @Override
219     protected ByteBuffer writeRealData(Object obj, int remainingRowLength, 
220                                        ByteOrder order)
221       throws IOException
222     {
223       return wrapCalculatedValue(super.writeRealData(
224                                      obj, remainingRowLength, order));
225     }
226   }
227 
228   /**
229    * Calculated MEMO column implementation.
230    */
231   private static class CalcMemoColImpl extends MemoColumnImpl
232   {
233     CalcMemoColImpl(InitArgs args) throws IOException {
234       super(args);
235     }
236 
237     @Override
238     protected int getMaxLengthInUnits() {
239       // the byte "length" includes the calculated field overhead.  remove
240       // that to get the _actual_ data length (in units)
241       return getType().toUnitSize(getType().getMaxSize() - CALC_EXTRA_DATA_LEN);
242     }
243 
244     @Override
245     protected byte[] readLongValue(byte[] lvalDefinition)
246       throws IOException
247     {
248       return unwrapCalculatedValue(super.readLongValue(lvalDefinition));
249     }
250 
251     @Override
252     protected ByteBuffer writeLongValue(byte[] value, int remainingRowLength) 
253       throws IOException
254     {
255       return super.writeLongValue(
256           wrapCalculatedValue(value), remainingRowLength);
257     }    
258   }
259 
260   /**
261    * Calculated NUMERIC column implementation.
262    */
263   private static class CalcNumericColImpl extends NumericColumnImpl
264   {
265     CalcNumericColImpl(InitArgs args) throws IOException {
266       super(args);
267     }
268 
269     @Override
270     public byte getPrecision() {
271       return (byte)getType().getMaxPrecision();
272     }
273 
274     @Override
275     public Object read(byte[] data, ByteOrder order) throws IOException {
276       data = unwrapCalculatedValue(data);
277       if(data.length == 0) {
278         // apparently "null" values can be written as actual data
279         return null;
280       }
281       return readCalcNumericValue(ByteBuffer.wrap(data).order(order));
282     }
283 
284     @Override
285     protected ByteBuffer writeRealData(Object obj, int remainingRowLength, 
286                                        ByteOrder order)
287       throws IOException
288     {
289       int totalDataLen = Math.min(CALC_EXTRA_DATA_LEN + 16 + 4, getLength());
290       // data length must be multiple of 4
291       int dataLen = toMul4(totalDataLen - CALC_EXTRA_DATA_LEN);
292       ByteBuffer buffer = prepareWrappedCalcValue(dataLen, order);
293 
294       writeCalcNumericValue(buffer, obj, dataLen);
295 
296       buffer.rewind();
297       return buffer;
298     }
299 
300     private static BigDecimal readCalcNumericValue(ByteBuffer buffer)
301     {
302       short totalLen = buffer.getShort();
303       // numeric bytes need to be a multiple of 4 and we currently handle at
304       // most 16 bytes
305       int numByteLen = ((totalLen > 0) ? totalLen : buffer.remaining()) - 2;
306       numByteLen = Math.min(toMul4(numByteLen), 16);
307 
308       byte scale = buffer.get();
309       boolean negate = (buffer.get() != 0);
310       byte[] tmpArr = ByteUtil.getBytes(buffer, numByteLen);
311 
312       if(buffer.order() != ByteOrder.BIG_ENDIAN) {
313         fixNumericByteOrder(tmpArr);
314       }
315 
316       return toBigDecimal(tmpArr, negate, scale);
317     }
318 
319     private void writeCalcNumericValue(ByteBuffer buffer, Object value,
320                                        int dataLen)
321       throws IOException
322     {
323       Object inValue = value;
324       try {
325         BigDecimal decVal = toBigDecimal(value);
326         inValue = decVal;
327 
328         int signum = decVal.signum();
329         if(signum < 0) {
330           decVal = decVal.negate();
331         }
332 
333         int maxScale = getType().getMaxScale();
334         if(decVal.scale() > maxScale) {
335           // adjust scale according to max (will cause the an
336           // ArithmeticException if number has too many decimal places)
337           decVal = decVal.setScale(maxScale);
338         }
339         int scale = decVal.scale();
340         
341         // check precision
342         if(decVal.precision() > getType().getMaxPrecision()) {
343           throw new IOException(withErrorContext(
344               "Numeric value is too big for specified precision "
345               + getType().getMaxPrecision() + ": " + decVal));
346         }
347     
348         // convert to unscaled BigInteger, big-endian bytes
349         byte[] intValBytes = toUnscaledByteArray(decVal, dataLen - 4);
350 
351         if(buffer.order() != ByteOrder.BIG_ENDIAN) {
352           fixNumericByteOrder(intValBytes);
353         }
354 
355         buffer.putShort((short)(dataLen - 2));
356         buffer.put((byte)scale);
357         // write sign byte
358         buffer.put((signum < 0) ? NUMERIC_NEGATIVE_BYTE : 0);
359         buffer.put(intValBytes);
360 
361       } catch(ArithmeticException e) {
362         throw (IOException)
363           new IOException(withErrorContext(
364               "Numeric value '" + inValue + "' out of range"))
365           .initCause(e);
366       }
367     }
368 
369     private static void fixNumericByteOrder(byte[] bytes) {
370 
371       // this is a little weird.  it looks like they decided to truncate
372       // leading 0 bytes and _then_ swap endian, which ends up kind of odd.
373       int pos = 0;
374       if((bytes.length % 8) != 0) {
375         // leading 4 bytes are swapped
376         ByteUtil.swap4Bytes(bytes, 0);
377         pos += 4;
378       }
379 
380       // then fix endianness of each 8 byte segment
381       for(; pos < bytes.length; pos+=8) {
382         ByteUtil.swap8Bytes(bytes, pos);
383       }
384     }
385 
386     private static int toMul4(int val) {
387       return ((val / 4) * 4);
388     }
389   }
390 
391 }