1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
35
36
37
38 public class PropertyMaps implements Iterable<PropertyMapImpl>
39 {
40
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
49
50
51
52
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
82
83
84 public PropertyMapImpl getDefault() {
85 return get(DEFAULT_NAME, DEFAULT_PROPERTY_VALUE_LIST);
86 }
87
88
89
90
91
92 public PropertyMapImpl get(String name) {
93 return get(name, COLUMN_PROPERTY_VALUE_LIST);
94 }
95
96
97
98
99
100
101
102 public PropertyMapImpl getIndex(String name) {
103 return get(name, INDEX_PROPERTY_VALUE_LIST);
104 }
105
106
107
108
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
122
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
183
184 static final class Handler
185 {
186
187 private final DatabaseImpl _database;
188
189 private final ColumnImpl _propCol;
190
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
202
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
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
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
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
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
280 writeBlock(null, propNames, PROPERTY_NAME_LIST, bab);
281
282
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
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
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
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
348
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
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
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
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
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
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
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
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
464
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
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
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
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
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
525
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
551
552 static interface Owner {
553
554
555
556
557 public void propertiesUpdated() throws IOException;
558 }
559 }