View Javadoc
1   /*
2   Copyright (c) 2011 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.ArrayList;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.LinkedHashMap;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import com.healthmarketscience.jackcess.DataType;
32  import com.healthmarketscience.jackcess.InvalidValueException;
33  import com.healthmarketscience.jackcess.PropertyMap;
34  
35  /**
36   * Collection of PropertyMap instances read from a single property data block.
37   *
38   * @author James Ahlborn
39   */
40  public class PropertyMaps implements Iterable<PropertyMapImpl>
41  {
42    /** the name of the "default" properties for a PropertyMaps instance */
43    public static final String DEFAULT_NAME = "";
44  
45    private static final short PROPERTY_NAME_LIST = 0x80;
46    private static final short DEFAULT_PROPERTY_VALUE_LIST = 0x00;
47    private static final short COLUMN_PROPERTY_VALUE_LIST = 0x01;
48    private static final short INDEX_PROPERTY_VALUE_LIST = 0x02;
49    /** the length of a value entry without its data */
50    private static final int VALUE_HEADER_LENGTH = 8;
51    /** the longest value entry.  Its length is a short, which is read as
52        signed */
53    private static final int MAX_VALUE_LENGTH = Short.MAX_VALUE;
54  
55    /** maps the PropertyMap name (case-insensitive) and block type to the
56        PropertyMap instance.  the type belongs in the key because a block of
57        type 0x02 holds the properties of an index, and access names an index
58        after its column by default, so a table can hold a column block and an
59        index block with the same name */
60    private final Map<Key,PropertyMapImpl> _maps =
61      new LinkedHashMap<>();
62    private final int _objectId;
63    private final RowIdImpl _rowId;
64    private final Handler _handler;
65    private final Owner _owner;
66  
67    public PropertyMaps(int objectId, RowIdImpl rowId, Handler handler,
68                        Owner owner) {
69      _objectId = objectId;
70      _rowId = rowId;
71      _handler = handler;
72      _owner = owner;
73    }
74  
75    public int getObjectId() {
76      return _objectId;
77    }
78  
79    public int getSize() {
80      return _maps.size();
81    }
82  
83    public boolean isEmpty() {
84      return _maps.isEmpty();
85    }
86  
87    /**
88     * @return the unnamed "default" PropertyMap in this group, creating if
89     *         necessary.
90     */
91    public PropertyMapImpl getDefault() {
92      return get(DEFAULT_NAME, DEFAULT_PROPERTY_VALUE_LIST);
93    }
94  
95    /**
96     * @return the PropertyMap with the given name in this group, creating if
97     *         necessary
98     */
99    public PropertyMapImpl get(String name) {
100     return get(name, COLUMN_PROPERTY_VALUE_LIST);
101   }
102 
103   /**
104    * @return the PropertyMap for the index with the given name in this group,
105    *         creating if necessary.  An index has its own block, and access
106    *         names an index after its column by default, so this is a separate
107    *         map from the one {@link #get} returns for the same name.
108    */
109   public PropertyMapImpl getIndex(String name) {
110     return get(name, INDEX_PROPERTY_VALUE_LIST);
111   }
112 
113   /**
114    * @return the PropertyMap with the given name and type in this group,
115    *         creating if necessary
116    */
117   private PropertyMapImpl get(String name, short type) {
118     Key key = new Key(name, type);
119     PropertyMapImpl map = _maps.get(key);
120     if(map == null) {
121       map = new PropertyMapImpl(name, type, this);
122       _maps.put(key, map);
123     }
124     return map;
125   }
126 
127   /**
128    * The name and block type which together identify a PropertyMap within one
129    * group.
130    */
131   private static final class Key
132   {
133     private final String _lookupName;
134     private final short _type;
135 
136     private Key(String name, short type) {
137       _lookupName = DatabaseImpl.toLookupName(name);
138       _type = type;
139     }
140 
141     @Override
142     public int hashCode() {
143       return _lookupName.hashCode() + _type;
144     }
145 
146     @Override
147     public boolean equals(Object o) {
148       return ((this == o) ||
149               ((o instanceof Key) && (_type == ((Key)o)._type) &&
150                _lookupName.equals(((Key)o)._lookupName)));
151     }
152 
153     @Override
154     public String toString() {
155       return _lookupName + "[" + _type + "]";
156     }
157   }
158 
159   @Override
160   public Iterator<PropertyMapImpl> iterator() {
161     return _maps.values().iterator();
162   }
163 
164   public byte[] write() throws IOException {
165     return _handler.write(this);
166   }
167 
168   public void save() throws IOException {
169     _handler.save(this);
170     if(_owner != null) {
171       _owner.propertiesUpdated();
172     }
173   }
174 
175   @Override
176   public String toString() {
177     return ToStringBuilder.builder(this)
178       .append(null, _maps.values())
179       .toString();
180   }
181 
182   public static String getTrimmedStringProperty(
183       PropertyMap props, String propName)
184   {
185     return StringUtil.trimToNull((String)props.getValue(propName));
186   }
187 
188   /**
189    * Utility class for reading/writing property blocks.
190    */
191   static final class Handler
192   {
193     /** the current database */
194     private final DatabaseImpl _database;
195     /** the system table "property" column */
196     private final ColumnImpl _propCol;
197     /** cache of PropColumns used to read/write property values */
198     private final Map<DataType,PropColumn> _columns =
199       new HashMap<>();
200 
201     Handler(DatabaseImpl database) {
202       _database = database;
203       _propCol = _database.getSystemCatalog().getColumn(
204           DatabaseImpl.CAT_COL_PROPS);
205     }
206 
207     /**
208      * @return a PropertyMaps instance decoded from the given bytes (always
209      *         returns non-{@code null} result).
210      */
211     public PropertyMaps read(byte[] propBytes, int objectId,
212                              RowIdImpl rowId, Owner owner)
213       throws IOException
214     {
215       PropertyMaps/impl/PropertyMaps.html#PropertyMaps">PropertyMaps maps = new PropertyMaps(objectId, rowId, this, owner);
216       if((propBytes == null) || (propBytes.length == 0)) {
217         return maps;
218       }
219 
220       ByteBuffer bb = PageChannel.wrap(propBytes);
221 
222       // check for known header
223       boolean knownType = false;
224       for(byte[] tmpType : JetFormat.PROPERTY_MAP_TYPES) {
225         if(ByteUtil.matchesRange(bb, bb.position(), tmpType)) {
226           ByteUtil.forward(bb, tmpType.length);
227           knownType = true;
228           break;
229         }
230       }
231 
232       if(!knownType) {
233         throw new IOException("Unknown property map type " +
234                               ByteUtil.toHexString(bb, 4));
235       }
236 
237       // parse each data "chunk"
238       List<String> propNames = null;
239       while(bb.hasRemaining()) {
240 
241         requireRemaining(bb, 6, "chunk header");
242         int len = bb.getInt();
243         short type = bb.getShort();
244         int endPos = requireEnd(bb, bb.position() + len - 6, "chunk");
245 
246         ByteBuffer bbBlock = PageChannel.narrowBuffer(bb, bb.position(),
247                                                       endPos);
248 
249         if(type == PROPERTY_NAME_LIST) {
250           propNames = readPropertyNames(bbBlock);
251         } else {
252           readPropertyValues(bbBlock, propNames, type, maps);
253         }
254 
255         bb.position(endPos);
256       }
257 
258       return maps;
259     }
260 
261     /**
262      * @return a byte[] encoded from the given PropertyMaps instance
263      */
264     public byte[] write(PropertyMaps maps)
265       throws IOException
266     {
267       if(maps == null) {
268         return null;
269       }
270 
271       ByteArrayBuilderpl/ByteArrayBuilder.html#ByteArrayBuilder">ByteArrayBuilder bab = new ByteArrayBuilder();
272 
273       bab.put(_database.getFormat().PROPERTY_MAP_TYPE);
274 
275       // grab the property names from all the maps
276       Set<String> propNames = new LinkedHashSet<>();
277       for(PropertyMapImpl propMap : maps) {
278         for(PropertyMap.Property prop : propMap) {
279           propNames.add(prop.getName());
280         }
281       }
282 
283       if(propNames.isEmpty()) {
284         return null;
285       }
286 
287       // write the full set of property names
288       writeBlock(null, propNames, PROPERTY_NAME_LIST, bab);
289 
290       // write all the map values
291       for(PropertyMapImpl propMap : maps) {
292         if(!propMap.isEmpty()) {
293           writeBlock(propMap, propNames, propMap.getType(), bab);
294         }
295       }
296 
297       return bab.toArray();
298     }
299 
300     /**
301      * Saves PropertyMaps instance to the db.
302      */
303     public void save(PropertyMaps maps) throws IOException
304     {
305       RowIdImpl rowId = maps._rowId;
306       if(rowId == null) {
307         throw new IllegalStateException(
308             "PropertyMaps cannot be saved without a row id");
309       }
310 
311       byte[] mapsBytes = write(maps);
312 
313       // for now assume all properties come from system catalog table
314       _propCol.getTable().updateValue(_propCol, rowId, mapsBytes);
315     }
316 
317     private void writeBlock(
318         PropertyMapImpl propMap, Set<String> propNames,
319         short blockType, ByteArrayBuilder bab)
320       throws IOException
321     {
322       int blockStartPos = bab.position();
323       bab.reserveInt()
324         .putShort(blockType);
325 
326       if(blockType == PROPERTY_NAME_LIST) {
327         writePropertyNames(propNames, bab);
328       } else {
329         writePropertyValues(propMap, propNames, bab);
330       }
331 
332       int len = bab.position() - blockStartPos;
333       bab.putInt(blockStartPos, len);
334     }
335 
336     /**
337      * @return the property names parsed from the given data chunk
338      */
339     private List<String> readPropertyNames(ByteBuffer bbBlock)
340       throws IOException
341     {
342       List<String> names = new ArrayList<>();
343       while(bbBlock.hasRemaining()) {
344         names.add(readPropName(bbBlock));
345       }
346       return names;
347     }
348 
349     private void writePropertyNames(Set<String> propNames,
350                                     ByteArrayBuilder bab) {
351       for(String propName : propNames) {
352         writePropName(propName, bab);
353       }
354     }
355 
356     /**
357      * @return the PropertyMap created from the values parsed from the given
358      *         data chunk combined with the given property names
359      */
360     private PropertyMapImpl readPropertyValues(
361         ByteBuffer bbBlock, List<String> propNames, short blockType,
362         PropertyMaps maps)
363       throws IOException
364     {
365       String mapName = DEFAULT_NAME;
366 
367       if(bbBlock.hasRemaining()) {
368 
369         // read the map name, if any
370         requireRemaining(bbBlock, 4, "map name header");
371         int nameBlockLen = bbBlock.getInt();
372         int endPos = requireEnd(bbBlock, bbBlock.position() + nameBlockLen - 4,
373                                 "map name");
374         if(nameBlockLen > 6) {
375           mapName = readPropName(bbBlock);
376         }
377         bbBlock.position(endPos);
378       }
379 
380       PropertyMapImpl map = maps.get(mapName, blockType);
381 
382       // read the values
383       while(bbBlock.hasRemaining()) {
384 
385         requireRemaining(bbBlock, 8, "value header");
386         int valLen = bbBlock.getShort();
387         int endPos = requireEnd(bbBlock, bbBlock.position() + valLen - 2,
388                                 "value");
389         byte flags = bbBlock.get();
390         DataType dataType = DataType.fromByte(bbBlock.get());
391         int nameIdx = bbBlock.getShort();
392         int dataSize = bbBlock.getShort();
393 
394         // the name list is a chunk of its own, which a corrupt block can
395         // leave out or put after the values which use it
396         if((propNames == null) || (nameIdx < 0) ||
397            (nameIdx >= propNames.size())) {
398           throw new IOException(
399               "Property name index " + nameIdx + " is not one of the " +
400               ((propNames != null) ? propNames.size() : 0) + " known names");
401         }
402         String propName = propNames.get(nameIdx);
403         PropColumn col = getColumn(dataType, propName, dataSize, null);
404 
405         if((dataSize < 0) || (dataSize > bbBlock.remaining())) {
406           throw new IOException(
407               "Property " + propName + " holds " + dataSize + " bytes, " +
408               bbBlock.remaining() + " left in the block");
409         }
410         // a fixed length value which is short would underflow the decode
411         DataType colType = col.getType();
412         if(!colType.isVariableLength() &&
413            (dataSize < colType.getFixedSize())) {
414           throw new IOException(
415               "Property " + propName + " holds " + dataSize +
416               " bytes, too few for " + colType);
417         }
418 
419         byte[] data = ByteUtil.getBytes(bbBlock, dataSize);
420         Object value = col.read(data);
421 
422         map.putStored(propName, dataType, value, flags);
423 
424         bbBlock.position(endPos);
425       }
426 
427       return map;
428     }
429 
430     private void writePropertyValues(
431         PropertyMapImpl propMap, Set<String> propNames, ByteArrayBuilder bab)
432       throws IOException
433     {
434       // write the map name, if any
435       String mapName = propMap.getName();
436       int blockStartPos = bab.position();
437       bab.reserveInt();
438       writePropName(mapName, bab);
439       int len = bab.position() - blockStartPos;
440       bab.putInt(blockStartPos, len);
441 
442       // write the map values
443       int nameIdx = 0;
444       for(String propName : propNames) {
445 
446         PropertyMapImpl.PropertyImpl prop = (PropertyMapImpl.PropertyImpl)
447           propMap.get(propName);
448 
449         if(prop != null) {
450 
451           Object value = prop.getValue();
452           if(value != null) {
453 
454             int valStartPos = bab.position();
455             bab.reserveShort();
456 
457             // the whole byte, not just the ddl bit
458             bab.put(prop.getFlags());
459             bab.put(prop.getType().getValue());
460             bab.putShort((short)nameIdx);
461 
462             PropColumn col = getColumn(prop.getType(), propName, -1, value);
463 
464             ByteBuffer data;
465             try {
466               data = col.write(value, _database.getFormat().MAX_ROW_SIZE);
467             } catch(InvalidValueException e) {
468               throw new InvalidValueException(
469                   "Invalid value for property " + propName + ": " +
470                   e.getMessage(), e);
471             }
472             if((VALUE_HEADER_LENGTH + data.remaining()) > MAX_VALUE_LENGTH) {
473               throw new InvalidValueException(
474                   "Invalid value for property " + propName + ": " +
475                   data.remaining() + " bytes is too long, max " +
476                   (MAX_VALUE_LENGTH - VALUE_HEADER_LENGTH) + " (Db=" +
477                   _database.getName() + ")");
478             }
479 
480             bab.putShort((short)data.remaining());
481             bab.put(data);
482 
483             len = bab.position() - valStartPos;
484             bab.putShort(valStartPos, (short)len);
485           }
486         }
487 
488         ++nameIdx;
489       }
490     }
491 
492     /**
493      * A length in a property block is a signed field read from the file, so
494      * it can promise more bytes than the block holds.
495      */
496     private static void requireRemaining(ByteBuffer buffer, int len,
497                                          String what)
498       throws IOException
499     {
500       if(buffer.remaining() < len) {
501         throw new IOException(
502             "Property " + what + " is " + len + " bytes, " +
503             buffer.remaining() + " left in the block");
504       }
505     }
506 
507     /**
508      * @return the given end position, checked against the block
509      */
510     private static int requireEnd(ByteBuffer buffer, int endPos, String what)
511       throws IOException
512     {
513       if((endPos < buffer.position()) || (endPos > buffer.limit())) {
514         throw new IOException(
515             "Property " + what + " ends at " + endPos + ", outside " +
516             buffer.position() + " to " + buffer.limit());
517       }
518       return endPos;
519     }
520 
521     /**
522      * Reads a property name from the given data block
523      */
524     private String readPropName(ByteBuffer buffer) throws IOException {
525       requireRemaining(buffer, 2, "name length");
526       int nameLength = buffer.getShort();
527       if((nameLength < 0) || (nameLength > buffer.remaining())) {
528         throw new IOException(
529             "Property name is " + nameLength + " bytes, " +
530             buffer.remaining() + " left in the block");
531       }
532       byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength);
533       return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset());
534     }
535 
536     /**
537      * Writes a property name to the given data block
538      */
539     private void writePropName(String propName, ByteArrayBuilder bab) {
540       ByteBuffer textBuf = ColumnImpl.encodeUncompressedText(
541           propName, _database.getCharset());
542       bab.putShort((short)textBuf.remaining());
543       bab.put(textBuf);
544     }
545 
546     /**
547      * Gets a PropColumn capable of reading/writing a property of the given
548      * DataType
549      */
550     private PropColumn getColumn(DataType dataType, String propName,
551                                  int dataSize, Object value)
552       throws IOException
553     {
554 
555       if(isPseudoGuidColumn(dataType, propName, dataSize, value)) {
556         dataType = DataType.GUID;
557       }
558 
559       PropColumn col = _columns.get(dataType);
560 
561       if(col == null) {
562 
563         // translate long value types into simple types
564         DataType colType = dataType;
565         if(dataType == DataType.MEMO) {
566           colType = DataType.TEXT;
567         } else if(dataType == DataType.OLE) {
568           colType = DataType.BINARY;
569         }
570 
571         // create column with ability to read/write the given data type
572         if(colType == DataType.BOOLEAN) {
573           col = new BooleanPropColumn();
574         } else if(dataType == DataType.MEMO) {
575           // the length of the whole value entry is checked when it is
576           // written
577           col = new PropColumn(colType, Integer.MAX_VALUE);
578         } else {
579           col = new PropColumn(colType);
580         }
581 
582         _columns.put(dataType, col);
583       }
584 
585       return col;
586     }
587 
588     private static boolean isPseudoGuidColumn(
589         DataType dataType, String propName, int dataSize, Object value)
590       throws IOException
591     {
592       // guids seem to be marked as "binary" fields
593       return((dataType == DataType.BINARY) &&
594              ((dataSize == DataType.GUID.getFixedSize()) ||
595               ((dataSize == -1) && ColumnImpl.isGUIDValue(value))) &&
596              PropertyMap.GUID_PROP.equalsIgnoreCase(propName));
597     }
598 
599     /**
600      * Column adapted to work w/out a Table.
601      */
602     private class PropColumn extends ColumnImpl
603     {
604       /** the longest text value this column writes */
605       private final int _maxChars;
606 
607       private PropColumn(DataType type) {
608         super(null, null, type, 0, 0, 0);
609         _maxChars = getLengthInUnits();
610       }
611 
612       private PropColumn(DataType type, int maxChars) {
613         super(null, null, type, 0, 0, 0);
614         _maxChars = maxChars;
615       }
616 
617       @Override
618       public DatabaseImpl getDatabase() {
619         return _database;
620       }
621 
622       @Override
623       protected ByteBuffer writeRealData(Object obj, int remainingRowLength,
624                                          ByteOrder order)
625         throws IOException
626       {
627         if(getType() == DataType.TEXT) {
628           return encodeTextValue(obj, 0, _maxChars, false).order(order);
629         }
630         return super.writeRealData(obj, remainingRowLength, order);
631       }
632 
633       @Override
634       String withErrorContext(String msg) {
635         // there is no table, so the property name is added by the caller
636         return msg + " (Db=" + _database.getName() + ")";
637       }
638     }
639 
640     /**
641      * Normal boolean columns do not write into the actual row data, so we
642      * need to do a little extra work.
643      */
644     private final class BooleanPropColumn extends PropColumn
645     {
646       private BooleanPropColumn() {
647         super(DataType.BOOLEAN);
648       }
649 
650       @Override
651       public Object read(byte[] data) {
652         return ((data[0] != 0) ? Boolean.TRUE : Boolean.FALSE);
653       }
654 
655       @Override
656       public ByteBuffer write(Object obj, int remainingRowLength)
657       {
658         ByteBuffer buffer = PageChannel.createBuffer(1);
659         buffer.put(((Number)booleanToInteger(obj)).byteValue());
660         buffer.flip();
661         return buffer;
662       }
663     }
664   }
665 
666   /**
667    * Utility interface for the object which owns the PropertyMaps
668    */
669   public static interface Owner {
670 
671     /**
672      * Invoked when new properties are saved.
673      */
674     public void propertiesUpdated() throws IOException;
675   }
676 }