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