001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.schematic.internal.document;
017
018import static org.junit.Assert.assertEquals;
019import static org.junit.Assert.assertNotNull;
020import static org.junit.Assert.fail;
021import java.io.ByteArrayInputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Date;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.UUID;
035import java.util.concurrent.Callable;
036import java.util.concurrent.ExecutorService;
037import java.util.concurrent.Executors;
038import java.util.concurrent.Future;
039import java.util.concurrent.TimeUnit;
040import java.util.regex.Pattern;
041import org.bson.BSONObject;
042import org.bson.BasicBSONCallback;
043import org.bson.BasicBSONDecoder;
044import org.bson.BasicBSONEncoder;
045import org.bson.BasicBSONObject;
046import org.bson.types.BSONTimestamp;
047import org.bson.types.BasicBSONList;
048import org.codehaus.jackson.JsonToken;
049import org.junit.After;
050import org.junit.Assert;
051import org.junit.Before;
052import org.junit.Test;
053import org.modeshape.schematic.document.Binary;
054import org.modeshape.schematic.document.Code;
055import org.modeshape.schematic.document.CodeWithScope;
056import org.modeshape.schematic.document.Document;
057import org.modeshape.schematic.document.Json;
058import org.modeshape.schematic.document.MaxKey;
059import org.modeshape.schematic.document.MinKey;
060import org.modeshape.schematic.document.ObjectId;
061import org.modeshape.schematic.document.Symbol;
062import org.modeshape.schematic.document.Timestamp;
063import org.modeshape.schematic.internal.annotation.FixFor;
064import org.modeshape.schematic.internal.io.BufferCache;
065
066public class BsonReadingAndWritingTest {
067
068    protected BsonReader reader;
069    protected BsonWriter writer;
070    protected Document input;
071    protected Document output;
072    protected boolean print;
073
074    @Before
075    public void beforeTest() {
076        reader = new BsonReader();
077        writer = new BsonWriter();
078        print = false;
079    }
080
081    @After
082    public void afterTest() {
083        reader = null;
084        writer = null;
085    }
086
087    @Test
088    public void shouldReadExampleBsonStream() throws IOException {
089        // "\x16\x00\x00\x00\x02hello\x00\x06\x00\x00\x00world\x00\x00"
090        byte[] bytes = new byte[] {0x16, 0x00, 0x00, 0x00, 0x02, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x06, 0x00, 0x00, 0x00,
091            0x77, 0x6f, 0x72, 0x6c, 0x64, 0x00, 0x00};
092        output = reader.read(new ByteArrayInputStream(bytes));
093        String json = Json.write(output);
094        String expected = "{ \"hello\" : \"world\" }";
095        if (print) {
096            System.out.println(json);
097            System.out.flush();
098        }
099        assertEquals(expected, json);
100    }
101
102    @Test
103    public void shouldRoundTripSimpleBsonObjectWithStringValue() {
104        input = new BasicDocument("name", "Joe");
105        assertRoundtrip(input);
106    }
107
108    @Test
109    public void shouldRoundTripSimpleBsonObjectWithBooleanValue() {
110        input = new BasicDocument("foo", 3L);
111        assertRoundtrip(input);
112    }
113
114    @Test
115    public void shouldRoundTripSimpleBsonObjectWithIntValue() {
116        input = new BasicDocument("foo", 3);
117        assertRoundtrip(input);
118    }
119
120    @Test
121    public void shouldRoundTripSimpleBsonObjectWithLongValue() {
122        input = new BasicDocument("foo", 3L);
123        assertRoundtrip(input);
124    }
125
126    @Test
127    public void shouldRoundTripSimpleBsonObjectWithFloatValue() {
128        input = new BasicDocument("foo", 3.0f);
129        assertRoundtrip(input);
130    }
131
132    @Test
133    public void shouldRoundTripSimpleBsonObjectWithDoubleValue() {
134        input = new BasicDocument("foo", 3.0d);
135        assertRoundtrip(input);
136    }
137
138    @Test
139    public void shouldRoundTripSimpleBsonObjectWithDateValue() {
140        input = new BasicDocument("foo", new Date());
141        assertRoundtrip(input);
142    }
143
144    @Test
145    public void shouldRoundTripSimpleBsonObjectWithTimestampValue() {
146        input = new BasicDocument("foo", new Timestamp(new Date()));
147        assertRoundtrip(input);
148    }
149
150    @Test
151    public void shouldRoundTripSimpleBsonObjectWithObjectId() {
152        // print = true;
153        int time = Math.abs((int) new Date().getTime());
154        if (print) System.out.println("time value: " + time);
155        input = new BasicDocument("foo", new ObjectId(time, 1, 2, 3));
156        assertRoundtrip(input);
157    }
158
159    @Test
160    public void shouldRoundTripSimpleBsonObjectWithCode() {
161        input = new BasicDocument("foo", new Code("bar"));
162        assertRoundtrip(input);
163    }
164
165    @Test
166    public void shouldRoundTripSimpleBsonObjectWithCodeWithScope() {
167        Document scope = new BasicDocument("baz", "bam", "bak", "bat");
168        input = new BasicDocument("foo", new CodeWithScope("bar", scope));
169        assertRoundtrip(input);
170    }
171
172    @Test
173    public void shouldRoundTripSimpleBsonObjectWithMaxKey() {
174        input = new BasicDocument("foo", MaxKey.getInstance());
175        assertRoundtrip(input, false);
176    }
177
178    @Test
179    public void shouldRoundTripSimpleBsonObjectWithMinKey() {
180        input = new BasicDocument("foo", MinKey.getInstance());
181        assertRoundtrip(input, false);
182    }
183
184    @Test
185    public void shouldRoundTripSimpleBsonObjectWithSymbol() {
186        input = new BasicDocument("foo", new Symbol("bar"));
187        assertRoundtrip(input);
188    }
189
190    @Test
191    public void shouldRoundTripSimpleBsonObjectWithNull() {
192        input = new BasicDocument("foo", null);
193        assertRoundtrip(input);
194    }
195
196    @Test
197    public void shouldRoundTripSimpleBsonObjectWithBinary1() {
198        byte[] data = new byte[] {0x16, 0x00, 0x00, 0x00, 0x02, 0x68, 0x65, 0x6c};
199        input = new BasicDocument("foo", new Binary(data));
200        assertRoundtrip(input);
201    }
202
203    @Test
204    //Fix-For MODE-1575
205    public void shouldRoundTripSimpleBsonObjectWithBinary2() throws Exception {
206        ByteArrayOutputStream bos = new ByteArrayOutputStream();
207        InputStream is = getClass().getClassLoader().getResourceAsStream("binary");
208        assertNotNull(is);
209        try {
210            byte[] buff = new byte[1024];
211            int read;
212            while ((read = is.read(buff)) != -1) {
213                bos.write(buff, 0, read);
214            }
215        } finally {
216            bos.close();
217            is.close();
218        }
219
220        input = new BasicDocument("foo", new Binary(bos.toByteArray()));
221        assertRoundtrip(input);
222    }
223
224    @Test
225    public void shouldRoundTripSimpleBsonObjectWithUuid() {
226        input = new BasicDocument("foo", UUID.randomUUID());
227        assertRoundtrip(input);
228    }
229
230    @Test
231    public void shouldRoundTripSimpleBsonObjectWithPattern() {
232        // print = true;
233        input = new BasicDocument("foo", Pattern.compile("[CH]at\\s+"));
234        assertRoundtrip(input);
235    }
236
237    @Test
238    public void shouldRoundTripSimpleBsonObjectWithPatternAndFlags() {
239        // print = true;
240        input = new BasicDocument("foo", Pattern.compile("[CH]at\\s+", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE));
241        assertRoundtrip(input);
242    }
243
244    @Test
245    public void shouldRoundTripSimpleBsonObjectWithArray() {
246        BasicArray array = new BasicArray();
247        array.addValue("value1");
248        array.addValue(new Symbol("value2"));
249        array.addValue(30);
250        array.addValue(40L);
251        array.addValue(4.33d);
252        array.addValue(false);
253        array.addValue(null);
254        array.addValue("value2");
255        input = new BasicDocument("foo", array);
256        assertRoundtrip(input);
257    }
258
259    @Test
260    public void shouldRoundTripBsonObjectWithTwoFields() {
261        input = new BasicDocument("name", "Joe", "age", 35);
262        assertRoundtrip(input);
263    }
264
265    @Test
266    public void shouldRoundTripBsonObjectWithThreeFields() {
267        input = new BasicDocument("name", "Joe", "age", 35, "nick", "joey");
268        assertRoundtrip(input);
269    }
270
271    @Test
272    public void shouldRoundTripBsonObjectWithNestedDocument() {
273        BasicDocument address = new BasicDocument("street", "100 Main", "city", "Springfield", "zip", 12345);
274        input = new BasicDocument("name", "Joe", "age", 35, "address", address, "nick", "joey");
275        assertRoundtrip(input);
276    }
277
278    @Test
279    public void shouldRoundTripLargeModeShapeDocument() throws Exception {
280        Document doc = Json.read(resource("json/sample-large-modeshape-doc.json"));
281        // OutputStream os = new FileOutputStream("src/test/resources/json/sample-large-modeshape-doc2.json");
282        // Json.writePretty(doc, os);
283        // os.flush();
284        // os.close();
285        assertRoundtrip(doc);
286    }
287
288    @Test
289    @FixFor( "MODE-2074" )
290    public void shouldRoundTripBsonWithLargeStringField() throws Exception {
291        //use a string which overflows the default buffer BufferCache.MINIMUM_SIZE
292        final String largeString = readFile("json/sample-large-modeshape-doc3.json");
293        Document document = new BasicDocument("largeString", largeString);
294        assertRoundtrip(document);
295    }
296
297    @Test
298    @FixFor( "MODE-2074" )
299    public void shouldRoundTripBsonWithLargeStringFieldFromMultipleThreads() throws Exception {
300        final String largeString = readFile("json/sample-large-modeshape-doc3.json");
301        int threadCount = 10;
302        List<Future<Void>> results = new ArrayList<Future<Void>>();
303        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
304        for (int i = 0; i < threadCount; i++) {
305            results.add(executorService.submit(new Callable<Void>() {
306                @Override
307                public Void call() throws Exception {
308                    Document document = new BasicDocument("largeString", largeString);
309                    assertRoundtrip(document);
310                    return null;
311                }
312            }));
313        }
314
315        for (Future<Void> result : results) {
316            result.get(1, TimeUnit.SECONDS);
317        }
318    }
319    
320    @Test
321    @FixFor( "MODE-2430" )
322    public void shouldRoundTripLargeStringsSuccessively() throws Exception {
323        int limit = 1048 * 8; // the default buffer size
324        int iterations = 20;
325        char letter = 'a';
326        for (int i = 0; i < iterations; i++) {
327            int size = limit + i;
328            char[] chars = new char[size];
329            Arrays.fill(chars, letter);
330            letter = (char)((byte) letter + 1);
331            String str = new String(chars);
332            Document document = new BasicDocument("largeString", str);
333            assertRoundtrip(document, false);
334        }
335    }
336
337    @Test
338    @FixFor( "MODE-2615" )
339    public void shouldRoundTripDocumentWithMultiByteUTF8Chars() throws Exception{
340        char[] chars = new char[BufferCache.MINIMUM_SIZE];
341        Arrays.fill(chars, 'a');
342        chars[BufferCache.MINIMUM_SIZE - 1] = '\u00A3'; // 2 bytes UTF-8
343        Document document = new BasicDocument("string", new String(chars));
344        assertRoundtrip(document);
345
346        chars[BufferCache.MINIMUM_SIZE - 1] = '\uFFFF'; // 3 bytes UTF-8
347        document = new BasicDocument("string", new String(chars));
348        assertRoundtrip(document);
349
350        chars[BufferCache.MINIMUM_SIZE - 1] = '\u00A3'; // 2 bytes UTF-8
351        chars[BufferCache.MINIMUM_SIZE - 2] = '\uFFFF'; // 3 bytes UTF-8
352        document = new BasicDocument("string", new String(chars));
353        assertRoundtrip(document);
354
355        chars[BufferCache.MINIMUM_SIZE - 1] = '\uFFFF'; // 3 bytes UTF-8
356        chars[BufferCache.MINIMUM_SIZE - 2] = '\u00A3'; // 2 bytes UTF-8
357        document = new BasicDocument("string", new String(chars));
358        assertRoundtrip(document);
359    }
360
361    protected String readFile(String filePath) throws IOException {
362        InputStreamReader reader = new InputStreamReader(resource(filePath));
363        StringBuilder stringBuilder = new StringBuilder();
364        boolean error = false;
365        try {
366            int numRead = 0;
367            char[] buffer = new char[1024];
368            while ((numRead = reader.read(buffer)) > -1) {
369                stringBuilder.append(buffer, 0, numRead);
370            }
371        } catch (IOException e) {
372            error = true; // this error should be thrown, even if there is an error closing reader
373            throw e;
374        } catch (RuntimeException e) {
375            error = true; // this error should be thrown, even if there is an error closing reader
376            throw e;
377        } finally {
378            try {
379                reader.close();
380            } catch (IOException e) {
381                if (!error) throw e;
382            }
383        }
384        return stringBuilder.toString();
385    }
386
387    protected void assertRoundtrip( Document input ) {
388        assertRoundtrip(input, true);
389    }
390
391    protected void assertRoundtrip( Document input,
392                                    boolean compareToOtherImpls ) {
393        assertNotNull(input);
394        Document output = writeThenRead(input, compareToOtherImpls);
395        if (print) {
396            System.out.println("********************************************************************************");
397            System.out.println("INPUT :  " + input);
398            System.out.println();
399            System.out.println("OUTPUT:  " + output);
400            System.out.println("********************************************************************************");
401            System.out.flush();
402        }
403        Assert.assertEquals("Round trip failed", input, output);
404    }
405
406    protected Document writeThenRead( Document object,
407                                      boolean compareToOtherImpls ) {
408        try {
409            long start = System.nanoTime();
410            byte[] bytes = writer.write(object);
411            long writeTime = System.nanoTime() - start;
412
413            start = System.nanoTime();
414            Document result = reader.read(new ByteArrayInputStream(bytes));
415            long readTime = System.nanoTime() - start;
416
417            if (compareToOtherImpls) {
418                // Convert to MongoDB, write to bytes, and compare ...
419                BSONObject mongoData = createMongoData(object);
420                start = System.nanoTime();
421                byte[] mongoBytes = new BasicBSONEncoder().encode(mongoData);
422                long mongoWriteTime = System.nanoTime() - start;
423                assertSame(bytes, mongoBytes, "BSON   ", "Mongo  ");
424
425                // FYI: The Jackson BSON library writes several of the types incorrectly,
426                // whereas the MongoDB library seems to write things per the spec.
427
428                // // Convert to Jackson BSON, write to bytes, and compare ...
429                // ByteArrayOutputStream stream2 = new ByteArrayOutputStream();
430                // ObjectMapper om = new ObjectMapper(new BsonFactory());
431                // Map<String, Object> jacksonData = createJacksonData(object);
432                // om.writeValue(stream2, jacksonData);
433                // byte[] jacksonBytes = stream2.toByteArray();
434                // assertSame(bytes, jacksonBytes, "BSON   ", "Jackson");
435
436                start = System.nanoTime();
437                new BasicBSONDecoder().decode(bytes, new BasicBSONCallback());
438                long mongoReadTime = System.nanoTime() - start;
439
440                Document fromMongo = reader.read(new ByteArrayInputStream(mongoBytes));
441                if (!fromMongo.equals(result)) {
442                    System.out.println("from Schematic: " + result);
443                    System.out.println("from Mongo:     " + fromMongo);
444                    fail("Document read from bytes written by Mongo did not match expected document: " + result);
445                }
446
447                if (print) {
448                    System.out.println("Reading with Schematic:  " + percent(readTime, mongoReadTime) + " than Mongo");
449                    System.out.println("Writing with Schematic:  " + percent(writeTime, mongoWriteTime) + " than Mongo");
450                }
451            }
452
453            return result;
454        } catch (IOException e) {
455            throw new AssertionError(e);
456        }
457    }
458
459    protected String time( long nanos ) {
460        return "" + TimeUnit.NANOSECONDS.convert(nanos, TimeUnit.NANOSECONDS) + "ns";
461    }
462
463    protected String percent( long nanos1,
464                              long nanos2 ) {
465        float percent = 100.0f * (float)(((double)nanos2 - (double)nanos1) / nanos1);
466        if (percent < 0.0d) {
467            return "" + -percent + "% slower";
468        }
469        return "" + percent + "% faster";
470    }
471
472    protected BSONObject createMongoData( Document document ) {
473        BSONObject obj = new BasicBSONObject();
474        for (Document.Field field : document.fields()) {
475            Object value = field.getValue();
476            obj.put(field.getName(), createMongoData(value));
477        }
478        return obj;
479    }
480
481    protected Object createMongoData( Object value ) {
482        if (value instanceof MinKey) {
483            value = "MinKey";
484        } else if (value instanceof MaxKey) {
485            value = "MaxKey";
486        } else if (value instanceof Symbol) {
487            Symbol symbol = (Symbol)value;
488            value = new org.bson.types.Symbol(symbol.getSymbol());
489        } else if (value instanceof ObjectId) {
490            ObjectId id = (ObjectId)value;
491            value = new org.bson.types.ObjectId(id.getBytes());
492        } else if (value instanceof Timestamp) {
493            Timestamp ts = (Timestamp)value;
494            value = new BSONTimestamp(ts.getTime(), ts.getInc());
495        } else if (value instanceof CodeWithScope) {
496            CodeWithScope code = (CodeWithScope)value;
497            value = new org.bson.types.CodeWScope(code.getCode(), createMongoData(code.getScope()));
498        } else if (value instanceof Code) {
499            Code code = (Code)value;
500            value = new org.bson.types.Code(code.getCode());
501        } else if (value instanceof Binary) {
502            Binary binary = (Binary)value;
503            value = new org.bson.types.Binary(binary.getBytes());
504        } else if (value instanceof List) {
505            List<?> values = (List<?>)value;
506            BasicBSONList newValues = new BasicBSONList();
507            for (Object v : values) {
508                newValues.add(createMongoData(v));
509            }
510            value = newValues;
511        } else if (value instanceof Document) {
512            value = createMongoData((Document)value);
513        }
514        return value;
515    }
516
517    protected Map<String, Object> createJacksonData( Document document ) {
518        Map<String, Object> data = new LinkedHashMap<String, Object>();
519        for (Document.Field field : document.fields()) {
520            Object value = field.getValue();
521            data.put(field.getName(), createJacksonData(value));
522        }
523        return data;
524    }
525
526    protected Object createJacksonData( Object value ) {
527        if (value instanceof MinKey) {
528            value = JsonToken.VALUE_STRING;
529        } else if (value instanceof MaxKey) {
530            value = JsonToken.VALUE_STRING;
531        } else if (value instanceof Symbol) {
532            value = new de.undercouch.bson4jackson.types.Symbol(((Symbol)value).getSymbol());
533        } else if (value instanceof ObjectId) {
534            ObjectId id = (ObjectId)value;
535            value = new de.undercouch.bson4jackson.types.ObjectId(id.getTime(), id.getMachine(), id.getInc());
536        } else if (value instanceof Timestamp) {
537            Timestamp ts = (Timestamp)value;
538            value = new de.undercouch.bson4jackson.types.Timestamp(ts.getTime(), ts.getInc());
539        } else if (value instanceof CodeWithScope) {
540            CodeWithScope code = (CodeWithScope)value;
541            value = new de.undercouch.bson4jackson.types.JavaScript(code.getCode(), createJacksonData(code.getScope()));
542        } else if (value instanceof Code) {
543            Code code = (Code)value;
544            value = new de.undercouch.bson4jackson.types.JavaScript(code.getCode(), null);
545        } else if (value instanceof List) {
546            List<?> values = (List<?>)value;
547            List<Object> newValues = new ArrayList<Object>(values.size());
548            for (Object v : values) {
549                newValues.add(createJacksonData(v));
550            }
551            value = newValues;
552        } else if (value instanceof Document) {
553            value = createJacksonData((Document)value);
554        }
555        return value;
556    }
557
558    protected void assertSame( byte[] b1,
559                               byte[] b2,
560                               String name1,
561                               String name2 ) {
562        if (b1.equals(b2)) return;
563        int s1 = b1.length;
564        int s2 = b2.length;
565        String sb1 = toString(b1);
566        String sb2 = toString(b2);
567        if (!sb1.equals(sb2)) {
568            System.out.println(name1 + " size: " + padLeft(s1, 3) + " content: " + sb1);
569            System.out.println(name2 + " size: " + padLeft(s2, 3) + " content: " + sb2);
570            fail();
571        }
572    }
573
574    protected String padLeft( Object value,
575                              int width ) {
576        String result = value != null ? value.toString() : "null";
577        while (result.length() < width) {
578            result = " " + result;
579        }
580        return result;
581    }
582
583    protected String toString( byte[] bytes ) {
584        StringBuilder sb = new StringBuilder();
585        for (byte b : bytes) {
586            sb.append(padLeft((int)b, 4)).append(' ');
587        }
588        return sb.toString();
589    }
590
591    protected boolean delete( File fileOrDirectory ) {
592        if (fileOrDirectory == null) {
593            return false;
594        }
595        if (!fileOrDirectory.exists()) {
596            return false;
597        }
598
599        // The file/directory exists, so if a directory delete all of the contents ...
600        if (fileOrDirectory.isDirectory()) {
601            for (File childFile : fileOrDirectory.listFiles()) {
602                delete(childFile); // recursive call (good enough for now until we need something better)
603            }
604            // Now an empty directory ...
605        }
606        // Whether this is a file or empty directory, just delete it ...
607        return fileOrDirectory.delete();
608    }
609
610    protected InputStream resource( String resourcePath ) {
611        InputStream stream = BsonReadingAndWritingTest.class.getClassLoader().getResourceAsStream(resourcePath);
612        if (stream == null) {
613            File file = new File(resourcePath);
614            if (!file.exists()) {
615                file = new File("src/test/resources" + resourcePath);
616            }
617            if (!file.exists()) {
618                file = new File("src/test/resources/" + resourcePath);
619            }
620            if (file.exists()) {
621                try {
622                    stream = new FileInputStream(file);
623                } catch (IOException e) {
624                    throw new AssertionError("Failed to open stream to \"" + file.getAbsolutePath() + "\"");
625                }
626            }
627        }
628        assert stream != null : "Resource at \"" + resourcePath + "\" could not be found";
629        return stream;
630    }
631
632}