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.nio.ByteOrder;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.LinkedHashMap;
26 import java.util.LinkedHashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30
31 import com.healthmarketscience.jackcess.DataType;
32 import com.healthmarketscience.jackcess.InvalidValueException;
33 import com.healthmarketscience.jackcess.PropertyMap;
34
35
36
37
38
39
40 public class PropertyMaps implements Iterable<PropertyMapImpl>
41 {
42
43 public static final String DEFAULT_NAME = "";
44
45 private static final short PROPERTY_NAME_LIST = 0x80;
46 private static final short DEFAULT_PROPERTY_VALUE_LIST = 0x00;
47 private static final short COLUMN_PROPERTY_VALUE_LIST = 0x01;
48 private static final short INDEX_PROPERTY_VALUE_LIST = 0x02;
49
50 private static final int VALUE_HEADER_LENGTH = 8;
51
52
53 private static final int MAX_VALUE_LENGTH = Short.MAX_VALUE;
54
55
56
57
58
59
60 private final Map<Key,PropertyMapImpl> _maps =
61 new LinkedHashMap<>();
62 private final int _objectId;
63 private final RowIdImpl _rowId;
64 private final Handler _handler;
65 private final Owner _owner;
66
67 public PropertyMaps(int objectId, RowIdImpl rowId, Handler handler,
68 Owner owner) {
69 _objectId = objectId;
70 _rowId = rowId;
71 _handler = handler;
72 _owner = owner;
73 }
74
75 public int getObjectId() {
76 return _objectId;
77 }
78
79 public int getSize() {
80 return _maps.size();
81 }
82
83 public boolean isEmpty() {
84 return _maps.isEmpty();
85 }
86
87
88
89
90
91 public PropertyMapImpl getDefault() {
92 return get(DEFAULT_NAME, DEFAULT_PROPERTY_VALUE_LIST);
93 }
94
95
96
97
98
99 public PropertyMapImpl get(String name) {
100 return get(name, COLUMN_PROPERTY_VALUE_LIST);
101 }
102
103
104
105
106
107
108
109 public PropertyMapImpl getIndex(String name) {
110 return get(name, INDEX_PROPERTY_VALUE_LIST);
111 }
112
113
114
115
116
117 private PropertyMapImpl get(String name, short type) {
118 Key key = new Key(name, type);
119 PropertyMapImpl map = _maps.get(key);
120 if(map == null) {
121 map = new PropertyMapImpl(name, type, this);
122 _maps.put(key, map);
123 }
124 return map;
125 }
126
127
128
129
130
131 private static final class Key
132 {
133 private final String _lookupName;
134 private final short _type;
135
136 private Key(String name, short type) {
137 _lookupName = DatabaseImpl.toLookupName(name);
138 _type = type;
139 }
140
141 @Override
142 public int hashCode() {
143 return _lookupName.hashCode() + _type;
144 }
145
146 @Override
147 public boolean equals(Object o) {
148 return ((this == o) ||
149 ((o instanceof Key) && (_type == ((Key)o)._type) &&
150 _lookupName.equals(((Key)o)._lookupName)));
151 }
152
153 @Override
154 public String toString() {
155 return _lookupName + "[" + _type + "]";
156 }
157 }
158
159 @Override
160 public Iterator<PropertyMapImpl> iterator() {
161 return _maps.values().iterator();
162 }
163
164 public byte[] write() throws IOException {
165 return _handler.write(this);
166 }
167
168 public void save() throws IOException {
169 _handler.save(this);
170 if(_owner != null) {
171 _owner.propertiesUpdated();
172 }
173 }
174
175 @Override
176 public String toString() {
177 return ToStringBuilder.builder(this)
178 .append(null, _maps.values())
179 .toString();
180 }
181
182 public static String getTrimmedStringProperty(
183 PropertyMap props, String propName)
184 {
185 return StringUtil.trimToNull((String)props.getValue(propName));
186 }
187
188
189
190
191 static final class Handler
192 {
193
194 private final DatabaseImpl _database;
195
196 private final ColumnImpl _propCol;
197
198 private final Map<DataType,PropColumn> _columns =
199 new HashMap<>();
200
201 Handler(DatabaseImpl database) {
202 _database = database;
203 _propCol = _database.getSystemCatalog().getColumn(
204 DatabaseImpl.CAT_COL_PROPS);
205 }
206
207
208
209
210
211 public PropertyMaps read(byte[] propBytes, int objectId,
212 RowIdImpl rowId, Owner owner)
213 throws IOException
214 {
215 PropertyMaps/impl/PropertyMaps.html#PropertyMaps">PropertyMaps maps = new PropertyMaps(objectId, rowId, this, owner);
216 if((propBytes == null) || (propBytes.length == 0)) {
217 return maps;
218 }
219
220 ByteBuffer bb = PageChannel.wrap(propBytes);
221
222
223 boolean knownType = false;
224 for(byte[] tmpType : JetFormat.PROPERTY_MAP_TYPES) {
225 if(ByteUtil.matchesRange(bb, bb.position(), tmpType)) {
226 ByteUtil.forward(bb, tmpType.length);
227 knownType = true;
228 break;
229 }
230 }
231
232 if(!knownType) {
233 throw new IOException("Unknown property map type " +
234 ByteUtil.toHexString(bb, 4));
235 }
236
237
238 List<String> propNames = null;
239 while(bb.hasRemaining()) {
240
241 requireRemaining(bb, 6, "chunk header");
242 int len = bb.getInt();
243 short type = bb.getShort();
244 int endPos = requireEnd(bb, bb.position() + len - 6, "chunk");
245
246 ByteBuffer bbBlock = PageChannel.narrowBuffer(bb, bb.position(),
247 endPos);
248
249 if(type == PROPERTY_NAME_LIST) {
250 propNames = readPropertyNames(bbBlock);
251 } else {
252 readPropertyValues(bbBlock, propNames, type, maps);
253 }
254
255 bb.position(endPos);
256 }
257
258 return maps;
259 }
260
261
262
263
264 public byte[] write(PropertyMaps maps)
265 throws IOException
266 {
267 if(maps == null) {
268 return null;
269 }
270
271 ByteArrayBuilderpl/ByteArrayBuilder.html#ByteArrayBuilder">ByteArrayBuilder bab = new ByteArrayBuilder();
272
273 bab.put(_database.getFormat().PROPERTY_MAP_TYPE);
274
275
276 Set<String> propNames = new LinkedHashSet<>();
277 for(PropertyMapImpl propMap : maps) {
278 for(PropertyMap.Property prop : propMap) {
279 propNames.add(prop.getName());
280 }
281 }
282
283 if(propNames.isEmpty()) {
284 return null;
285 }
286
287
288 writeBlock(null, propNames, PROPERTY_NAME_LIST, bab);
289
290
291 for(PropertyMapImpl propMap : maps) {
292 if(!propMap.isEmpty()) {
293 writeBlock(propMap, propNames, propMap.getType(), bab);
294 }
295 }
296
297 return bab.toArray();
298 }
299
300
301
302
303 public void save(PropertyMaps maps) throws IOException
304 {
305 RowIdImpl rowId = maps._rowId;
306 if(rowId == null) {
307 throw new IllegalStateException(
308 "PropertyMaps cannot be saved without a row id");
309 }
310
311 byte[] mapsBytes = write(maps);
312
313
314 _propCol.getTable().updateValue(_propCol, rowId, mapsBytes);
315 }
316
317 private void writeBlock(
318 PropertyMapImpl propMap, Set<String> propNames,
319 short blockType, ByteArrayBuilder bab)
320 throws IOException
321 {
322 int blockStartPos = bab.position();
323 bab.reserveInt()
324 .putShort(blockType);
325
326 if(blockType == PROPERTY_NAME_LIST) {
327 writePropertyNames(propNames, bab);
328 } else {
329 writePropertyValues(propMap, propNames, bab);
330 }
331
332 int len = bab.position() - blockStartPos;
333 bab.putInt(blockStartPos, len);
334 }
335
336
337
338
339 private List<String> readPropertyNames(ByteBuffer bbBlock)
340 throws IOException
341 {
342 List<String> names = new ArrayList<>();
343 while(bbBlock.hasRemaining()) {
344 names.add(readPropName(bbBlock));
345 }
346 return names;
347 }
348
349 private void writePropertyNames(Set<String> propNames,
350 ByteArrayBuilder bab) {
351 for(String propName : propNames) {
352 writePropName(propName, bab);
353 }
354 }
355
356
357
358
359
360 private PropertyMapImpl readPropertyValues(
361 ByteBuffer bbBlock, List<String> propNames, short blockType,
362 PropertyMaps maps)
363 throws IOException
364 {
365 String mapName = DEFAULT_NAME;
366
367 if(bbBlock.hasRemaining()) {
368
369
370 requireRemaining(bbBlock, 4, "map name header");
371 int nameBlockLen = bbBlock.getInt();
372 int endPos = requireEnd(bbBlock, bbBlock.position() + nameBlockLen - 4,
373 "map name");
374 if(nameBlockLen > 6) {
375 mapName = readPropName(bbBlock);
376 }
377 bbBlock.position(endPos);
378 }
379
380 PropertyMapImpl map = maps.get(mapName, blockType);
381
382
383 while(bbBlock.hasRemaining()) {
384
385 requireRemaining(bbBlock, 8, "value header");
386 int valLen = bbBlock.getShort();
387 int endPos = requireEnd(bbBlock, bbBlock.position() + valLen - 2,
388 "value");
389 byte flags = bbBlock.get();
390 DataType dataType = DataType.fromByte(bbBlock.get());
391 int nameIdx = bbBlock.getShort();
392 int dataSize = bbBlock.getShort();
393
394
395
396 if((propNames == null) || (nameIdx < 0) ||
397 (nameIdx >= propNames.size())) {
398 throw new IOException(
399 "Property name index " + nameIdx + " is not one of the " +
400 ((propNames != null) ? propNames.size() : 0) + " known names");
401 }
402 String propName = propNames.get(nameIdx);
403 PropColumn col = getColumn(dataType, propName, dataSize, null);
404
405 if((dataSize < 0) || (dataSize > bbBlock.remaining())) {
406 throw new IOException(
407 "Property " + propName + " holds " + dataSize + " bytes, " +
408 bbBlock.remaining() + " left in the block");
409 }
410
411 DataType colType = col.getType();
412 if(!colType.isVariableLength() &&
413 (dataSize < colType.getFixedSize())) {
414 throw new IOException(
415 "Property " + propName + " holds " + dataSize +
416 " bytes, too few for " + colType);
417 }
418
419 byte[] data = ByteUtil.getBytes(bbBlock, dataSize);
420 Object value = col.read(data);
421
422 map.putStored(propName, dataType, value, flags);
423
424 bbBlock.position(endPos);
425 }
426
427 return map;
428 }
429
430 private void writePropertyValues(
431 PropertyMapImpl propMap, Set<String> propNames, ByteArrayBuilder bab)
432 throws IOException
433 {
434
435 String mapName = propMap.getName();
436 int blockStartPos = bab.position();
437 bab.reserveInt();
438 writePropName(mapName, bab);
439 int len = bab.position() - blockStartPos;
440 bab.putInt(blockStartPos, len);
441
442
443 int nameIdx = 0;
444 for(String propName : propNames) {
445
446 PropertyMapImpl.PropertyImpl prop = (PropertyMapImpl.PropertyImpl)
447 propMap.get(propName);
448
449 if(prop != null) {
450
451 Object value = prop.getValue();
452 if(value != null) {
453
454 int valStartPos = bab.position();
455 bab.reserveShort();
456
457
458 bab.put(prop.getFlags());
459 bab.put(prop.getType().getValue());
460 bab.putShort((short)nameIdx);
461
462 PropColumn col = getColumn(prop.getType(), propName, -1, value);
463
464 ByteBuffer data;
465 try {
466 data = col.write(value, _database.getFormat().MAX_ROW_SIZE);
467 } catch(InvalidValueException e) {
468 throw new InvalidValueException(
469 "Invalid value for property " + propName + ": " +
470 e.getMessage(), e);
471 }
472 if((VALUE_HEADER_LENGTH + data.remaining()) > MAX_VALUE_LENGTH) {
473 throw new InvalidValueException(
474 "Invalid value for property " + propName + ": " +
475 data.remaining() + " bytes is too long, max " +
476 (MAX_VALUE_LENGTH - VALUE_HEADER_LENGTH) + " (Db=" +
477 _database.getName() + ")");
478 }
479
480 bab.putShort((short)data.remaining());
481 bab.put(data);
482
483 len = bab.position() - valStartPos;
484 bab.putShort(valStartPos, (short)len);
485 }
486 }
487
488 ++nameIdx;
489 }
490 }
491
492
493
494
495
496 private static void requireRemaining(ByteBuffer buffer, int len,
497 String what)
498 throws IOException
499 {
500 if(buffer.remaining() < len) {
501 throw new IOException(
502 "Property " + what + " is " + len + " bytes, " +
503 buffer.remaining() + " left in the block");
504 }
505 }
506
507
508
509
510 private static int requireEnd(ByteBuffer buffer, int endPos, String what)
511 throws IOException
512 {
513 if((endPos < buffer.position()) || (endPos > buffer.limit())) {
514 throw new IOException(
515 "Property " + what + " ends at " + endPos + ", outside " +
516 buffer.position() + " to " + buffer.limit());
517 }
518 return endPos;
519 }
520
521
522
523
524 private String readPropName(ByteBuffer buffer) throws IOException {
525 requireRemaining(buffer, 2, "name length");
526 int nameLength = buffer.getShort();
527 if((nameLength < 0) || (nameLength > buffer.remaining())) {
528 throw new IOException(
529 "Property name is " + nameLength + " bytes, " +
530 buffer.remaining() + " left in the block");
531 }
532 byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength);
533 return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset());
534 }
535
536
537
538
539 private void writePropName(String propName, ByteArrayBuilder bab) {
540 ByteBuffer textBuf = ColumnImpl.encodeUncompressedText(
541 propName, _database.getCharset());
542 bab.putShort((short)textBuf.remaining());
543 bab.put(textBuf);
544 }
545
546
547
548
549
550 private PropColumn getColumn(DataType dataType, String propName,
551 int dataSize, Object value)
552 throws IOException
553 {
554
555 if(isPseudoGuidColumn(dataType, propName, dataSize, value)) {
556 dataType = DataType.GUID;
557 }
558
559 PropColumn col = _columns.get(dataType);
560
561 if(col == null) {
562
563
564 DataType colType = dataType;
565 if(dataType == DataType.MEMO) {
566 colType = DataType.TEXT;
567 } else if(dataType == DataType.OLE) {
568 colType = DataType.BINARY;
569 }
570
571
572 if(colType == DataType.BOOLEAN) {
573 col = new BooleanPropColumn();
574 } else if(dataType == DataType.MEMO) {
575
576
577 col = new PropColumn(colType, Integer.MAX_VALUE);
578 } else {
579 col = new PropColumn(colType);
580 }
581
582 _columns.put(dataType, col);
583 }
584
585 return col;
586 }
587
588 private static boolean isPseudoGuidColumn(
589 DataType dataType, String propName, int dataSize, Object value)
590 throws IOException
591 {
592
593 return((dataType == DataType.BINARY) &&
594 ((dataSize == DataType.GUID.getFixedSize()) ||
595 ((dataSize == -1) && ColumnImpl.isGUIDValue(value))) &&
596 PropertyMap.GUID_PROP.equalsIgnoreCase(propName));
597 }
598
599
600
601
602 private class PropColumn extends ColumnImpl
603 {
604
605 private final int _maxChars;
606
607 private PropColumn(DataType type) {
608 super(null, null, type, 0, 0, 0);
609 _maxChars = getLengthInUnits();
610 }
611
612 private PropColumn(DataType type, int maxChars) {
613 super(null, null, type, 0, 0, 0);
614 _maxChars = maxChars;
615 }
616
617 @Override
618 public DatabaseImpl getDatabase() {
619 return _database;
620 }
621
622 @Override
623 protected ByteBuffer writeRealData(Object obj, int remainingRowLength,
624 ByteOrder order)
625 throws IOException
626 {
627 if(getType() == DataType.TEXT) {
628 return encodeTextValue(obj, 0, _maxChars, false).order(order);
629 }
630 return super.writeRealData(obj, remainingRowLength, order);
631 }
632
633 @Override
634 String withErrorContext(String msg) {
635
636 return msg + " (Db=" + _database.getName() + ")";
637 }
638 }
639
640
641
642
643
644 private final class BooleanPropColumn extends PropColumn
645 {
646 private BooleanPropColumn() {
647 super(DataType.BOOLEAN);
648 }
649
650 @Override
651 public Object read(byte[] data) {
652 return ((data[0] != 0) ? Boolean.TRUE : Boolean.FALSE);
653 }
654
655 @Override
656 public ByteBuffer write(Object obj, int remainingRowLength)
657 {
658 ByteBuffer buffer = PageChannel.createBuffer(1);
659 buffer.put(((Number)booleanToInteger(obj)).byteValue());
660 buffer.flip();
661 return buffer;
662 }
663 }
664 }
665
666
667
668
669 public static interface Owner {
670
671
672
673
674 public void propertiesUpdated() throws IOException;
675 }
676 }