// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.io; import com.yahoo.text.AbstractUtf8Array; import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.util.ArrayList; import java.util.List; /** * Data store for AbstractByteWriter. Tested in unit tests for ByteWriter. * * @author Steinar Knutsen */ public final class BufferChain { // refer to the revision history of ByteWriter for more information about // the reasons behind the sizing of BUFFERSIZE, WATERMARK and MAXBUFFERS static final int BUFFERSIZE = 4096; static final int WATERMARK = 1024; static final int MAXBUFFERS = 50; static { //noinspection ConstantConditions assert BUFFERSIZE > WATERMARK; } private final List buffers = new ArrayList<>(); private final WritableByteTransmitter endpoint; private ByteBuffer current = ByteBuffer.allocate(BUFFERSIZE); private long appended = 0L; public BufferChain(final WritableByteTransmitter endpoint) { this.endpoint = endpoint; } public void append(final byte b) throws IOException { makeRoom(1); current.put(b); } private final boolean shouldCopy(int length) { return (length < WATERMARK); } private final void makeRoom(int length) throws IOException { if (current.remaining() < length) { scratch(); } } public void append(AbstractUtf8Array v) throws IOException { final int length = v.getByteLength(); if (shouldCopy(length)) { makeRoom(length); v.writeTo(current); } else { append(v.wrap()); } } public void append(final byte[] alreadyEncoded) throws java.io.IOException { if (alreadyEncoded.length > 0) { append(alreadyEncoded, 0, alreadyEncoded.length); } } public void append(final byte[] alreadyEncoded, final int offset, final int length) throws java.io.IOException { if (shouldCopy(length)) { makeRoom(length); current.put(alreadyEncoded, offset, length); } else { append(ByteBuffer.wrap(alreadyEncoded, offset, length)); } } public void append(final ByteBuffer alreadyEncoded) throws java.io.IOException { if (alreadyEncoded.remaining() == 0) { return; } final int length = alreadyEncoded.limit() - alreadyEncoded.position(); if (shouldCopy(length)) { makeRoom(length); current.put(alreadyEncoded); } else { scratch(); add(alreadyEncoded); } } private final void add(final ByteBuffer buf) { buffers.add(buf); appended += buf.limit(); } public void append(final CharBuffer toEncode, final CharsetEncoder encoder) throws java.io.IOException { CoderResult overflow; do { overflow = encoder.encode(toEncode, current, true); if (overflow.isOverflow()) { scratch(); } else if (overflow.isError()) { try { toEncode.get(); } catch (final BufferUnderflowException e) { // Give up if we can't discard some presumptively malformed // or unmappable data break; } } } while (!overflow.isUnderflow()); } private void scratch() throws java.io.IOException { if (!possibleFlush() && current.position() != 0) { current.flip(); add(current); current = ByteBuffer.allocate(BUFFERSIZE); } } private boolean possibleFlush() throws java.io.IOException { if (buffers.size() > MAXBUFFERS) { flush(); return true; } return false; } public void flush() throws IOException { for (final ByteBuffer b : buffers) { endpoint.send(b); } buffers.clear(); if (current.position() > 0) { current.flip(); appended += current.limit(); endpoint.send(current); current = ByteBuffer.allocate(BUFFERSIZE); } } /** * @return number of bytes written to this buffer */ public long appended() { return appended + current.position(); } }