/*
* Copyright 2016 Netflix, Inc.
*
* 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.reactivex.netty.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandler;
import io.netty.util.ByteProcessor;
import java.nio.charset.Charset;
import java.util.List;
/**
* A utility class to be used with a channel handler to parse {@code ByteBuffer}s that contain strings terminated with
* a new line. This reader supports non-blocking incremental reads split across random places in the line. <p>
*
* This is <em>not</em> thread-safe and must only be called from a {@link ChannelHandler}.
*/
public class LineReader {
public static final int DEFAULT_INITIAL_CAPACITY = 256;
private static final ByteProcessor LINE_END_FINDER = new ByteProcessor() {
public static final char LF = 10;
@Override
public boolean process(byte value) throws Exception {
char nextByte = (char) value;
return LF != nextByte;
}
};
private ByteBuf incompleteBuffer;
private final int maxLineLength;
private final Charset encoding;
public LineReader() {
this(Integer.MAX_VALUE, Charset.defaultCharset());
}
public LineReader(int maxLineLength, Charset encoding) {
this.maxLineLength = maxLineLength;
this.encoding = encoding;
}
/**
* Reads the {@code in} buffer as much as it can and adds all the read lines into the {@code out} list. <p>
* If there is any outstanding data that is not read, it is stored into a temporary buffer and the next decode will
* prepend this data to the newly read data. <p>
*
* {@link #dispose()} must be called when the associated {@link ChannelHandler} is removed from the pipeline.
*
* @param in Buffer to decode.
* @param out List to add the read lines to.
* @param allocator Allocator to allocate new buffers, if required.
*/
public void decode(ByteBuf in, List<Object> out, ByteBufAllocator allocator) {
while (in.isReadable()) {
final int startIndex = in.readerIndex();
int lastReadIndex = in.forEachByte(LINE_END_FINDER);
if (-1 == lastReadIndex) {
// Buffer end without line termination
if (null == incompleteBuffer) {
incompleteBuffer = allocator.buffer(DEFAULT_INITIAL_CAPACITY, maxLineLength);
}
/*Add to the incomplete buffer*/
incompleteBuffer.ensureWritable(in.readableBytes());
incompleteBuffer.writeBytes(in);
} else {
ByteBuf lineBuf = in.readSlice(lastReadIndex - startIndex);
String line;
if (null != incompleteBuffer) {
line = incompleteBuffer.toString(encoding) + lineBuf.toString(encoding);
incompleteBuffer.release();
incompleteBuffer = null;
} else {
line = lineBuf.toString(encoding);
}
out.add(line);
in.skipBytes(1); // Skip new line character.
}
}
}
/**
* Same as {@link #decode(ByteBuf, List, ByteBufAllocator)} but it also produces the left-over buffer, even in
* absence of a line termination.
*
* {@link #dispose()} must be called when the associated {@link ChannelHandler} is removed from the pipeline.
*
* @param in Buffer to decode.
* @param out List to add the read lines to.
* @param allocator Allocator to allocate new buffers, if required.
*/
public void decodeLast(ByteBuf in, List<Object> out, ByteBufAllocator allocator) {
decode(in, out, allocator);
if (null != incompleteBuffer && incompleteBuffer.isReadable()) {
out.add(incompleteBuffer.toString(encoding));
}
}
/**
* Disposes any half-read data buffers.
*/
public void dispose() {
if (null != incompleteBuffer) {
incompleteBuffer.release();
}
}
public static boolean isLineDelimiter(char c) {
return c == '\r' || c == '\n';
}
/*Visible for testing*/ ByteBuf getIncompleteBuffer() {
return incompleteBuffer;
}
}