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.time.LocalDateTime;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Objects;
28
29 import com.healthmarketscience.jackcess.Column;
30 import com.healthmarketscience.jackcess.NameMap;
31 import com.healthmarketscience.jackcess.PropertyMap;
32 import com.healthmarketscience.jackcess.Table;
33 import com.healthmarketscience.jackcess.query.Query;
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 public abstract class NameMapImpl implements NameMap
53 {
54 private static final int MAGIC = 0x550ECC0A;
55 private static final int MAGIC_LENGTH = 4;
56 private static final int HEADER_LENGTH = 8;
57 private static final int GUID_LENGTH = 16;
58 private static final int PAYLOAD_LENGTH = 16;
59
60 private static final int FIXED_LENGTH = GUID_LENGTH + 4 + PAYLOAD_LENGTH;
61 private static final int TRAILER_LENGTH = 4;
62
63 static final int TABLE_TYPE = 0;
64 static final int REFERENCE_TYPE = 6;
65 static final int COLUMN_TYPE = 7;
66 static final int END_TYPE = 12;
67
68
69
70 private final Integer _reserved;
71 final List<RecordImpl> _records = new ArrayList<>();
72
73
74
75
76
77 protected NameMapImpl(byte[] bytes) throws IOException
78 {
79 ByteBuffer bb = PageChannel.wrap(bytes);
80 if((bb.remaining() < MAGIC_LENGTH) || (bb.getInt() != MAGIC) ||
81 (bb.hasRemaining() && (bb.remaining() < MAGIC_LENGTH))) {
82 throw new IOException(
83 "Unknown name map type " +
84 ByteUtil.toHexString(PageChannel.wrap(bytes), 0,
85 Math.min(bytes.length, MAGIC_LENGTH)));
86 }
87 _reserved = (bb.hasRemaining() ? bb.getInt() : null);
88
89 while(bb.hasRemaining()) {
90 if(bb.remaining() < FIXED_LENGTH) {
91 throw new IOException("Name map ends inside a record");
92 }
93 byte[] guid = ByteUtil.getBytes(bb, GUID_LENGTH);
94 int type = bb.getInt();
95 byte[] payload = ByteUtil.getBytes(bb, PAYLOAD_LENGTH);
96 String name = readName(bb);
97 if(bb.hasRemaining()) {
98
99
100 if((bb.remaining() < TRAILER_LENGTH) || (bb.getInt() != 0) ||
101 !bb.hasRemaining()) {
102 throw new IOException(
103 "Name map record " + name + " has an unexpected end");
104 }
105 }
106 _records.add(new RecordImpl(type, guid, payload, name));
107 }
108 }
109
110
111
112
113
114 private static String readName(ByteBuffer bb) throws IOException {
115 StringBuilder sb = new StringBuilder();
116 while(bb.remaining() >= 2) {
117 char c = bb.getChar();
118 if(c == 0) {
119 return sb.toString();
120 }
121 sb.append(c);
122 }
123 throw new IOException("Name map ends inside a name");
124 }
125
126
127
128
129 protected abstract boolean isWritable();
130
131 @Override
132 public Iterator<Record> iterator() {
133 return Collections.<Record>unmodifiableList(_records).iterator();
134 }
135
136 @Override
137 public List<Record> getTableRecords() {
138 return getRecords(TABLE_TYPE);
139 }
140
141 @Override
142 public List<Record> getColumnRecords() {
143 return getRecords(COLUMN_TYPE);
144 }
145
146 List<Record> getRecords(int type) {
147 List<Record> recs = new ArrayList<>();
148 for(RecordImpl rec : _records) {
149 if(rec._type == type) {
150 recs.add(rec);
151 }
152 }
153 return recs;
154 }
155
156 @Override
157 public Record getRecord(String guid) {
158 return findRecord(guid);
159 }
160
161 RecordImpl findRecord(String guid) {
162 for(RecordImpl rec : _records) {
163 if(((rec._type == TABLE_TYPE) || (rec._type == COLUMN_TYPE)) &&
164 guid.equalsIgnoreCase(rec.getGuid())) {
165 return rec;
166 }
167 }
168 return null;
169 }
170
171 @Override
172 public Integer getWriterTag() {
173 for(RecordImpl rec : _records) {
174 if(rec._type == END_TYPE) {
175 return PageChannel.wrap(rec._payload).getInt();
176 }
177 }
178 return null;
179 }
180
181
182
183
184 public byte[] toBytes() {
185 if((_reserved == null) && _records.isEmpty()) {
186 return PageChannel.createBuffer(MAGIC_LENGTH).putInt(MAGIC).array();
187 }
188
189 int len = HEADER_LENGTH;
190 for(RecordImpl rec : _records) {
191 len += FIXED_LENGTH + ((rec._name.length() + 1) * 2) + TRAILER_LENGTH;
192 }
193 if(!_records.isEmpty()) {
194 len -= TRAILER_LENGTH;
195 }
196
197 ByteBuffer bb = PageChannel.createBuffer(len);
198 bb.putInt(MAGIC);
199 bb.putInt((_reserved != null) ? _reserved : 0);
200 for(Iterator<RecordImpl> iter = _records.iterator(); iter.hasNext(); ) {
201 RecordImpl rec = iter.next();
202 bb.put(rec._guid);
203 bb.putInt(rec._type);
204 bb.put(rec._payload);
205 for(int i = 0; i < rec._name.length(); ++i) {
206 bb.putChar(rec._name.charAt(i));
207 }
208 bb.putChar((char)0);
209 if(iter.hasNext()) {
210 bb.putInt(0);
211 }
212 }
213 return bb.array();
214 }
215
216
217
218
219
220 static String getGuid(PropertyMap props) {
221 Object guid = props.getValue(PropertyMap.GUID_PROP);
222 return ((guid instanceof String) ? (String)guid : null);
223 }
224
225
226
227
228 static String toGuidKey(String guid) {
229 return StringUtil.toUpperCase(guid);
230 }
231
232
233
234
235
236 static byte[] toGuidBytes(String guid) {
237 ByteBuffer guidBuf = PageChannel.createBuffer(GUID_LENGTH);
238 if(!ColumnImpl.writeGUIDValue(guidBuf, guid)) {
239 throw new IllegalArgumentException("Invalid GUID: " + guid);
240 }
241 return guidBuf.array();
242 }
243
244 @Override
245 public String toString() {
246 return ToStringBuilder.builder(this)
247 .append(null, _records)
248 .toString();
249 }
250
251 final class RecordImpl implements Record
252 {
253 final int _type;
254 final byte[] _guid;
255 final byte[] _payload;
256 String _name;
257
258 RecordImpl(int type, byte[] guid, byte[] payload, String name) {
259 _type = type;
260 _guid = guid;
261 _payload = payload;
262 _name = name;
263 }
264
265 @Override
266 public RecordType getType() {
267 switch(_type) {
268 case TABLE_TYPE:
269 return RecordType.TABLE;
270 case REFERENCE_TYPE:
271 return RecordType.REFERENCE;
272 case COLUMN_TYPE:
273 return RecordType.COLUMN;
274 case END_TYPE:
275 return RecordType.END;
276 default:
277 return RecordType.UNKNOWN;
278 }
279 }
280
281 @Override
282 public String getGuid() {
283 return toGuid(_guid);
284 }
285
286 @Override
287 public String getParentGuid() {
288 return (((_type == COLUMN_TYPE) || (_type == REFERENCE_TYPE)) ?
289 toGuid(_payload) : null);
290 }
291
292 @Override
293 public String getName() {
294 return _name;
295 }
296
297 @Override
298 public void setName(String name) {
299 if(!isWritable()) {
300 throw new UnsupportedOperationException(
301 "The name map of a query cannot be changed");
302 }
303 _name = Objects.requireNonNull(name, "name");
304 }
305
306 @Override
307 public LocalDateTime getDate() {
308 return ((_type == TABLE_TYPE) ?
309 ColumnImpl.ldtFromLocalDateDouble(
310 PageChannel.wrap(_payload).getDouble()) :
311 null);
312 }
313
314 private String toGuid(byte[] bytes) {
315 return (isZero(bytes) ? null :
316 ColumnImpl.readGUIDValue(PageChannel.wrap(bytes),
317 PageChannel.DEFAULT_BYTE_ORDER));
318 }
319
320 private boolean isZero(byte[] bytes) {
321 return Arrays.equals(bytes, new byte[bytes.length]);
322 }
323
324 @Override
325 public String toString() {
326 return ToStringBuilder.valueBuilder(this)
327 .append("type", getType())
328 .appendIfNotNull("guid", getGuid())
329 .appendIfNotNull("parentGuid", getParentGuid())
330 .append("name", _name)
331 .toString();
332 }
333 }
334
335 static final class MismatchImpl implements Mismatch
336 {
337 private final MismatchType _type;
338 private final Table _table;
339 private final Query _query;
340 private final Column _column;
341 private final Record _record;
342
343 MismatchImpl(MismatchType type, Table table, Column column,
344 Record record) {
345 this(type, table, null, column, record);
346 }
347
348 MismatchImpl(MismatchType type, Query query, Record record) {
349 this(type, null, query, null, record);
350 }
351
352 private MismatchImpl(MismatchType type, Table table, Query query,
353 Column column, Record record) {
354 _type = type;
355 _table = table;
356 _query = query;
357 _column = column;
358 _record = record;
359 }
360
361 @Override
362 public MismatchType getType() {
363 return _type;
364 }
365
366 @Override
367 public Table getTable() {
368 return _table;
369 }
370
371 @Override
372 public Query getQuery() {
373 return _query;
374 }
375
376 @Override
377 public Column getColumn() {
378 return _column;
379 }
380
381 @Override
382 public Record getRecord() {
383 return _record;
384 }
385
386 @Override
387 public String toString() {
388 return ToStringBuilder.valueBuilder(this)
389 .append("type", _type)
390 .appendIfNotNull("table", ((_table != null) ?
391 _table.getName() : null))
392 .appendIfNotNull("query", ((_query != null) ?
393 _query.getName() : null))
394 .appendIfNotNull("column", ((_column != null) ?
395 _column.getName() : null))
396 .appendIfNotNull("record", _record)
397 .toString();
398 }
399 }
400 }