package com.github.kmkt.util; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; /** * InputStream を指定されたデリミタ(byte[])で個別の InputStream に分割する。 * mod 演算高速化のためバッファサイズ調整 + QuickSearch化 * <pre> * 非スレッドセーフ * </pre> * * License : MIT License */ public class StreamSplitter implements AutoCloseable { private byte[] delimiter = null; // デリミタ private int delimiterLength; // デリミタ長 private int[] skipTable = new int[256]; // デリミタに対応するQuickSearch用シフトテーブル private InputStream inputStream = null; // 元InputStream private InnerInputStream childStream = null; // 分割後のInputStream private static final int BUF_SIZE = 4*1024; private byte[] ringBufffer = null; private int modMask = 0; private int avalableBufSize = 0; // 読み込み済みサイズ private int avalableSize = 0; // 検索済みサイズ private int readPos = 0; // 次読み込みindex private int searchPos = 0; // 次検索開始index private int delimiterPos = -1; // デリミタ先頭index -1 はデリミタ未発見 private boolean inputEoS = false; // 元InputStream終端フラグ /** * * @param is * @param delimiter */ public StreamSplitter(InputStream is, byte[] delimiter) { this(is, delimiter, BUF_SIZE); } /** * * @param is * @param delimiter * @param buf_size */ public StreamSplitter(InputStream is, byte[] delimiter, int buf_size) { if (is == null) throw new IllegalArgumentException("is should not be null"); if (delimiter == null) throw new IllegalArgumentException("delimiter should not be null"); if (buf_size < delimiter.length*4) throw new IllegalArgumentException("buf_size should be 4 times larger than delimiter size"); // 指定の buf_size が収まる 2^n サイズにバッファサイズを調整 int mod_mask = buf_size - 1; mod_mask |= (mod_mask >>> 1); mod_mask |= (mod_mask >>> 2); mod_mask |= (mod_mask >>> 4); mod_mask |= (mod_mask >>> 6); mod_mask |= (mod_mask >>> 16); buf_size = mod_mask + 1; this.modMask = mod_mask; this.inputStream = is; this.delimiter = Arrays.copyOf(delimiter, delimiter.length); this.delimiterLength = this.delimiter.length; this.ringBufffer = new byte[buf_size]; // delimiter に対応する QuickSearch シフトテーブル作成 for (int i = 0; i < this.skipTable.length; i++) { this.skipTable[i] = delimiter.length + 1; } for (int i = 0; i < delimiter.length; i++) { this.skipTable[delimiter[i]] = delimiter.length - i; } } /** * ringBufffer 内の delimiter を検索する * @param stpos 検索範囲先頭 * @param limitpos 検索範囲末 * @return 発見できない場合は-1 */ private int searchDelimiter(int stpos, int limitpos) { if (stpos < 0 || ringBufffer.length <= stpos) throw new IllegalArgumentException("stpos is out of range"); if (limitpos < 0 || ringBufffer.length <= limitpos) throw new IllegalArgumentException("limitpos is out of range"); if (stpos == limitpos) throw new IllegalArgumentException("stpos and limitpos should not be at same pos"); int search_range = (stpos < limitpos ? limitpos - stpos + 1 : ringBufffer.length - stpos + limitpos + 1); search_range = search_range - delimiter.length; // System.out.printf("%d %d %d%n", stpos, limitpos,search_range); if (search_range < 0) return -1; // Quick Search for (int i = 0; i <= search_range;) { int pos = (stpos + i) & modMask; for (int j = 0; j < delimiterLength; j++) { if (ringBufffer[(pos + j) & modMask] == delimiter[j]) { if (j == delimiterLength - 1) { return (pos & modMask); } } else { i += skipTable[ringBufffer[(pos + delimiterLength) & modMask] & 0xff]; break; } } } return -1; } @Override public void close() throws IOException { inputStream.close(); } public InputStream nextStream() { if (childStream == null) { childStream = new InnerInputStream(); return childStream; } if (inputEoS && avalableBufSize <= delimiter.length) return null; if (!childStream.isFinished()) throw new IllegalStateException("Previous stream has available data yet"); childStream = new InnerInputStream(); // デリミタ分読み飛ばし操作 avalableBufSize -= delimiter.length; readPos = searchPos; searchNextDelimiter(); return childStream; } /** * 分割後のInputStream */ private class InnerInputStream extends InputStream { private boolean eos = false; protected boolean isFinished() { return eos; } @Override public int available() throws IOException { return avalableSize; } @Override public int read() throws IOException { if (eos || readPos == delimiterPos) { eos = true; return -1; } if (!inputEoS && (avalableSize <= 0)) { readNextBlock(); } int c = readRingBuffer(); if (c == -1) { eos = true; } return c; } } /** * RingBufferへの書き込み * @param c * @return */ private boolean writeRingBuffer(byte c) { if (avalableBufSize == ringBufffer.length) return false; // full int pos = (readPos + avalableBufSize) & modMask; ringBufffer[pos] = c; avalableBufSize++; return true; } /** * RingBufferから読み出し * @return */ private int readRingBuffer() { if (avalableBufSize == 0 || avalableSize == 0) return -1; // empty int c = ringBufffer[readPos]; readPos++; if (ringBufffer.length == readPos) readPos = 0; avalableBufSize--; avalableSize--; return c & 0x00ff; } /** * 元InputStreamからRingBufferの空き部分に読み込む * @throws IOException */ private void readNextBlock() throws IOException { if (inputEoS) return; int limit = ringBufffer.length - avalableBufSize; int i = 0; IOException e = null; try { for (i = 0; i < limit; i++) { int c = inputStream.read(); if (c == -1) { inputEoS = true; // 入力側EOS break; } writeRingBuffer((byte) (c & 0x00ff)); } } catch (IOException e2) { if (i == 0) { throw e2; // 初回読み込みでの例外はそのまま投げる } else { e = e2; // それ以外は次の処理を終えてから } } searchNextDelimiter(); if (e != null) throw e; } /** * デリミタ検索:前回終了位置~今回の書き込み末 の範囲 */ private void searchNextDelimiter() { if (avalableBufSize < delimiter.length) { // バッファ内データがデリミタサイズに満たない場合:未発見 delimiterPos = -1; return; } int edpos = (readPos + avalableBufSize - 1) & modMask; // 有効範囲末 int delimiterpos = searchDelimiter(searchPos, edpos); // 検索 if (delimiterpos == -1) { // 未発見 delimiterPos = -1; // デリミタが無い範囲 int len = (edpos + 1) - searchPos - delimiter.length + 1; if (len < 0) len += ringBufffer.length; avalableSize += len; searchPos = (edpos + 1) - delimiter.length + 1; // 次検索開始位置 if (searchPos < 0) { searchPos += ringBufffer.length; } } else { // 発見 delimiterPos = delimiterpos; int len = delimiterpos - searchPos; if (len < 0) { len += ringBufffer.length; } avalableSize += len; // 次検索開始はデリミタの後ろから searchPos = (delimiterpos + delimiter.length) & modMask; } } }