io.vertx.core.parsetools.impl.RecordParserImpl.java Source code

Java tutorial

Introduction

Here is the source code for io.vertx.core.parsetools.impl.RecordParserImpl.java

Source

/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core.parsetools.impl;

import io.netty.buffer.Unpooled;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.impl.Arguments;
import io.vertx.core.parsetools.RecordParser;
import io.vertx.core.streams.ReadStream;

import java.util.Objects;

/**
 * @author <a href="http://tfox.org">Tim Fox</a>
 * @author <a href="mailto:larsdtimm@gmail.com">Lars Timm</a>
 */
public class RecordParserImpl implements RecordParser {

    // Empty and unmodifiable
    private static final Buffer EMPTY_BUFFER = Buffer.buffer(Unpooled.EMPTY_BUFFER);

    private Buffer buff = EMPTY_BUFFER;
    private int pos; // Current position in buffer
    private int start; // Position of beginning of current record
    private int delimPos; // Position of current match in delimiter array

    private boolean delimited;
    private byte[] delim;
    private int recordSize;
    private int maxRecordSize;
    private long demand = Long.MAX_VALUE;
    private Handler<Buffer> eventHandler;
    private Handler<Void> endHandler;
    private Handler<Throwable> exceptionHandler;
    private boolean parsing;
    private boolean streamEnded;

    private final ReadStream<Buffer> stream;

    private RecordParserImpl(ReadStream<Buffer> stream) {
        this.stream = stream;
    }

    public void setOutput(Handler<Buffer> output) {
        Objects.requireNonNull(output, "output");
        eventHandler = output;
    }

