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