package com.mastfrog.acteur.jdbc; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.mastfrog.util.Exceptions; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.util.CharsetUtil; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ExecutorService; /** * Writes out a result set as JSON, calling itself back one row at a time so * threads are maximally available to serve other requests. * <p/> * Contains a workaround for * <a href="https://github.com/netty/netty/issues/2415">this Netty issue</a> * for the time being, ensuring writes happen in the expected sequence. * <p/> * For ease of construction, ask for a <a href="ResultSetWriterFactory.html">ResultSetWriterFactory</a> * to be injected, or use <a href="ResultSetWriterActeur.html">ResultSetWriterActeur</a>. * * @author Tim Boudreau */ public class ResultSetWriter implements ChannelFutureListener { private final ResultSet resultSet; private volatile boolean first = true; private final ObjectMapper mapper; private final ByteBufAllocator alloc; private final ExecutorService svc; @Inject public ResultSetWriter(ResultSet resultSet, ObjectMapper mapper, ByteBufAllocator alloc, @Named(/*ServerModule.BACKGROUND_THREAD_POOL_NAME*/ "background") ExecutorService svc) { this.resultSet = resultSet; this.mapper = mapper; this.alloc = alloc; this.svc = svc; } private volatile int entryCount; @Override public synchronized void operationComplete(ChannelFuture f) { if (entryCount > 0) { // See https://github.com/netty/netty/issues/2415 for why this is needed final ChannelFuture ff = f; svc.submit(new Runnable() { @Override public void run() { ResultSetWriter.this.operationComplete(ff); } }); return; } try { entryCount++; StringBuilder sb = new StringBuilder(); boolean done = false; if (resultSet.isClosed()) { f.channel().close(); return; } boolean wasFirst = first; if (first) { sb.append("["); first = false; } if (resultSet.next()) { if (!wasFirst) { sb.append(",\n"); } ResultSetMetaData md = resultSet.getMetaData(); Map<String, Object> data = new LinkedHashMap<>(); int count = md.getColumnCount(); for (int i = 0; i < count; i++) { String name = md.getColumnName(i + 1); Object value = resultSet.getObject(i + 1); data.put(name, value); } sb.append(mapper.writeValueAsString(data)); } else { done = true; sb.append("]\n"); } if (sb.length() > 0) { // inserting this sleep cures the problem // Thread.sleep(200); ByteBuf buf = alloc.buffer().writeBytes(sb.toString().getBytes(CharsetUtil.UTF_8)); f = f.channel().writeAndFlush(new DefaultHttpContent(buf)); } if (done) { f = f.channel().writeAndFlush(new DefaultLastHttpContent()).addListener(CLOSE); } else { f.addListener(this); } } catch (SQLException | JsonProcessingException e) { try { f.channel().close(); } finally { try { resultSet.close(); } catch (SQLException ex) { e.addSuppressed(ex); } } Exceptions.chuck(e); } finally { first = false; entryCount--; } } }