/* ****************************************************************************** * Copyright (c) 2006-2012 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ package org.xmind.core.command.transfer; import java.io.Closeable; import java.io.FilterOutputStream; import java.io.Flushable; import java.io.IOException; import java.io.OutputStream; /** * A helper class that writes data to an output stream in chunks. * * @author Frank Shaka * @see ChunkReader */ public class ChunkWriter implements Closeable, Flushable { private class ChunkOutputStream extends FilterOutputStream { private boolean closed = false; public ChunkOutputStream(OutputStream out) { super(out); } @Override public void close() throws IOException { flushBinaryBuffer(true); flushBuffer(); if (!closed) { out.write(separator); out.flush(); } closed = true; } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) throws IOException { if (closed) throw new IOException("Stream already closed."); //$NON-NLS-1$ for (int i = 0; i < len; i++) { appendToBuffer(b[off + i]); } } @Override public void write(int b) throws IOException { if (closed) throw new IOException("Stream already closed."); //$NON-NLS-1$ appendToBuffer((byte) (b & 0xff)); } @Override public void flush() throws IOException { flushBinaryBuffer(false); flushBuffer(); out.flush(); } } private static final byte DEFAULT_CHUNK_SEPARATOR = (byte) '\n'; private static final String DEFAULT_TEXT_ENCODING = "UTF-8"; //$NON-NLS-1$ private static final int DEFAULT_BUFFER_SIZE = 16; private static final int DEFAULT_BINARY_BUFFER_SIZE = DEFAULT_BUFFER_SIZE / 4 * 3; /** * This array is a lookup table that translates 6-bit positive integer index * values into their "Base64 Alphabet" equivalents as specified in Table 1 * of RFC 2045. */ private static final byte intToBase64[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; private OutputStream out; private String textEncoding; private byte separator; private ChunkOutputStream binaryStream = null; private byte[] binaryBuffer = new byte[DEFAULT_BINARY_BUFFER_SIZE]; private int binaryBuffered = 0; private byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; private int buffered = 0; public ChunkWriter(OutputStream out) { this(out, DEFAULT_CHUNK_SEPARATOR, DEFAULT_TEXT_ENCODING); } public ChunkWriter(OutputStream out, byte chunkSeparator, String textEncoding) { this.out = out; this.separator = chunkSeparator; this.textEncoding = textEncoding; } public synchronized void writeText(String text) throws IOException { closeBinaryStream(); if (text == null) text = ""; //$NON-NLS-1$ byte[] bytes = text.getBytes(textEncoding); out.write(bytes); out.write(separator); out.flush(); } public synchronized OutputStream openNextChunkAsStream() throws IOException { closeBinaryStream(); return binaryStream = new ChunkOutputStream(out); } public synchronized void close() throws IOException { closeBinaryStream(); try { out.flush(); } finally { out.close(); } } public synchronized void flush() throws IOException { flushBinaryBuffer(false); flushBuffer(); out.flush(); } private synchronized void closeBinaryStream() throws IOException { if (binaryStream != null && !binaryStream.closed) { binaryStream.close(); binaryStream = null; } } private synchronized void appendToBuffer(byte b) throws IOException { binaryBuffer[binaryBuffered++] = b; if (binaryBuffered >= binaryBuffer.length) { int off = 0; while (off < binaryBuffered) { int b0 = binaryBuffer[off++] & 0xff; int b1 = off >= binaryBuffered ? -1 : binaryBuffer[off++] & 0xff; int b2 = off >= binaryBuffered ? -1 : binaryBuffer[off++] & 0xff; writeBinary(b0, b1, b2); } binaryBuffered = 0; } } private synchronized void writeBinary(int b0, int b1, int b2) throws IOException { buffer[buffered++] = intToBase64[b0 >> 2]; if (b1 < 0) { buffer[buffered++] = intToBase64[(b0 << 4) & 0x3f]; buffer[buffered++] = '='; buffer[buffered++] = '='; } else { buffer[buffered++] = intToBase64[(b0 << 4) & 0x3f | (b1 >> 4)]; if (b2 < 0) { buffer[buffered++] = intToBase64[(b1 << 2) & 0x3f]; buffer[buffered++] = '='; } else { buffer[buffered++] = intToBase64[(b1 << 2) & 0x3f | (b2 >> 6)]; buffer[buffered++] = intToBase64[b2 & 0x3f]; } } if (buffered >= buffer.length || b2 < 0) { try { out.write(buffer, 0, buffered); } finally { buffered = 0; } } } private synchronized void flushBinaryBuffer(boolean all) throws IOException { if (binaryBuffered > 0) { int off = 0; int max = all ? binaryBuffered : binaryBuffered - 2; while (off < max) { int b0 = binaryBuffer[off++] & 0xff; int b1 = off >= binaryBuffered ? -1 : binaryBuffer[off++] & 0xff; int b2 = off >= binaryBuffered ? -1 : binaryBuffer[off++] & 0xff; writeBinary(b0, b1, b2); } int newCount = binaryBuffered - off; if (newCount > 0) { System.arraycopy(binaryBuffer, off, binaryBuffer, 0, newCount); } binaryBuffered = newCount; } } private synchronized void flushBuffer() throws IOException { if (buffered > 0) { try { out.write(buffer, 0, buffered); } finally { buffered = 0; } } } }