/* * The MIT License * * Copyright 2015 Tim Boudreau. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.mastfrog.acteur.mongo.async; import com.google.inject.Inject; import com.mastfrog.acteur.HttpEvent; import com.mastfrog.acteur.spi.ApplicationControl; import com.mastfrog.util.Codec; import com.mongodb.async.AsyncBatchCursor; import com.mongodb.async.SingleResultCallback; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.LastHttpContent; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * * @author Tim Boudreau */ final class CursorWriter<T> implements ChannelFutureListener, SingleResultCallback<List<T>> { private final AsyncBatchCursor<T> cursor; private final boolean closeConnection; private final List<Object> collectedResults = new LinkedList<>(); private final ApplicationControl ctrl; private final AtomicBoolean first = new AtomicBoolean(); private final ConstantBuffers constant; private AtomicReference<ChannelFuture> future = new AtomicReference<>(); private volatile boolean done; private volatile boolean resultWritten; private final Codec codec; @Inject public CursorWriter(final AsyncBatchCursor<T> cursor, HttpEvent evt, ApplicationControl ctrl, ConstantBuffers constant, Codec codec) { this.cursor = cursor; this.closeConnection = !evt.isKeepAlive(); this.ctrl = ctrl; this.constant = constant; this.codec = codec; } ChannelFuture future(ChannelFuture channel) { future.set(channel); return channel; } private void addBuf(CompositeByteBuf buf, ByteBuf toAdd) { int writerIndex = buf.writerIndex(); buf.addComponent(toAdd.retain()); buf.writerIndex(writerIndex + toAdd.readableBytes()); } @Override @SuppressWarnings("unchecked") public void operationComplete(ChannelFuture f) throws Exception { boolean done = this.done; if (!f.isSuccess() && f.cause() != null) { if (f.cause() != null) { ctrl.internalOnError(f.cause()); } cursor.close(); return; } List<Object> results = new LinkedList<>(); synchronized (this) { results.addAll(collectedResults); collectedResults.clear(); } // Buffer count will be size + a comma for each, plus potential open and close marks and a leading comma int maxComponents = (results.size() * 2) + 5; // Use a composite byte buf for zero copy if using ByteBufCodec (which shares the // allocator with the application, so they are drawn from the same pool) CompositeByteBuf buf = f.channel().alloc().compositeBuffer(maxComponents); boolean first = this.first.compareAndSet(false, true); if (first) { addBuf(buf, constant.open()); } if (!results.isEmpty()) { if (resultWritten) { addBuf(buf, constant.comma()); } for (Iterator<Object> it = results.iterator(); it.hasNext();) { resultWritten = true; Object next = it.next(); if (next instanceof ByteBuf) { addBuf(buf, (ByteBuf) next); } else { addBuf(buf, f.channel().alloc().buffer().writeBytes(codec.writeValueAsBytes(next))); } if (it.hasNext()) { addBuf(buf, constant.comma()); } } } if (results.isEmpty() && done) { addBuf(buf, constant.close()); } if (buf.writerIndex() > 0) { future(f.channel().writeAndFlush(new DefaultHttpContent(buf))); } else { future(f); } if (done) { future(f.channel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)); } if (done && closeConnection) { future(f.addListener(CLOSE)); } if (!done) { f.addListener(fetchNext); } } @Override public void onResult(List<T> t, Throwable thrwbl) { if (t == null) { done = true; } else { synchronized (this) { collectedResults.addAll(t); } } ChannelFuture f = future.get(); if (thrwbl != null) { ctrl.internalOnError(thrwbl); cursor.close(); if (f != null) { f.channel().close(); } } else if (f != null) { try { operationComplete(f); } catch (Exception ex) { ctrl.internalOnError(ex); } } } private final ChannelFutureListener fetchNext = new ChannelFutureListener() { @Override @SuppressWarnings("unchecked") public void operationComplete(ChannelFuture f) throws Exception { cursor.next(CursorWriter.this); } }; }