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