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