    /**
     * Helper method to convert a latin-1 String to an array of bytes for use as a delimiter
     * Please do not use this for non latin-1 characters
     *
     * @param str  the string
     * @return The byte[] form of the string
     */
    public static Buffer latin1StringToBytes(String str) {
        byte[] bytes = new byte[str.length()];
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            bytes[i] = (byte) (c & 0xFF);
        }
        return Buffer.buffer(bytes);
    }

    /**
     * Create a new {@code RecordParser} instance, initially in delimited mode, and where the delimiter can be represented
     * by the String {@code} delim endcoded in latin-1 . Don't use this if your String contains other than latin-1 characters.
     * <p>
     * {@code output} Will receive whole records which have been parsed.
     *
     * @param delim  the initial delimiter string
     * @param output  handler that will receive the output
     */
    public static RecordParser newDelimited(String delim, ReadStream<Buffer> stream, Handler<Buffer> output) {
        return newDelimited(latin1StringToBytes(delim), stream, output);
    }

    /**
     * Create a new {@code RecordParser} instance, initially in delimited mode, and where the delimiter can be represented
     * by the {@code buffer} delim.
     * <p>
     * {@code output} Will receive whole records which have been parsed.
     *
     * @param delim  the initial delimiter buffer
     * @param output  handler that will receive the output
     */
    public static RecordParser newDelimited(Buffer delim, ReadStream<Buffer> stream, Handler<Buffer> output) {
        RecordParserImpl ls = new RecordParserImpl(stream);
        ls.handler(output);
        ls.delimitedMode(delim);
        return ls;
    }

    /**
     * Create a new {@code RecordParser} instance, initially in fixed size mode, and where the record size is specified
     * by the {@code size} parameter.
     * <p>
     * {@code output} Will receive whole records which have been parsed.
     *
     * @param size  the initial record size
     * @param output  handler that will receive the output
     */
    public static RecordParser newFixed(int size, ReadStream<Buffer> stream, Handler<Buffer> output) {
        Arguments.require(size > 0, "Size must be > 0");
        RecordParserImpl ls = new RecordParserImpl(stream);
        ls.handler(output);
        ls.fixedSizeMode(size);
        return ls;
    }

    /**
     * Flip the parser into delimited mode, and where the delimiter can be represented
     * by the String {@code delim} encoded in latin-1 . Don't use this if your String contains other than latin-1 characters.
     * <p>
     * This method can be called multiple times with different values of delim while data is being parsed.
     *
     * @param delim  the new delimeter
     */
    public void delimitedMode(String delim) {
        delimitedMode(latin1StringToBytes(delim));
    }

    /**
     * Flip the parser into delimited mode, and where the delimiter can be represented
     * by the delimiter {@code delim}.
     * <p>
     * This method can be called multiple times with different values of delim while data is being parsed.
     *
     * @param delim  the new delimiter
     */
    public void delimitedMode(Buffer delim) {
        Objects.requireNonNull(delim, "delim");
        delimited = true;
        this.delim = delim.getBytes();
        delimPos = 0;
    }

    /**
     * Flip the parser into fixed size mode, where the record size is specified by {@code size} in bytes.
     * <p>
     * This method can be called multiple times with different values of size while data is being parsed.
     *
     * @param size  the new record size
     */
    public void fixedSizeMode(int size) {
        Arguments.require(size > 0, "Size must be > 0");
        delimited = false;
        recordSize = size;
    }

    /**
     * Set the maximum allowed size for a record when using the delimited mode.
     * The delimiter itself does not count for the record size.
     * <p>
     * If a record is longer than specified, an {@link IllegalStateException} will be thrown.
     *
     * @param size the maximum record size
     * @return  a reference to this, so the API can be used fluently
     */
    public RecordParser maxRecordSize(int size) {
        Arguments.require(size > 0, "Size must be > 0");
        maxRecordSize = size;
        return this;
    }

    private void handleParsing() {
        if (parsing) {
            return;
        }
        parsing = true;
        try {
            do {
                if (demand > 0L) {
                    int next;
                    if (delimited) {
                        next = parseDelimited();
                    } else {
                        next = parseFixed();
                    }
                    if (next == -1) {
                        if (streamEnded) {
                            if (buff.length() == 0) {
                                break;
                            }
                            next = buff.length();
                        } else {
                            ReadStream<Buffer> s = stream;
                            if (s != null) {
                                s.resume();
                            }
                            if (streamEnded) {
                                continue;
                            }
                            break;
                        }
                    }
                    if (demand != Long.MAX_VALUE) {
                        demand--;
                    }
                    Buffer event = buff.getBuffer(start, next);
                    start = pos;
                    Handler<Buffer> handler = eventHandler;
                    if (handler != null) {
                        handler.handle(event);
                    }
                    if (streamEnded) {
                        break;
                    }
                } else {
                    // Should use a threshold ?
                    ReadStream<Buffer> s = stream;
                    if (s != null) {
                        s.pause();
                    }
                    break;
                }
            } while (true);
            int len = buff.length();
            if (start == len) {
                buff = EMPTY_BUFFER;
            } else {
                buff = buff.getBuffer(start, len);
            }
            pos -= start;
            start = 0;
            if (streamEnded) {
                end();
            }
        } finally {
            parsing = false;
        }
    }

    private int parseDelimited() {
        int len = buff.length();
        for (; pos < len; pos++) {
            if (buff.getByte(pos) == delim[delimPos]) {
                delimPos++;
                if (delimPos == delim.length) {
                    pos++;
                    delimPos = 0;
                    return pos - delim.length;
                }
            } else {
                if (delimPos > 0) {
                    pos -= delimPos;
                    delimPos = 0;
                }
            }
        }
        return -1;
    }

    private int parseFixed() {
        int len = buff.length();
        if (len - start >= recordSize) {
            int end = start + recordSize;
            pos = end;
            return end;
        }
        return -1;
    }

    /**
     * This method is called to provide the parser with data.
     *
     * @param buffer  a chunk of data
     */
    public void handle(Buffer buffer) {
        if (buff.length() == 0) {
            buff = buffer;
        } else {
            buff.appendBuffer(buffer);
        }
        handleParsing();
        if (buff != null && maxRecordSize > 0 && buff.length() > maxRecordSize) {
            IllegalStateException ex = new IllegalStateException("The current record is too long");
            if (exceptionHandler != null) {
                exceptionHandler.handle(ex);
            } else {
                throw ex;
            }
        }
    }

    private void end() {
        Handler<Void> handler = endHandler;
        if (handler != null) {
            handler.handle(null);
        }
    }

    @Override
    public RecordParser exceptionHandler(Handler<Throwable> handler) {
        exceptionHandler = handler;
        return this;
    }

    @Override
    public RecordParser handler(Handler<Buffer> handler) {
        eventHandler = handler;
        if (stream != null) {
            if (handler != null) {
                stream.endHandler(v -> {
                    streamEnded = true;
                    handleParsing();
                });
                stream.exceptionHandler(err -> {
                    if (exceptionHandler != null) {
                        exceptionHandler.handle(err);
                    }
                });
                stream.handler(this);
            } else {
                stream.handler(null);
                stream.endHandler(null);
                stream.exceptionHandler(null);
            }
        }
        return this;
    }

    @Override
    public RecordParser pause() {
        demand = 0L;
        return this;
    }

    @Override
    public RecordParser fetch(long amount) {
        Arguments.require(amount > 0, "Fetch amount must be > 0");
        demand += amount;
        if (demand < 0L) {
            demand = Long.MAX_VALUE;
        }
        handleParsing();
        return this;
    }

    @Override
    public RecordParser resume() {
        return fetch(Long.MAX_VALUE);
    }

    @Override
    public RecordParser endHandler(Handler<Void> handler) {
        endHandler = handler;
        return this;
    }
}