// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com> package com.iwebpp.node.stream; import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.List; import com.iwebpp.node.EventEmitter; import com.iwebpp.node.EventEmitter2; import com.iwebpp.node.NodeContext; import com.iwebpp.node.Util; public abstract class Writable2 extends EventEmitter2 implements Writable { public static class WriteReq { /** * @return the chunk */ public Object getChunk() { return chunk; } /** * @return the encoding */ public String getEncoding() { return encoding; } /** * @return the callback */ public WriteCB getCallback() { return callback; } private Object chunk; private String encoding; private WriteCB callback; public WriteReq(Object chunk, String encoding, WriteCB cb) { this.chunk = chunk; this.encoding = encoding; this.callback = cb; } @SuppressWarnings("unused") private WriteReq() { } } public static class Options { /** * @return the highWaterMark */ public int getHighWaterMark() { return highWaterMark; } /** * @return the objectMode */ public boolean isObjectMode() { return objectMode; } /** * @return the decodeStrings */ public boolean isDecodeStrings() { return decodeStrings; } /** * @return the defaultEncoding */ public String getDefaultEncoding() { return defaultEncoding; } private int highWaterMark; private boolean objectMode; private boolean decodeStrings; private String defaultEncoding; private boolean writable; /** * @return the writable */ public boolean isWritable() { return writable; } public Options( int highWaterMark, boolean decodeStrings, String defaultEncoding, boolean objectMode, boolean writable) { this.highWaterMark = highWaterMark; this.objectMode = objectMode; this.decodeStrings = decodeStrings; this.defaultEncoding = defaultEncoding; this.writable = writable; } @SuppressWarnings("unused") private Options(){} } public class State { private List<WriteReq> buffer; boolean objectMode; int highWaterMark; boolean needDrain; /** * @return the needDrain */ public boolean isNeedDrain() { return needDrain; } private boolean ending; private boolean ended; private boolean finished; private boolean decodeStrings; String defaultEncoding; private int length; boolean writing; int corked; boolean sync; boolean bufferProcessing; WriteCB onwrite; WriteCB writecb; int writelen; int pendingcb; boolean prefinished; private boolean errorEmitted; public State(Options options, final Writable2 stream) { // object stream flag to indicate whether or not this stream // contains buffers or objects. this.objectMode = options.objectMode; // TBD... ///if (stream instanceof Stream.Duplex) /// this.objectMode = this.objectMode || !!options.writableObjectMode; // the point at which write() starts returning false // Note: 0 is a valid value, means that we always return false if // the entire buffer is not flushed immediately on write() int hwm = options.highWaterMark; int defaultHwm = this.objectMode ? 16 : 16 * 1024; this.highWaterMark = (hwm >= 0) ? hwm : defaultHwm; // cast to ints. ///this.highWaterMark = ~~this.highWaterMark; this.needDrain = false; // at the start of calling end() this.setEnding(false); // when end() has been called, and returned this.setEnded(false); // when 'finish' is emitted this.setFinished(false); // should we decode strings into buffers before passing to _write? // this is here so that some node-core streams can optimize string // handling at a lower level. boolean noDecode = options.decodeStrings == false; this.setDecodeStrings(!noDecode); // Crypto is kind of old and crusty. Historically, its default string // encoding is 'binary' so we have to make this configurable. // Everything else in the universe uses 'utf8', though. this.defaultEncoding = options.defaultEncoding != null ? options.defaultEncoding : "UTF-8"; // not an actual buffer we keep track of, but a measurement // of how much we're waiting to get pushed to some underlying // socket or file. this.setLength(0); // a flag to see when we're in the middle of a write. this.writing = false; // when true all writes will be buffered until .uncork() call this.corked = 0; // a flag to be able to tell if the onwrite cb is called immediately, // or on a later tick. We set this to true at first, because any // actions that shouldn't happen until "later" should generally also // not happen before the first write call. this.sync = true; // a flag to know if we're processing previously buffered items, which // may call the _write() callback in the same tick, so that we don't // end up in an overlapped onwrite situation. this.bufferProcessing = false; // the callback that's passed to _write(chunk,cb) this.onwrite = new WriteCB() { @Override public void writeDone(String error) throws Exception { onwrite(stream, error); } }; // the callback that the user supplies to write(chunk,encoding,cb) this.writecb = null; // the amount that is being written when _write is called. this.writelen = 0; // WriteReq buffer this.setBuffer(new LinkedList<WriteReq>()); // number of pending user-supplied write callbacks // this must be 0 before 'finish' can be emitted this.pendingcb = 0; // emit prefinish if the only thing we're waiting for is _write cbs // This is relevant for synchronous Transform streams this.prefinished = false; } /** * @return the ended */ public boolean isEnded() { return ended; } /** * @param ended the ended to set */ public void setEnded(boolean ended) { this.ended = ended; } /** * @return the ending */ public boolean isEnding() { return ending; } /** * @param ending the ending to set */ public void setEnding(boolean ending) { this.ending = ending; } /** * @return the finished */ public boolean isFinished() { return finished; } /** * @param finished the finished to set */ public void setFinished(boolean finished) { this.finished = finished; } /** * @return the errorEmitted */ public boolean isErrorEmitted() { return errorEmitted; } /** * @param errorEmitted the errorEmitted to set */ public void setErrorEmitted(boolean errorEmitted) { this.errorEmitted = errorEmitted; } /** * @return the decodeStrings */ public boolean isDecodeStrings() { return decodeStrings; } /** * @param decodeStrings the decodeStrings to set */ public void setDecodeStrings(boolean decodeStrings) { this.decodeStrings = decodeStrings; } /** * @return the length */ public int getLength() { return length; } /** * @param length the length to set */ public void setLength(int length) { this.length = length; } /** * @return the buffer */ public List<WriteReq> getBuffer() { return buffer; } /** * @param buffer the buffer to set */ public void setBuffer(List<WriteReq> buffer) { this.buffer = buffer; } } // _write(chunk, encoding, callback) protected abstract void _write(Object chunk, String encoding, WriteCB cb) throws Exception; protected State _writableState; public boolean isNeedDrain() { return _writableState.needDrain; } private boolean writable; private NodeContext context; protected Writable2(NodeContext context, Options options) { super(); this.context = context; // Writable ctor is applied to Duplexes, though they're not // instanceof Writable, they're instanceof Readable. ///if (!(this instanceof Writable) && !(this instanceof Stream.Duplex)) /// return new Writable(options); this._writableState = new State(options, this); // legacy. this.writable = options.writable; //true; // TBD... } @SuppressWarnings("unused") private Writable2() { } public boolean writable() { return writable; } public void writable(boolean writable) { this.writable = writable; } // Helpers functions private void writeAfterEnd(Writable2 stream, State state, final WriteCB cb) throws Exception { ///var er = new Error('write after end'); // TODO: defer error events consistently everywhere, not just the cb stream.emit("error", "write after end"); //TBD... ///process.nextTick(function() { context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { cb.writeDone("write after end"); } }); } // If we get something that is not a buffer, string, null, or undefined, // and we're not in objectMode, then that's an error. // Otherwise stream chunks are all considered to be of length=1, and the // watermarks determine how many objects to keep in the buffer, rather than // how many bytes or characters. private boolean validChunk(Writable2 stream, State state, Object chunk, final WriteCB cb) throws Exception { boolean valid = true; if (!Util.isBuffer(chunk) && !Util.isString(chunk) && !Util.isNullOrUndefined(chunk) && !state.objectMode) { ///var er = new TypeError('Invalid non-string/buffer chunk'); final String er = "Invalid non-string/buffer chunk"; stream.emit("error", er); //TBD... ///process.nextTick(function() { context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { cb.writeDone(er); } }); valid = false; } return valid; } public boolean write(Object chunk, String encoding, WriteCB cb) throws Exception { State state = this._writableState; boolean ret = false; /*if (util.isFunction(encoding)) { cb = encoding; encoding = null; }*/ if (Util.isBuffer(chunk)) encoding = "buffer"; else if (Util.zeroString(encoding)) encoding = state.defaultEncoding; ///if (!util.isFunction(cb)) /// cb = function() {}; if (cb == null) cb = new WriteCB() { @Override public void writeDone(String error) { } }; if (state.isEnded()) writeAfterEnd(this, state, cb); else if (validChunk(this, state, chunk, cb)) { state.pendingcb++; ret = writeOrBuffer(this, state, chunk, encoding, cb); } return ret; } public boolean write(Object chunk, String encoding) throws Exception { return write(chunk, encoding, null); } public boolean write(Object chunk) throws Exception { return write(chunk, null, null); } public boolean write() throws Exception { return write(null, null, null); } public boolean end(Object chunk, String encoding, WriteCB cb) throws Exception { State state = this._writableState; /*if (util.isFunction(chunk)) { cb = chunk; chunk = null; encoding = null; } else if (util.isFunction(encoding)) { cb = encoding; encoding = null; }*/ ///if (!util.isNullOrUndefined(chunk)) if (!Util.isNullOrUndefined(chunk)) this.write(chunk, encoding, null); // .end() fully uncorks if (state.corked != 0) { state.corked = 1; this.uncork(); } // ignore unnecessary end() calls. if (!state.isEnding() && !state.isFinished()) endWritable(this, state, cb); return false; } public boolean end(Object chunk, String encoding) throws Exception { return end(chunk, encoding, null); } public boolean end(Object chunk) throws Exception { return end(chunk, null, null); } public boolean end() throws Exception { return end(null, null, null); } private void endWritable(Writable2 stream, State state, final WriteCB cb) throws Exception { state.setEnding(true); finishMaybe(stream, state); if (cb != null) { if (state.isFinished()) ///process.nextTick(cb); context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { cb.writeDone(null); } }); else stream.once("finish", new EventEmitter.Listener() { @Override public void onEvent(Object data) throws Exception { cb.writeDone(null); } }); } state.setEnded(true); } public void cork() { State state = this._writableState; state.corked++; } public void uncork() throws Exception { State state = this._writableState; if (state.corked > 0) { state.corked--; if (!state.writing && state.corked == 0 && !state.isFinished() && !state.bufferProcessing && state.getBuffer().size() > 0) clearBuffer(this, state); } } public int corked() { return this._writableState.corked; } // if we're already writing something, then just put this // in the queue, and wait our turn. Otherwise, call _write // If we return false, then we need a drain event, so set that flag. private boolean writeOrBuffer(Writable2 stream, State state, Object chunk, String encoding, WriteCB cb) throws Exception { chunk = decodeChunk(state, chunk, encoding); if (Util.isBuffer(chunk)) encoding = "buffer"; int len = state.objectMode ? 1 : Util.chunkLength(chunk); state.setLength(state.getLength() + len); boolean ret = state.getLength() < state.highWaterMark; // we must ensure that previous needDrain will not be reset to false. if (!ret) state.needDrain = true; if (state.writing || state.corked != 0) state.getBuffer().add(new WriteReq(chunk, encoding, cb)); else doWrite(stream, state, false, len, chunk, encoding, cb); return ret; } private Object decodeChunk(State state, Object chunk, String encoding) throws Exception { if (!state.objectMode && state.isDecodeStrings() != false && Util.isString(chunk)) { chunk = ByteBuffer.wrap(((String)chunk).getBytes(encoding)); } return chunk; } private void onwrite(final Writable2 stream, String error) throws Exception { final State state = stream._writableState; boolean sync = state.sync; final WriteCB cb = state.writecb; onwriteStateUpdate(state); if (error != null) onwriteError(stream, state, sync, error, cb); else { // Check if we're actually ready to finish, but don't emit yet final boolean finished = needFinish(stream, state); if (!finished && state.corked == 0 && !state.bufferProcessing && state.getBuffer().size() > 0) { clearBuffer(stream, state); } if (sync) { ///TBD ///process.nextTick(function() { context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { afterWrite(stream, state, finished, cb); } }); } else { afterWrite(stream, state, finished, cb); } } } // if there's something in the buffer waiting, then process it private void clearBuffer(Writable2 stream, State state) throws Exception { state.bufferProcessing = true; /*if (stream._writev && state.buffer.length > 1) { // Fast case, write everything using _writev() var cbs = []; for (var c = 0; c < state.buffer.length; c++) cbs.push(state.buffer[c].callback); // count the one we are adding, as well. // TODO(isaacs) clean this up state.pendingcb++; doWrite(stream, state, true, state.length, state.buffer, '', function(err) { for (var i = 0; i < cbs.length; i++) { state.pendingcb--; cbs[i](err); } }); // Clear buffer state.buffer = []; } else */ { // Slow case, write chunks one-by-one int c = 0; for (c = 0; c < state.getBuffer().size(); c++) { WriteReq entry = state.getBuffer().get(c); Object chunk = entry.chunk; String encoding = entry.encoding; WriteCB cb = entry.callback; int len = state.objectMode ? 1 : Util.chunkLength(chunk); doWrite(stream, state, false, len, chunk, encoding, cb); // if we didn't call the onwrite immediately, then // it means that we need to wait until it does. // also, that means that the chunk and cb are currently // being processed, so move the buffer counter past them. if (state.writing) { c++; break; } } if (c < state.getBuffer().size()) state.setBuffer((LinkedList<WriteReq>) state.getBuffer().subList(c, state.getBuffer().size())); else state.getBuffer().clear(); } state.bufferProcessing = false; } private void doWrite(Writable2 stream, State state, boolean b, int len, Object chunk, String encoding, WriteCB cb) throws Exception { state.writelen = len; state.writecb = cb; state.writing = true; state.sync = true; /*if (writev) stream._writev(chunk, state.onwrite); else*/ stream._write(chunk, encoding, state.onwrite); state.sync = false; } private void afterWrite(Writable2 stream, State state, boolean finished, WriteCB cb) throws Exception { if (!finished) onwriteDrain(stream, state); state.pendingcb--; cb.writeDone(null); finishMaybe(stream, state); } private boolean finishMaybe(Writable2 stream, State state) throws Exception { boolean need = needFinish(stream, state); if (need) { if (state.pendingcb == 0) { prefinish(stream, state); state.setFinished(true); stream.emit("finish"); } else prefinish(stream, state); } return need; } private void prefinish(Writable2 stream, State state) throws Exception { if (!state.prefinished) { state.prefinished = true; stream.emit("prefinish"); } } // Must force callback to be called on nextTick, so that we don't // emit 'drain' before the write() consumer gets the 'false' return // value, and has a chance to attach a 'drain' listener. private void onwriteDrain(Writable2 stream, State state) throws Exception { if (state.getLength() == 0 && state.needDrain) { state.needDrain = false; stream.emit("drain"); } } private boolean needFinish(Writable2 stream, State state) { return (state.isEnding() && state.getLength() == 0 && state.getBuffer().size() == 0 && !state.isFinished() && !state.writing); } private void onwriteError(Writable2 stream, final State state, boolean sync, final String error, final WriteCB cb) throws Exception { if (sync) { /// TBD ///process.nextTick(function() { context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { state.pendingcb--; cb.writeDone(error); } }); } else { state.pendingcb--; cb.writeDone(error); } stream.emit("error", error); } private void onwriteStateUpdate(State state) { state.writing = false; state.writecb = null; state.setLength(state.getLength() - state.writelen); state.writelen = 0; } }