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         boolean isDdl = (bbBlock.get() != 0);
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, value, isDdl);
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             byte ddlFlag = (byte)(prop.isDdl() ? 1 : 0);
363             bab.put(ddlFlag);
364             bab.put(prop.getType().getValue());
365             bab.putShort((short)nameIdx);
366 
367             PropColumn col = getColumn(prop.getType(), propName, -1, value);
368 
369             ByteBuffer data = col.write(
370                 value, _database.getFormat().MAX_ROW_SIZE);
371 
372             bab.putShort((short)data.remaining());
373             bab.put(data);
374 
375             len = bab.position() - valStartPos;
376             bab.putShort(valStartPos, (short)len);
377           }
378         }
379 
380         ++nameIdx;
381       }
382     }
383 
384     /**
385      * Reads a property name from the given data block
386      */
387     private String readPropName(ByteBuffer buffer) { 
388       int nameLength = buffer.getShort();
389       byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength);
390       return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset());
391     }
392 
393     /**
394      * Writes a property name to the given data block
395      */
396     private void writePropName(String propName, ByteArrayBuilder bab) {
397       ByteBuffer textBuf = ColumnImpl.encodeUncompressedText(
398           propName, _database.getCharset());
399       bab.putShort((short)textBuf.remaining());
400       bab.put(textBuf);
401     }
402 
403     /**
404      * Gets a PropColumn capable of reading/writing a property of the given
405      * DataType
406      */
407     private PropColumn getColumn(DataType dataType, String propName, 
408                                  int dataSize, Object value) 
409       throws IOException
410     {
411 
412       if(isPseudoGuidColumn(dataType, propName, dataSize, value)) {
413         dataType = DataType.GUID;
414       }
415 
416       PropColumn col = _columns.get(dataType);
417 
418       if(col == null) {
419 
420         // translate long value types into simple types
421         DataType colType = dataType;
422         if(dataType == DataType.MEMO) {
423           colType = DataType.TEXT;
424         } else if(dataType == DataType.OLE) {
425           colType = DataType.BINARY;
426         }
427 
428         // create column with ability to read/write the given data type
429         col = ((colType == DataType.BOOLEAN) ? 
430                new BooleanPropColumn() : new PropColumn(colType));
431 
432         _columns.put(dataType, col);
433       }
434 
435       return col;
436     }
437 
438     private static boolean isPseudoGuidColumn(
439         DataType dataType, String propName, int dataSize, Object value) 
440       throws IOException
441     {
442       // guids seem to be marked as "binary" fields
443       return((dataType == DataType.BINARY) && 
444              ((dataSize == DataType.GUID.getFixedSize()) ||
445               ((dataSize == -1) && ColumnImpl.isGUIDValue(value))) &&
446              PropertyMap.GUID_PROP.equalsIgnoreCase(propName));
447     }
448 
449     /**
450      * Column adapted to work w/out a Table.
451      */
452     private class PropColumn extends ColumnImpl
453     {
454       private PropColumn(DataType type) {
455         super(null, null, type, 0, 0, 0);
456       }
457       
458       @Override
459       public DatabaseImpl getDatabase() {
460         return _database;
461       }
462     }
463 
464     /**
465      * Normal boolean columns do not write into the actual row data, so we
466      * need to do a little extra work.
467      */
468     private final class BooleanPropColumn extends PropColumn
469     {
470       private BooleanPropColumn() {
471         super(DataType.BOOLEAN);
472       }
473 
474       @Override
475       public Object read(byte[] data) throws IOException {
476         return ((data[0] != 0) ? Boolean.TRUE : Boolean.FALSE);
477       }
478 
479       @Override
480       public ByteBuffer write(Object obj, int remainingRowLength)
481         throws IOException
482       {
483         ByteBuffer buffer = PageChannel.createBuffer(1);
484         buffer.put(((Number)booleanToInteger(obj)).byteValue());
485         buffer.flip();
486         return buffer;
487       }
488     }
489   }
490 }