package org.yamcs.api.rest; import java.nio.channels.ClosedChannelException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.yamcs.api.YamcsApiException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; /** * Used to post large quantities of data to yamcs. * The data is sent using HTTP chuncked encoding */ public class BulkRestDataSender extends SimpleChannelInboundHandler<FullHttpResponse>{ ChannelHandlerContext ctx; CompletableFuture<byte[]> completeRequestCf = new CompletableFuture<>(); volatile YamcsApiException yamcsApiException = null; @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { this.ctx = ctx; } /** * send the next chunk of data. * The caller is blocked if it sends data faster that can be transfered to the server. * @param data * @throws YamcsApiException - thrown if there is an exception sending the data. The exception is also thrown if no data can be sent for 10 seconds */ public void sendData(byte[] data) throws YamcsApiException { ByteBuf buf = Unpooled.wrappedBuffer(data); sendData(buf); } int count =0; public void sendData(ByteBuf buf) throws YamcsApiException { if(yamcsApiException!=null) { throw yamcsApiException; } count++; try { Channel ch = ctx.channel(); if (!ch.isOpen()) { throw new ClosedChannelException(); } ChannelFuture writeFuture = ctx.writeAndFlush(new DefaultHttpContent(buf)); if (!ch.isWritable()) { boolean writeCompleted = writeFuture.await(10, TimeUnit.SECONDS); if (!writeCompleted) { throw new YamcsApiException("Channel did not become writable in 10 seconds"); } } } catch (Exception e) { completeRequestCf.completeExceptionally(e); if(e instanceof YamcsApiException) { throw (YamcsApiException) e; } else { throw new YamcsApiException(e.toString(), e); } } } /** * Complete the request by a final empty chunck and return the response from the server. * * @return a CompletableFuture that completes once the response from the server has been received. */ public CompletableFuture<byte[]> completeRequest() { if(completeRequestCf.isDone()) { return completeRequestCf; } Channel ch = ctx.channel(); if (!ch.isOpen()) { completeRequestCf.completeExceptionally(new ClosedChannelException()); } ChannelFuture writeFuture = ctx.writeAndFlush(new DefaultLastHttpContent()); writeFuture.addListener(f-> { if(!f.isSuccess()) { completeRequestCf.completeExceptionally(f.cause()); } }); return completeRequestCf; } @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception { HttpResponseStatus status = msg.status(); if(status.equals(HttpResponseStatus.OK)) { byte[] b = HttpClient.getByteArray(msg.content()); completeRequestCf.complete(b); } else { yamcsApiException = HttpClient.decodeException(msg); completeRequestCf.completeExceptionally(yamcsApiException); } } /** * * This handler expects to receive a 100 Continue message which means that the request is ok and the sender can start streaming data * if this is received, a new bulk sender will be created and added to the pipeline and the CompletableFuture will be completed with the new object * if any other response is received, the CompletableFuture will be completed exceptionally. */ static class ContinuationHandler extends SimpleChannelInboundHandler<HttpResponse> { CompletableFuture<BulkRestDataSender> cf; public ContinuationHandler(CompletableFuture<BulkRestDataSender> cf) { this.cf = cf; } @Override protected void channelRead0(ChannelHandlerContext ctx, HttpResponse msg) { if(msg.status().equals(HttpResponseStatus.CONTINUE)) { ChannelPipeline pipeline = ctx.pipeline(); pipeline.remove(this); pipeline.addLast(new HttpObjectAggregator(512*1024)); BulkRestDataSender brds = new BulkRestDataSender(); pipeline.addLast(brds); cf.complete(brds); } else { cf.completeExceptionally(new YamcsApiException("Cannot continue the bulk load: "+msg.status())); } } } }