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.nio.ByteBuffer;
21  import java.nio.ByteOrder;
22  import java.util.Collection;
23  
24  import com.healthmarketscience.jackcess.InvalidValueException;
25  
26  /**
27   * ColumnImpl subclass which is used for long value data types.
28   *
29   * @author James Ahlborn
30   * @usage _advanced_class_
31   */
32  class LongValueColumnImpl extends ColumnImpl
33  {
34    /**
35     * Long value (LVAL) type that indicates that the value is stored on the
36     * same page
37     */
38    private static final byte LONG_VALUE_TYPE_THIS_PAGE = (byte) 0x80;
39    /**
40     * Long value (LVAL) type that indicates that the value is stored on another
41     * page
42     */
43    private static final byte LONG_VALUE_TYPE_OTHER_PAGE = (byte) 0x40;
44    /**
45     * Long value (LVAL) type that indicates that the value is stored on
46     * multiple other pages
47     */
48    private static final byte LONG_VALUE_TYPE_OTHER_PAGES = (byte) 0x00;
49    /**
50     * Mask to apply the long length in order to get the flag bits (only the
51     * first 2 bits are type flags).
52     */
53    private static final int LONG_VALUE_TYPE_MASK = 0xC0000000;
54  
55  
56    /** Holds additional info for writing long values */
57    private LongValueBufferHolder _lvalBufferH;
58    private int _maxLenInUnits = INVALID_LENGTH;
59  
60    LongValueColumnImpl(InitArgs args) throws IOException
61    {
62      super(args);
63    }
64  
65    @Override
66    public int getOwnedPageCount() {
67      return ((_lvalBufferH == null) ? 0 : _lvalBufferH.getOwnedPageCount());
68    }
69  
70    @Override
71    void setUsageMaps(UsageMap./../../com/healthmarketscience/jackcess/impl/UsageMap.html#UsageMap">UsageMap ownedPages, UsageMap freeSpacePages) {
72      _lvalBufferH = new UmapLongValueBufferHolder(ownedPages, freeSpacePages);
73    }
74  
75    @Override
76    void collectUsageMapPages(Collection<Integer> pages) {
77      _lvalBufferH.collectUsageMapPages(pages);
78    }
79  
80    @Override
81    void postTableLoadInit() throws IOException {
82      if(_lvalBufferH == null) {
83        _lvalBufferH = new LegacyLongValueBufferHolder();
84      }
85      super.postTableLoadInit();
86    }
87  
88    protected final int getMaxLengthInUnits() {
89      if(_maxLenInUnits == INVALID_LENGTH) {
90        _maxLenInUnits = calcMaxLengthInUnits();
91      }
92      return _maxLenInUnits;
93    }
94  
95    protected int calcMaxLengthInUnits() {
96      return getType().toUnitSize(getType().getMaxSize(), getFormat());
97    }
98  
99    @Override
100   public Object read(byte[] data, ByteOrder order) throws IOException {
101     switch(getType()) {
102     case OLE:
103       if (data.length > 0) {
104         return readLongValue(data);
105       }
106       return null;
107     case MEMO:
108       if (data.length > 0) {
109         return readLongStringValue(data);
110       }
111       return null;
112     default:
113       throw new RuntimeException(withErrorContext(
114               "unexpected var length, long value type: " + getType()));
115     }
116   }
117 
118   @Override
119   protected ByteBuffer writeRealData(Object obj, int remainingRowLength,
120                                      ByteOrder order)
121     throws IOException
122   {
123     switch(getType()) {
124     case OLE:
125       // should already be "encoded"
126       break;
127     case MEMO:
128       obj = encodeTextValue(obj, 0, getMaxLengthInUnits(), false).array();
129       break;
130     default:
131       throw new RuntimeException(withErrorContext(
132               "unexpected var length, long value type: " + getType()));
133     }
134 
135     // create long value buffer
136     return writeLongValue(toByteArray(obj), remainingRowLength);
137   }
138 
139   /**
140    * @param lvalDefinition Column value that points to an LVAL record
141    * @return The LVAL data
142    */
143   protected byte[] readLongValue(byte[] lvalDefinition)
144     throws IOException
145   {
146     ByteBuffer def = PageChannel.wrap(lvalDefinition);
147     int lengthWithFlags = def.getInt();
148     int length = lengthWithFlags & (~LONG_VALUE_TYPE_MASK);
149 
150     byte[] rtn = new byte[length];
151     byte type = (byte)((lengthWithFlags & LONG_VALUE_TYPE_MASK) >>> 24);
152 
153     if(type == LONG_VALUE_TYPE_THIS_PAGE) {
154 
155       // inline long value
156       def.getInt();  //Skip over lval_dp
157       def.getInt();  //Skip over unknown
158 
159       int rowLen = def.remaining();
160       if(rowLen < length) {
161         // warn the caller, but return whatever we can
162         LOG.warn(withErrorContext(
163                 "Value may be truncated: expected length " +
164                 length + " found " + rowLen));
165         rtn = new byte[rowLen];
166       }
167 
168       def.get(rtn);
169 
170     } else {
171 
172       // long value on other page(s)
173       if (lvalDefinition.length != getFormat().SIZE_LONG_VALUE_DEF) {
174         throw new IOException(withErrorContext(
175                 "Expected " + getFormat().SIZE_LONG_VALUE_DEF +
176                 " bytes in long value definition, but found " +
177                 lvalDefinition.length));
178       }
179 
180       int rowNum = ByteUtil.getUnsignedByte(def);
181       int pageNum = ByteUtil.get3ByteInt(def, def.position());
182       ByteBuffer lvalPage = getPageChannel().createPageBuffer();
183 
184       switch (type) {
185       case LONG_VALUE_TYPE_OTHER_PAGE:
186         {
187           getPageChannel().readPage(lvalPage, pageNum);
188 
189           short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat());
190           short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat());
191 
192           int rowLen = rowEnd - rowStart;
193           if(rowLen < length) {
194             // warn the caller, but return whatever we can
195             LOG.warn(withErrorContext(
196                     "Value may be truncated: expected length " +
197                     length + " found " + rowLen));
198             rtn = new byte[rowLen];
199           }
200 
201           lvalPage.position(rowStart);
202           lvalPage.get(rtn);
203         }
204         break;
205 
206       case LONG_VALUE_TYPE_OTHER_PAGES:
207 
208         ByteBuffer rtnBuf = ByteBuffer.wrap(rtn);
209         int remainingLen = length;
210         while(remainingLen > 0) {
211           lvalPage.clear();
212           getPageChannel().readPage(lvalPage, pageNum);
213 
214           short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat());
215           short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat());
216 
217           // read next page information
218           lvalPage.position(rowStart);
219           rowNum = ByteUtil.getUnsignedByte(lvalPage);
220           pageNum = ByteUtil.get3ByteInt(lvalPage);
221 
222           // update rowEnd and remainingLen based on chunkLength
223           int chunkLength = (rowEnd - rowStart) - 4;
224           if(chunkLength > remainingLen) {
225             rowEnd = (short)(rowEnd - (chunkLength - remainingLen));
226             chunkLength = remainingLen;
227           }
228           remainingLen -= chunkLength;
229 
230           lvalPage.limit(rowEnd);
231           rtnBuf.put(lvalPage);
232         }
233 
234         break;
235 
236       default:
237         throw new IOException(withErrorContext(
238                 "Unrecognized long value type: " + type));
239       }
240     }
241 
242     return rtn;
243   }
244 
245   /**
246    * @param lvalDefinition Column value that points to an LVAL record
247    * @return The LVAL data
248    */
249   private String readLongStringValue(byte[] lvalDefinition)
250     throws IOException
251   {
252     byte[] binData = readLongValue(lvalDefinition);
253     if(binData == null) {
254       return null;
255     }
256     if(binData.length == 0) {
257       return "";
258     }
259     return decodeTextValue(binData);
260   }
261 
262   /**
263    * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in
264    * other data page(s).
265    * @param value Value of the LVAL column
266    * @return A buffer containing the LVAL definition and (possibly) the column
267    *         value (unless written to other pages)
268    * @usage _advanced_method_
269    */
270   protected ByteBuffer writeLongValue(byte[] value, int remainingRowLength)
271     throws IOException
272   {
273     if(value.length > getType().getMaxSize()) {
274       throw new InvalidValueException(withErrorContext(
275               "value too big for column, max " +
276               getType().getMaxSize() + ", got " + value.length));
277     }
278 
279     // determine which type to write
280     byte type = 0;
281     int lvalDefLen = getFormat().SIZE_LONG_VALUE_DEF;
282     if(((getFormat().SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength)
283        && (value.length <= getFormat().MAX_INLINE_LONG_VALUE_SIZE)) {
284       type = LONG_VALUE_TYPE_THIS_PAGE;
285       lvalDefLen += value.length;
286     } else if(value.length <= getFormat().MAX_LONG_VALUE_ROW_SIZE) {
287       type = LONG_VALUE_TYPE_OTHER_PAGE;
288     } else {
289       type = LONG_VALUE_TYPE_OTHER_PAGES;
290     }
291 
292     ByteBuffer def = PageChannel.createBuffer(lvalDefLen);
293     // take length and apply type to first byte
294     int lengthWithFlags = value.length | (type << 24);
295     def.putInt(lengthWithFlags);
296 
297     if(type == LONG_VALUE_TYPE_THIS_PAGE) {
298       // write long value inline
299       def.putInt(0);
300       def.putInt(0);  //Unknown
301       def.put(value);
302     } else {
303 
304       ByteBuffer lvalPage = null;
305       int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER;
306       byte firstLvalRow = 0;
307 
308       // write other page(s)
309       switch(type) {
310       case LONG_VALUE_TYPE_OTHER_PAGE:
311         lvalPage = _lvalBufferH.getLongValuePage(value.length);
312         firstLvalPageNum = _lvalBufferH.getPageNumber();
313         firstLvalRow = (byte)TableImpl.addDataPageRow(lvalPage, value.length,
314                                                   getFormat(), 0);
315         lvalPage.put(value);
316         getPageChannel().writePage(lvalPage, firstLvalPageNum);
317         break;
318 
319       case LONG_VALUE_TYPE_OTHER_PAGES:
320 
321         ByteBuffer buffer = ByteBuffer.wrap(value);
322         int remainingLen = buffer.remaining();
323         buffer.limit(0);
324         lvalPage = _lvalBufferH.getLongValuePage(remainingLen);
325         firstLvalPageNum = _lvalBufferH.getPageNumber();
326         firstLvalRow = (byte)TableImpl.getRowsOnDataPage(lvalPage, getFormat());
327         int lvalPageNum = firstLvalPageNum;
328         ByteBuffer nextLvalPage = null;
329         int nextLvalPageNum = 0;
330         int nextLvalRowNum = 0;
331         while(remainingLen > 0) {
332           lvalPage.clear();
333 
334           // figure out how much we will put in this page (we need 4 bytes for
335           // the next page pointer)
336           int chunkLength = Math.min(getFormat().MAX_LONG_VALUE_ROW_SIZE - 4,
337                                      remainingLen);
338 
339           // figure out if we will need another page, and if so, allocate it
340           if(chunkLength < remainingLen) {
341             // force a new page to be allocated for the chunk after this
342             _lvalBufferH.clear();
343             nextLvalPage = _lvalBufferH.getLongValuePage(
344                 (remainingLen - chunkLength) + 4);
345             nextLvalPageNum = _lvalBufferH.getPageNumber();
346             nextLvalRowNum = TableImpl.getRowsOnDataPage(nextLvalPage,
347                                                          getFormat());
348           } else {
349             nextLvalPage = null;
350             nextLvalPageNum = 0;
351             nextLvalRowNum = 0;
352           }
353 
354           // add row to this page
355           TableImpl.addDataPageRow(lvalPage, chunkLength + 4, getFormat(), 0);
356 
357           // write next page info
358           lvalPage.put((byte)nextLvalRowNum); // row number
359           ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number
360 
361           // write this page's chunk of data
362           buffer.limit(buffer.limit() + chunkLength);
363           lvalPage.put(buffer);
364           remainingLen -= chunkLength;
365 
366           // write new page to database
367           getPageChannel().writePage(lvalPage, lvalPageNum);
368 
369           // move to next page
370           lvalPage = nextLvalPage;
371           lvalPageNum = nextLvalPageNum;
372         }
373         break;
374 
375       default:
376         throw new IOException(withErrorContext(
377                 "Unrecognized long value type: " + type));
378       }
379 
380       // update def
381       def.put(firstLvalRow);
382       ByteUtil.put3ByteInt(def, firstLvalPageNum);
383       def.putInt(0);  //Unknown
384 
385     }
386 
387     def.flip();
388     return def;
389   }
390 
391   /**
392    * Writes the header info for a long value page.
393    */
394   private void writeLongValueHeader(ByteBuffer lvalPage)
395   {
396     lvalPage.put(PageTypes.DATA); //Page type
397     lvalPage.put((byte) 1); //Unknown
398     lvalPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space
399     lvalPage.put((byte) 'L');
400     lvalPage.put((byte) 'V');
401     lvalPage.put((byte) 'A');
402     lvalPage.put((byte) 'L');
403     lvalPage.putInt(0); //unknown
404     lvalPage.putShort((short)0); // num rows in page
405   }
406 
407 
408   /**
409    * Manages secondary page buffers for long value writing.
410    */
411   private abstract class LongValueBufferHolder
412   {
413     /**
414      * Returns a long value data page with space for data of the given length.
415      */
416     public ByteBuffer getLongValuePage(int dataLength) throws IOException {
417 
418       TempPageHolder lvalBufferH = getBufferHolder();
419       dataLength = Math.min(dataLength, getFormat().MAX_LONG_VALUE_ROW_SIZE);
420 
421       ByteBuffer lvalPage = null;
422       if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) {
423         lvalPage = lvalBufferH.getPage(getPageChannel());
424         if(TableImpl.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) {
425           // the current page has space
426           return lvalPage;
427         }
428       }
429 
430       // need new page
431       return findNewPage(dataLength);
432     }
433 
434     protected ByteBuffer findNewPage(int dataLength) throws IOException {
435       ByteBuffer lvalPage = getBufferHolder().setNewPage(getPageChannel());
436       writeLongValueHeader(lvalPage);
437       return lvalPage;
438     }
439 
440     public int getOwnedPageCount() {
441       return 0;
442     }
443 
444     /**
445      * Returns the page number of the current long value data page.
446      */
447     public int getPageNumber() {
448       return getBufferHolder().getPageNumber();
449     }
450 
451     /**
452      * Discards the current the current long value data page.
453      */
454     public void clear() throws IOException {
455       getBufferHolder().clear();
456     }
457 
458     public void collectUsageMapPages(Collection<Integer> pages) {
459       // base does nothing
460     }
461 
462     protected abstract TempPageHolder getBufferHolder();
463   }
464 
465   /**
466    * Manages a common, shared extra page for long values.  This is legacy
467    * behavior from before it was understood that there were additional usage
468    * maps for each columns.
469    */
470   private final class LegacyLongValueBufferHolder extends LongValueBufferHolder
471   {
472     @Override
473     protected TempPageHolder getBufferHolder() {
474       return getTable().getLongValueBuffer();
475     }
476   }
477 
478   /**
479    * Manages the column usage maps for long values.
480    */
481   private final class UmapLongValueBufferHolder extends LongValueBufferHolder
482   {
483     /** Usage map of pages that this column owns */
484     private final UsageMap _ownedPages;
485     /** Usage map of pages that this column owns with free space on them */
486     private final UsageMap _freeSpacePages;
487     /** page buffer used to write "long value" data */
488     private final TempPageHolder _longValueBufferH =
489       TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
490 
491     private UmapLongValueBufferHolder(UsageMap ownedPages,
492                                       UsageMap freeSpacePages) {
493       _ownedPages = ownedPages;
494       _freeSpacePages = freeSpacePages;
495     }
496 
497     @Override
498     protected TempPageHolder getBufferHolder() {
499       return _longValueBufferH;
500     }
501 
502     @Override
503     public int getOwnedPageCount() {
504       return _ownedPages.getPageCount();
505     }
506 
507     @Override
508     protected ByteBuffer findNewPage(int dataLength) throws IOException {
509 
510       // grab last owned page and check for free space.
511       ByteBuffer newPage = TableImpl.findFreeRowSpace(
512           _ownedPages, _freeSpacePages, _longValueBufferH);
513 
514       if(newPage != null) {
515         if(TableImpl.rowFitsOnDataPage(dataLength, newPage, getFormat())) {
516           return newPage;
517         }
518         // discard this page and allocate a new one
519         clear();
520       }
521 
522       // nothing found on current pages, need new page
523       newPage = super.findNewPage(dataLength);
524       int pageNumber = getPageNumber();
525       _ownedPages.addPageNumber(pageNumber);
526       _freeSpacePages.addPageNumber(pageNumber);
527       return newPage;
528     }
529 
530     @Override
531     public void clear() throws IOException {
532       int pageNumber = getPageNumber();
533       if(pageNumber != PageChannel.INVALID_PAGE_NUMBER) {
534         _freeSpacePages.removePageNumber(pageNumber);
535       }
536       super.clear();
537     }
538 
539     @Override
540     public void collectUsageMapPages(Collection<Integer> pages) {
541       pages.add(_ownedPages.getTablePageNumber());
542       pages.add(_freeSpacePages.getTablePageNumber());
543     }
544   }
545 }