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
47
48
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
78
79
80 public PropertyMapImpl getDefault() {
81 return get(DEFAULT_NAME, DEFAULT_PROPERTY_VALUE_LIST);
82 }
83
84
85
86
87
88 public PropertyMapImpl get(String name) {
89 return get(name, COLUMN_PROPERTY_VALUE_LIST);
90 }
91
92
93
94
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
137
138 static final class Handler
139 {
140
141 private final DatabaseImpl _database;
142
143 private final ColumnImpl _propCol;
144
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
156
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
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
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
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
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
234 writeBlock(null, propNames, PROPERTY_NAME_LIST, bab);
235
236
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
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
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
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
302
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
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
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
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
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
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
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
418
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
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
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
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
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
479
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
506
507 static interface Owner {
508
509
510
511
512 public void propertiesUpdated() throws IOException;
513 }
514 }