View Javadoc
1   /*
2   Copyright (c) 2015 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;
18  
19  import java.io.*;
20  import java.lang.reflect.Field;
21  import java.nio.ByteBuffer;
22  import java.nio.channels.FileChannel;
23  import java.nio.charset.Charset;
24  import java.nio.file.Files;
25  import java.nio.file.StandardCopyOption;
26  import java.time.Instant;
27  import java.time.LocalDateTime;
28  import java.time.ZoneId;
29  import java.util.*;
30  import java.util.stream.StreamSupport;
31  
32  import com.healthmarketscience.jackcess.Database.FileFormat;
33  import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
34  import com.healthmarketscience.jackcess.impl.*;
35  import com.healthmarketscience.jackcess.impl.JetFormatTest.TestDB;
36  import com.healthmarketscience.jackcess.util.MemFileChannel;
37  import static org.junit.jupiter.api.Assertions.*;
38  
39  /**
40   * Utility code for the test cases.
41   *
42   * @author James Ahlborn
43   */
44  @SuppressWarnings("deprecation")
45  public class TestUtil
46  {
47    public static final TimeZone TEST_TZ =
48      TimeZone.getTimeZone("America/New_York");
49  
50    private static final ThreadLocal<Boolean> _autoSync =
51      new ThreadLocal<Boolean>();
52  
53    private TestUtil() {}
54  
55    static void setTestAutoSync(boolean autoSync) {
56      _autoSync.set(autoSync);
57    }
58  
59    static void clearTestAutoSync() {
60      _autoSync.remove();
61    }
62  
63    static boolean getTestAutoSync() {
64      Boolean autoSync = _autoSync.get();
65      return ((autoSync != null) ? autoSync : Database.DEFAULT_AUTO_SYNC);
66    }
67  
68    public static Database open(FileFormat fileFormat, File file)
69      throws Exception
70    {
71      return open(fileFormat, file, false);
72    }
73  
74    public static Database open(FileFormat fileFormat, File file, boolean inMem)
75      throws Exception
76    {
77      return open(fileFormat, file, inMem, null);
78    }
79  
80    public static Database open(FileFormat fileFormat, File file, boolean inMem,
81                                Charset charset)
82      throws Exception
83    {
84      return openDB(fileFormat, file, inMem, charset, true);
85    }
86  
87    public static Database open(TestDB testDB) throws Exception {
88      return open(testDB.getExpectedFileFormat(), testDB.getFile(), false,
89                  testDB.getExpectedCharset());
90    }
91  
92    public static Database openMem(TestDB testDB) throws Exception {
93      return openDB(testDB.getExpectedFileFormat(), testDB.getFile(), true,
94                      testDB.getExpectedCharset(), false);
95    }
96  
97    public static Database create(FileFormat fileFormat) throws Exception {
98      return create(fileFormat, false);
99    }
100 
101   public static Database create(FileFormat fileFormat, boolean keep)
102     throws Exception
103   {
104     return create(fileFormat, keep, true);
105   }
106 
107   public static Database createMem(FileFormat fileFormat) throws Exception {
108     return create(fileFormat);
109   }
110 
111   public static Database createFile(FileFormat fileFormat) throws Exception {
112     return create(fileFormat, false, false);
113   }
114 
115   private static Database create(FileFormat fileFormat, boolean keep,
116                                  boolean inMem)
117     throws Exception
118   {
119 
120     FileChannel channel = ((inMem && !keep) ? MemFileChannel.newChannel() :
121                            null);
122 
123     if (fileFormat == FileFormat.GENERIC_JET4) {
124       // while we don't support creating GENERIC_JET4 as a jackcess feature,
125       // we do want to be able to test these types of dbs
126       try (InputStream inStream = TestUtil.class.getClassLoader()
127           .getResourceAsStream("emptyJet4.mdb")) {
128         File f = createTempFile(keep);
129         if (channel != null) {
130           JetFormatTest.transferDbFrom(channel, inStream);
131         } else {
132           try (OutputStream outStream = new FileOutputStream(f)) {
133             ByteUtil.copy(inStream, outStream);
134           }
135         }
136         return new DatabaseBuilder(f)
137           .setAutoSync(getTestAutoSync()).setChannel(channel).open();
138       }
139     }
140 
141     return new DatabaseBuilder(createTempFile(keep)).setFileFormat(fileFormat)
142         .setAutoSync(getTestAutoSync()).setChannel(channel).create();
143   }
144 
145 
146   public static Database openCopy(TestDB testDB) throws Exception {
147     return openCopy(testDB, false);
148   }
149 
150   public static Database openCopy(TestDB testDB, boolean keep)
151     throws Exception
152   {
153     return openCopy(testDB.getExpectedFileFormat(), testDB.getFile(), keep);
154   }
155 
156   public static Database openCopy(FileFormat fileFormat, File file)
157     throws Exception
158   {
159     return openCopy(fileFormat, file, false);
160   }
161 
162   public static Database openCopy(FileFormat fileFormat, File file,
163                                   boolean keep)
164     throws Exception
165   {
166     File tmp = createTempFile(keep);
167     copyFile(file, tmp);
168     return openDB(fileFormat, tmp, false, null, false);
169   }
170 
171   private static Database openDB(
172       FileFormat fileFormat, File file, boolean inMem, Charset charset,
173       boolean readOnly)
174     throws Exception
175   {
176     FileChannel channel = (inMem ? MemFileChannel.newChannel(
177                                file, MemFileChannel.RW_CHANNEL_MODE)
178                            : null);
179     final Database db = new DatabaseBuilder(file).setReadOnly(readOnly)
180       .setAutoSync(getTestAutoSync()).setChannel(channel)
181       .setCharset(charset).open();
182     if(fileFormat != null) {
183       assertEquals(
184           DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(),
185           ((DatabaseImpl)db).getFormat(),
186           "Wrong JetFormat.");
187       assertEquals(
188           fileFormat, db.getFileFormat(), "Wrong FileFormat.");
189     }
190     return db;
191   }
192 
193   static Object[] createTestRow(String col1Val) {
194     return new Object[] {col1Val, "R", "McCune", 1234, (byte) 0xad, 555.66d,
195         777.88f, (short) 999, new Date()};
196   }
197 
198   public static Object[] createTestRow() {
199     return createTestRow("Tim");
200   }
201 
202   static Map<String,Object> createTestRowMap(String col1Val) {
203     return createExpectedRow("A", col1Val, "B", "R", "C", "McCune",
204                              "D", 1234, "E", (byte) 0xad, "F", 555.66d,
205                              "G", 777.88f, "H", (short) 999, "I", new Date());
206   }
207 
208   public static void createTestTable(Database db) throws Exception {
209     new TableBuilder("test")
210       .addColumn(new ColumnBuilder("A", DataType.TEXT))
211       .addColumn(new ColumnBuilder("B", DataType.TEXT))
212       .addColumn(new ColumnBuilder("C", DataType.TEXT))
213       .addColumn(new ColumnBuilder("D", DataType.LONG))
214       .addColumn(new ColumnBuilder("E", DataType.BYTE))
215       .addColumn(new ColumnBuilder("F", DataType.DOUBLE))
216       .addColumn(new ColumnBuilder("G", DataType.FLOAT))
217       .addColumn(new ColumnBuilder("H", DataType.INT))
218       .addColumn(new ColumnBuilder("I", DataType.SHORT_DATE_TIME))
219       .toTable(db);
220   }
221 
222   public static String createString(int len) {
223     return createString(len, 'a');
224   }
225 
226   static String createNonAsciiString(int len) {
227     return createString(len, '\u0CC0');
228   }
229 
230   private static String createString(int len, char firstChar) {
231     StringBuilder builder = new StringBuilder(len);
232     for(int i = 0; i < len; ++i) {
233       builder.append((char)(firstChar + (i % 26)));
234     }
235     return builder.toString();
236   }
237 
238   static void assertRowCount(int expectedRowCount, Table table)
239     throws Exception
240   {
241     assertEquals(expectedRowCount, countRows(table));
242     assertEquals(expectedRowCount, table.getRowCount());
243   }
244 
245   public static int countRows(Table table) throws Exception {
246     Cursor cursor = CursorBuilder.createCursor(table);
247     return (int) StreamSupport.stream(cursor.spliterator(), false).count();
248   }
249 
250   public static void assertTable(
251       List<? extends Map<String, Object>> expectedTable,
252       Table table)
253     throws IOException
254   {
255     assertCursor(expectedTable, CursorBuilder.createCursor(table));
256   }
257 
258   public static void assertCursor(
259       List<? extends Map<String, Object>> expectedTable,
260       Cursor cursor)
261   {
262     List<Map<String, Object>> foundTable =
263       new ArrayList<Map<String, Object>>();
264     for(Map<String, Object> row : cursor) {
265       foundTable.add(row);
266     }
267     assertEquals(expectedTable.size(), foundTable.size());
268     for(int i = 0; i < expectedTable.size(); ++i) {
269       assertEquals(expectedTable.get(i), foundTable.get(i));
270     }
271   }
272 
273   public static RowImpl createExpectedRow(Object... rowElements) {
274     RowImpl row = new RowImpl((RowIdImpl)null);
275     for(int i = 0; i < rowElements.length; i += 2) {
276       row.put((String)rowElements[i],
277               rowElements[i + 1]);
278     }
279     return row;
280   }
281 
282   public static List<Row> createExpectedTable(Row... rows) {
283     return Arrays.<Row>asList(rows);
284   }
285 
286   public static void dumpDatabase(Database mdb) throws Exception {
287     dumpDatabase(mdb, false);
288   }
289 
290   public static void dumpDatabase(Database mdb, boolean systemTables)
291     throws Exception
292   {
293     dumpDatabase(mdb, systemTables, new PrintWriter(System.out, true));
294   }
295 
296   public static void dumpTable(Table table) throws Exception {
297     dumpTable(table, new PrintWriter(System.out, true));
298   }
299 
300   public static void dumpProperties(Table table) throws Exception {
301     System.out.println("TABLE_PROPS: " + table.getName() + ": " +
302                        table.getProperties());
303     for(Column c : table.getColumns()) {
304       System.out.println("COL_PROPS: " + c.getName() + ": " +
305                          c.getProperties());
306     }
307   }
308 
309   static void dumpDatabase(Database mdb, boolean systemTables,
310                            PrintWriter writer) throws Exception
311   {
312     writer.println("DATABASE:");
313     for(Table table : mdb) {
314       dumpTable(table, writer);
315     }
316     if(systemTables) {
317       for(String sysTableName : mdb.getSystemTableNames()) {
318         dumpTable(mdb.getSystemTable(sysTableName), writer);
319       }
320     }
321   }
322 
323   static void dumpTable(Table table, PrintWriter writer) throws Exception {
324     // make sure all indexes are read
325     for(Index index : table.getIndexes()) {
326       ((IndexImpl)index).initialize();
327     }
328 
329     writer.println("TABLE: " + table.getName());
330     List<String> colNames = new ArrayList<String>();
331     for(Column col : table.getColumns()) {
332       colNames.add(col.getName());
333     }
334     writer.println("COLUMNS: " + colNames);
335     for(Map<String, Object> row : CursorBuilder.createCursor(table)) {
336       writer.println(massageRow(row));
337     }
338   }
339 
340   private static Map<String,Object> massageRow(Map<String, Object> row)
341     throws IOException
342   {
343       for(Map.Entry<String, Object> entry : row.entrySet()) {
344         Object v = entry.getValue();
345         if(v instanceof byte[]) {
346           // make byte[] printable
347           byte[] bv = (byte[])v;
348           entry.setValue(ByteUtil.toHexString(ByteBuffer.wrap(bv), bv.length));
349         } else if(v instanceof ComplexValueForeignKey) {
350           // deref complex values
351           String str = "ComplexValue(" + v + ")" +
352             ((ComplexValueForeignKey)v).getValues();
353           entry.setValue(str);
354         }
355       }
356 
357       return row;
358   }
359 
360   static void dumpIndex(Index index) throws Exception {
361     dumpIndex(index, Integer.MAX_VALUE);
362   }
363 
364   static void dumpIndex(Index index, int limit) throws Exception {
365     dumpIndex(index, new PrintWriter(System.out, true), limit);
366   }
367 
368   static void dumpIndex(Index index, PrintWriter writer, int limit)
369     throws Exception {
370     writer.println("INDEX: " + index);
371     IndexData.EntryCursor ec = ((IndexImpl)index).cursor();
372     IndexData.Entry lastE = ec.getLastEntry();
373     IndexData.Entry e = null;
374     int count = 0;
375     while((e = ec.getNextEntry()) != lastE) {
376       writer.println(e);
377       if((count++) > limit) {
378         break;
379       }
380     }
381   }
382 
383   static void assertSameDate(Date expected, Date found)
384   {
385     if(expected == found) {
386       return;
387     }
388     if((expected == null) || (found == null)) {
389       throw new AssertionError("Expected " + expected + ", found " + found);
390     }
391     long expTime = expected.getTime();
392     long foundTime = found.getTime();
393     // there are some rounding issues due to dates being stored as doubles,
394     // but it results in a 1 millisecond difference, so i'm not going to worry
395     // about it
396     if((expTime != foundTime) && (Math.abs(expTime - foundTime) > 1)) {
397       throw new AssertionError("Expected " + expTime + " (" + expected +
398                                "), found " + foundTime + " (" + found + ")");
399     }
400   }
401 
402   static void assertSameDate(Date expected, LocalDateTime found)
403   {
404     if((expected == null) && (found == null)) {
405       return;
406     }
407     if((expected == null) || (found == null)) {
408       throw new AssertionError("Expected " + expected + ", found " + found);
409     }
410 
411     LocalDateTime expectedLdt = LocalDateTime.ofInstant(
412         Instant.ofEpochMilli(expected.getTime()),
413         ZoneId.systemDefault());
414 
415     assertEquals(expectedLdt, found);
416   }
417 
418   public static void copyFile(File srcFile, File dstFile)
419     throws IOException
420   {
421     Files.copy(srcFile.toPath(), dstFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
422   }
423 
424   static void copyStream(InputStream istream, OutputStream ostream)
425     throws IOException
426   {
427     // FIXME should really be using commons io FileUtils here, but don't want
428     // to add dep for one simple test method
429     byte[] buf = new byte[1024];
430     int numBytes = 0;
431     while((numBytes = istream.read(buf)) >= 0) {
432       ostream.write(buf, 0, numBytes);
433     }
434   }
435 
436   public static File createTempFile(boolean keep) throws Exception {
437     File tmp = File.createTempFile("databaseTest", ".mdb");
438     if(keep) {
439       System.out.println("Created " + tmp);
440     } else {
441       tmp.deleteOnExit();
442     }
443     return tmp;
444   }
445 
446   public static void clearTableCache(Database db) throws Exception
447   {
448     Field f = db.getClass().getDeclaredField("_tableCache");
449     f.setAccessible(true);
450     Object val = f.get(db);
451     f = val.getClass().getDeclaredField("_tables");
452     f.setAccessible(true);
453     val = f.get(val);
454     ((Map<?,?>)val).clear();
455   }
456 
457   public static byte[] toByteArray(File file)
458     throws IOException
459   {
460     return Files.readAllBytes(file.toPath());
461   }
462 
463   public static byte[] toByteArray(InputStream in, long length)
464     throws IOException
465   {
466     // FIXME should really be using commons io IOUtils here, but don't want
467     // to add dep for one simple test method
468     try (DataInputStream din = new DataInputStream(in)) {
469       byte[] bytes = new byte[(int)length];
470       din.readFully(bytes);
471       return bytes;
472     }
473   }
474 
475   static void checkTestDBTable1RowABCDEFG(final TestDB testDB, final Table table, final Row row)
476   {
477     assertEquals("abcdefg", row.get("A"), () -> "testDB: " + testDB + "; table: " + table);
478     assertEquals("hijklmnop", row.get("B"));
479     assertEquals(new Byte((byte) 2), row.get("C"));
480     assertEquals(new Short((short) 222), row.get("D"));
481     assertEquals(Integer.valueOf(333333333), row.get("E"));
482     assertEquals(Double.valueOf(444.555d), row.get("F"));
483     final Calendar cal = Calendar.getInstance();
484     cal.setTime(row.getDate("G"));
485     assertEquals(Calendar.SEPTEMBER, cal.get(Calendar.MONTH));
486     assertEquals(21, cal.get(Calendar.DAY_OF_MONTH));
487     assertEquals(1974, cal.get(Calendar.YEAR));
488     assertEquals(0, cal.get(Calendar.HOUR_OF_DAY));
489     assertEquals(0, cal.get(Calendar.MINUTE));
490     assertEquals(0, cal.get(Calendar.SECOND));
491     assertEquals(0, cal.get(Calendar.MILLISECOND));
492     assertEquals(Boolean.TRUE, row.get("I"));
493   }
494 
495   static void checkTestDBTable1RowA(final TestDB testDB, final Table table, final Row row)
496   {
497     assertEquals("a", row.get("A"), () -> "testDB: " + testDB + "; table: " + table);
498     assertEquals("b", row.get("B"));
499     assertEquals(new Byte((byte) 0), row.get("C"));
500     assertEquals(new Short((short) 0), row.get("D"));
501     assertEquals(Integer.valueOf(0), row.get("E"));
502     assertEquals(Double.valueOf(0d), row.get("F"));
503     final Calendar cal = Calendar.getInstance();
504     cal.setTime(row.getDate("G"));
505     assertEquals(Calendar.DECEMBER, cal.get(Calendar.MONTH));
506     assertEquals(12, cal.get(Calendar.DAY_OF_MONTH));
507     assertEquals(1981, cal.get(Calendar.YEAR));
508     assertEquals(0, cal.get(Calendar.HOUR_OF_DAY));
509     assertEquals(0, cal.get(Calendar.MINUTE));
510     assertEquals(0, cal.get(Calendar.SECOND));
511     assertEquals(0, cal.get(Calendar.MILLISECOND));
512     assertEquals(Boolean.FALSE, row.get("I"));
513   }
514 
515 }