/* * Copyright 2015 GoDataDriven B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.divolte.server.mincode; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.CharStreams; import com.google.common.io.Resources; import org.hamcrest.Matchers; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import javax.annotation.ParametersAreNonnullByDefault; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.math.BigInteger; import java.util.Base64; import java.util.Random; import static org.junit.Assert.*; @ParametersAreNonnullByDefault public class MincodeParserTest { private static final ObjectMapper MAPPER = new ObjectMapper(new MincodeFactory()); private static JsonParser createParser(final String s) throws IOException { return MAPPER.getFactory().createParser(s); } @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void testBinaryDecoding() throws IOException { // Fixture. final byte[] binaryData = Resources.toByteArray(Resources.getResource("transparent1x1.gif")); final String mincode = 's' + Base64.getEncoder().encodeToString(binaryData) + '!'; // Test itself. assertArrayEquals(binaryData, MAPPER.readValue(mincode, byte[].class)); } // Size of the internal buffer that Jackson parsers use. private static final int JACKSON_BUFFER_SIZE = 4000; @Test public void testLargeStringMincodeDecoding() throws IOException { // Build a fixture by repeating a record multiple times. // (The template will be the same length as records.) final String propertyNameTemplate = "property%04X"; final String propertyValueTemplate = "still more %04X values"; final String recordTemplate = 's' + propertyNameTemplate + '!' + propertyValueTemplate + '!'; // Check that the record length and buffer size are relatively prime; this // ensures the record will be read at all possible offsets relative to the // internal buffer. assertEquals("Text size should be relatively prime to the buffer size.", BigInteger.ONE, BigInteger.valueOf(recordTemplate.length()).gcd(BigInteger.valueOf(JACKSON_BUFFER_SIZE))); final StringBuilder sb = new StringBuilder(2 + JACKSON_BUFFER_SIZE * recordTemplate.length()); sb.append('('); for (int i = 0; i < JACKSON_BUFFER_SIZE; ++i) { sb.append(String.format(recordTemplate, i, i)); } sb.append(')'); // Time for the test itself. final JsonParser parser = createParser(sb.toString()); // Before starting, everything should be clear. assertNull(parser.getCurrentToken()); assertJsonParserTextUnavailable(parser); // First token is an object. assertEquals(JsonToken.START_OBJECT, parser.nextToken()); assertJsonParserText(parser, "{"); // Now we're going to loop over the strings. for (int i = 0; i < JACKSON_BUFFER_SIZE; ++i) { final String expectedPropertyName = String.format(propertyNameTemplate, i); final String expectedPropertyValue = String.format(propertyValueTemplate, i); // First we test the property name. assertEquals(JsonToken.FIELD_NAME, parser.nextToken()); assertEquals(expectedPropertyName, parser.getCurrentName()); // Field names are always available via text. assertJsonParserText(parser, expectedPropertyName); // Next test the property value. assertEquals(JsonToken.VALUE_STRING, parser.nextToken()); assertEquals(expectedPropertyValue, parser.getValueAsString()); assertJsonParserText(parser, expectedPropertyValue); assertTrue(parser.hasTextCharacters()); // While looking at the value, the name is still available. assertEquals(expectedPropertyName, parser.getCurrentName()); } // Next we expect the end of the array. assertEquals(JsonToken.END_OBJECT, parser.nextToken()); assertJsonParserText(parser, "}"); // Reading beyond the end yields no more tokens... assertNull(parser.nextToken()); assertJsonParserTextUnavailable(parser); // ... even if we keep trying. assertNull(parser.nextToken()); assertJsonParserTextUnavailable(parser); } private static void assertJsonParserText(final JsonParser parser, final String expectedText) throws IOException { assertEquals(expectedText, parser.getText()); final char[] buffer = parser.getTextCharacters(); assertNotNull(buffer); assertEquals(expectedText, new String(buffer, parser.getTextOffset(), parser.getTextLength())); } private static void assertJsonParserTextUnavailable(JsonParser parser) throws IOException { assertNull(parser.getText()); assertNull(parser.getTextCharacters()); assertEquals(0, parser.getTextOffset()); assertEquals(0, parser.getTextLength()); } // None of these need to be escaped. private static char[] SAFE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); private static String createLargeStringValue() { final int length = 3 * JACKSON_BUFFER_SIZE + 1; final StringBuilder sb = new StringBuilder(length); final Random random = new Random(0); for (int i = 0; i < length; ++i) { sb.append(SAFE_CHARS[random.nextInt(SAFE_CHARS.length)]); } return sb.toString(); } @Test public void testLongLargeStringValueDecoding() throws IOException { // Build a very large string value to decode. final String value = createLargeStringValue(); final String record = 's' + value + '!'; // Using a reader ensures the parser performs its own buffering instead // of just scanning directly from the string. assertEquals(value, MAPPER.readValue(new StringReader(record), String.class)); } @Test public void testFirstRecordCannotBeEndOfObject() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Unexpected end of object"); createParser(")").nextToken(); } @Test public void testFirstRecordCannotBeEndOfArray() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Unexpected end of array"); createParser(".").nextToken(); } @Test public void testInvalidRecordType() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Unknown record type"); createParser("z").nextToken(); } @Test public void testUnterminatedStringRecord() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Unexpected end-of-input: was expecting end of string value"); createParser("sThis record isn't terminated").nextToken(); } @Test public void testUnterminatedEscapeSequenceInStringRecord() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Unexpected end-of-input in character escape sequence"); createParser("sThis record ends with an incomplete escape sequence: ~").nextToken(); } @Test public void testInvalidIntegerRecord() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Invalid integer record"); expectedException.expectCause(Matchers.isA(NumberFormatException.class)); createParser("d54@@!").nextToken(); } @Test public void testInvalidFloatingPointRecord() throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage("Invalid number record"); expectedException.expectCause(Matchers.isA(NumberFormatException.class)); createParser("j54@@!").nextToken(); } @Test public void testFloatingPointIntegerRecord() throws IOException { // Floating point records can also supply integers. In this case // we should supply the correct token type. final JsonParser parser = createParser("j1234!"); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(1234, parser.getValueAsInt()); } // Per JACKSON-804 the max for a byte is 255. private static final BigInteger MAX_BYTE = BigInteger.valueOf(255); private static final BigInteger MAX_SHORT = BigInteger.valueOf(Short.MAX_VALUE); private static final BigInteger MAX_INT = BigInteger.valueOf(Integer.MAX_VALUE); private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); private static <T extends Number> void assertInRange(final BigInteger value, final Class<T> type) throws IOException { final T parsedValue = MAPPER.readValue("j" + value + '!', type); assertEquals(value, MAPPER.convertValue(parsedValue, BigInteger.class)); } private static <T extends Number> void assertInRange(final BigInteger expectedValue, final BigInteger value, final Class<T> type) throws IOException { final T parsedValue = MAPPER.readValue("j" + value + '!', type); assertEquals(expectedValue, MAPPER.convertValue(parsedValue.longValue(), BigInteger.class)); } private <T extends Number> void assertOutOfRange(final String expectedMessageSubstring, final BigInteger maxValue, final Class<T> type) throws IOException { expectedException.expect(JsonParseException.class); expectedException.expectMessage(expectedMessageSubstring); MAPPER.readValue("j" + maxValue.add(BigInteger.ONE) + '!', type); } @Test public void testIntegerValueByteInRange() throws IOException { assertInRange(BigInteger.valueOf(-1), MAX_BYTE, Byte.class); } @Test public void testIntegerValueByteOutOfRange() throws IOException { assertOutOfRange("out of range of Java byte", MAX_BYTE, Byte.class); } @Test public void testIntegerValueShortInRange() throws IOException { assertInRange(MAX_SHORT, Short.class); } @Test public void testIntegerValueShortOutOfRange() throws IOException { assertOutOfRange("out of range of Java short", MAX_SHORT, Short.class); } @Test public void testIntegerValueIntegerInRange() throws IOException { assertInRange(MAX_INT, Integer.class); } @Test public void testIntegerValueIntegerOutOfRange() throws IOException { assertOutOfRange("out of range of int", MAX_INT, Integer.class); } @Test public void testIntegerValueLongInRange() throws IOException { assertInRange(MAX_LONG, Long.class); } @Test public void testIntegerValueLongOutOfRange() throws IOException { assertOutOfRange("out of range of long", MAX_LONG, Long.class); } @Test public void testIntegerValueBigIntegerInRange() throws IOException { assertInRange(MAX_BYTE, BigInteger.class); assertInRange(MAX_BYTE.add(BigInteger.ONE), BigInteger.class); assertInRange(MAX_SHORT, BigInteger.class); assertInRange(MAX_SHORT.add(BigInteger.ONE), BigInteger.class); assertInRange(MAX_INT, BigInteger.class); assertInRange(MAX_INT.add(BigInteger.ONE), BigInteger.class); assertInRange(MAX_LONG, BigInteger.class); assertInRange(MAX_LONG.add(BigInteger.ONE), BigInteger.class); } @Test public void testOnlyReadWhatIsRequired() throws IOException { final Reader reader = new StringReader("sA string record!Extra trailing data."); final JsonParser parser = MAPPER.getFactory().createParser(reader); assertEquals("A string record", MAPPER.readValue(parser, String.class)); // Check the trailing data can be retrieved. final StringWriter writer = new StringWriter(); parser.releaseBuffered(writer); assertTrue(0 < writer.getBuffer().length()); CharStreams.copy(reader, writer); // Check the trailer could be retrieved. assertEquals("Extra trailing data.", writer.toString()); } }