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