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