All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bazaarvoice.emodb.common.json.JsonStreamingArrayParser Maven / Gradle / Ivy

package com.bazaarvoice.emodb.common.json;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.google.common.base.Throwables;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.PeekingIterator;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

import static com.google.common.base.Preconditions.checkState;

/**
 * Incrementally parses an JSON array of objects, returning the results as an iterator.  Allows parsing an
 * arbitrary amount of data, potentially more than could fit in memory at one time.
 */
public class JsonStreamingArrayParser extends AbstractIterator implements PeekingIterator, Closeable {
    private final JsonParser _jp;
    private final ObjectReader _reader;
    private final Class _type;

    public JsonStreamingArrayParser(InputStream in, Class elementType) {
        this(in, CustomJsonObjectMapperFactory.build(), elementType);
    }

    public JsonStreamingArrayParser(InputStream in, ObjectMapper mapper, Class elementType) {
        this(in, mapper, (Type) elementType);
    }

    public JsonStreamingArrayParser(InputStream in, TypeReference elementType) {
        this(in, CustomJsonObjectMapperFactory.build(), elementType.getType());
    }

    public JsonStreamingArrayParser(InputStream in, ObjectMapper mapper, TypeReference elementType) {
        this(in, mapper, elementType.getType());
    }

    private JsonStreamingArrayParser(InputStream in, ObjectMapper mapper, Type elementType) {
        try {
            JavaType javaType = mapper.constructType(elementType);
            //noinspection unchecked
            _type = (Class) javaType.getRawClass();
            _jp = mapper.getFactory().createParser(in);
            _reader = mapper.readerFor(javaType);

            // Parse at least the first byte of the response to make sure the input stream is valid.
            if (_jp.nextToken() != JsonToken.START_ARRAY) {
                throw new JsonParseException("Invalid JSON response, expected content to start with '[': " +
                        _jp.getCurrentToken(), _jp.getTokenLocation());
            }
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    @Override
    protected T computeNext() {
        try {
            if (_jp.nextToken() == JsonToken.END_ARRAY) {
                checkState(_jp.nextToken() == null, "Expected EOF in JavaScript input stream.");
                _jp.close();
                return endOfData();
            }
            return _type.cast(_reader.readValue(_jp));
        } catch (IOException e) {
            // If the root cause is a JsonProcessingException then throw it as a JsonStreamProcessingException.
            if (isJsonProcessingException(e)) {
                throw new JsonStreamProcessingException(e);
            }

            // We already parsed the first few bytes in the constructor and verified that the InputStream looked valid
            // so if there's an unexpected end of input here it likely means we lost the connection to the server.
            // In practice this a JsonStreamingEOFException or a TruncatedChunkException or something similar.
            throw new JsonStreamingEOFException(e);
        }
    }

    /**
     * Returns true if the exception is a JsonProcessingException whose root cause is not a premature end of data,
     * since this is more likely caused by a connection error.
     */
    private boolean isJsonProcessingException(Exception e) {
        // Unfortunately Jackson doesn't have specific subclasses for each parse exception, so we have to use the
        // more brittle approach of checking the exception message.
        return e instanceof JsonProcessingException &&
                !(e instanceof JsonParseException &&
                    e.getMessage() != null &&
                    e.getMessage().startsWith("Unexpected end-of-input"));
    }

    @Override
    public void close() throws IOException {
        _jp.close();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy