/* * JLibs: Common Utilities for Java * Copyright (C) 2009 Santhosh Kumar T <santhosh.tekuri@gmail.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package jlibs.nio.filters; import jlibs.nio.Output; import jlibs.nio.OutputFilter; import jlibs.nio.Reactor; import java.io.IOException; import java.nio.ByteBuffer; import static jlibs.nio.Debugger.IO; import static jlibs.nio.Debugger.println; import static jlibs.nio.http.util.USAscii.CR; import static jlibs.nio.http.util.USAscii.LF; /** * @author Santhosh Kumar Tekuri */ public class ChunkedOutput extends OutputFilter{ private static final int MAX_LEN = Long.toString(Long.MAX_VALUE, 16).length()+2; private static final ByteBuffer CHUNK_END; static{ CHUNK_END = Reactor.current().allocator.allocate(2).put(CR).put(LF); CHUNK_END.flip(); } private static final ByteBuffer LAST_CHUNK; static{ LAST_CHUNK = Reactor.current().allocator.allocate(5); LAST_CHUNK.put((byte)'0').put(CR).put(LF).put(CR).put(LF); LAST_CHUNK.flip(); } private ByteBuffer chunkBegin; private long chunkLength; private ByteBuffer chunkEnd; private ByteBuffer buffers[] = new ByteBuffer[4]; public ChunkedOutput(Output peer){ super(peer); chunkBegin = Reactor.current().allocator.allocate(MAX_LEN); chunkEnd = CHUNK_END.duplicate(); buffers = new ByteBuffer[]{ chunkBegin, null, chunkEnd, LAST_CHUNK.duplicate()}; chunkEnd.position(chunkEnd.limit()); } private static final byte digits[] = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' }; private static final int shift = 4; private static final int mask = (1<<shift)-1; public void startChunk(long length){ if(length>0 && isOpen() && chunkLength==0 && !chunkEnd.hasRemaining()){ if(IO) println("startChunk: "+length); chunkLength = length; chunkBegin.clear(); // convert long to hex: borrowed from Long.toHexString(long) final int mag = Long.SIZE - Long.numberOfLeadingZeros(length); final int chars = Math.max(((mag + (shift-1))/shift), 1); int charPos = chars; do{ chunkBegin.put(--charPos, digits[((int)length)&mask]); length >>>= shift; }while(length!=0 && charPos>0); chunkBegin.position(chars); chunkBegin.put((byte)'\r'); chunkBegin.put((byte)'\n'); chunkBegin.flip(); chunkEnd.clear(); } } private boolean canUserWrite() throws IOException{ ensureOpen(); if(chunkLength==0){ while(chunkEnd.hasRemaining()){ if(peer.write(chunkEnd)==0) return false; } } return true; } @Override public int write(ByteBuffer src) throws IOException{ if(!canUserWrite() || !src.hasRemaining()) return 0; int pos = src.position(); if(chunkLength==0) startChunk(src.remaining()); assert chunkLength!=0; int userLimit = src.limit(); int min = (int)Math.min(chunkLength, src.remaining()); src.limit(min); buffers[1] = src; int offset = chunkBegin.hasRemaining() ? 0 : 1; int length = chunkBegin.hasRemaining() ? 2 : 1; if(min==chunkLength) ++length; int _pos = src.position(); try{ while(length>0){ if(peer.write(buffers, offset, length)==0) break; while(!buffers[offset].hasRemaining()){ ++offset; --length; } } }finally{ src.limit(userLimit); buffers[1] = null; } chunkLength -= src.position()-_pos; return src.position()-pos; } @Override public long write(ByteBuffer[] srcs, int offset, int length) throws IOException{ if(!canUserWrite()) return 0; while(length>0 && !srcs[offset].hasRemaining()){ ++offset; --length; } if(length==0) return 0; if(length==1 || chunkBegin.hasRemaining()) return write(srcs[offset]); if(chunkLength==0){ long remaining = 0; for(int i=0; i<length; i++) remaining += srcs[offset+i].remaining(); startChunk(remaining); ByteBuffer buffers[] = new ByteBuffer[length+2]; buffers[0] = chunkBegin; System.arraycopy(srcs, offset, buffers, 1, length); buffers[buffers.length-1] = chunkEnd; long wrote = peer.write(buffers, 0, buffers.length); if(chunkBegin.hasRemaining()) return 0; wrote = Math.min(wrote-chunkBegin.position(), remaining); chunkLength -= wrote; return wrote; }else{ int len = 0; ByteBuffer candidate = null; int candidateLimit = 0; long remaining = 0; for(; len<length; len++){ remaining += srcs[offset+len].remaining(); if(remaining==chunkLength) break; else if(remaining>chunkLength){ candidate = srcs[offset+len]; candidateLimit = candidate.limit(); candidate.limit((int)(candidateLimit-(remaining-chunkLength))); break; } } try{ long wrote = peer.write(srcs, offset, len); chunkLength -= wrote; return wrote; }finally{ if(candidate!=null) candidate.limit(candidateLimit); } } } @Override protected boolean _flush() throws IOException{ int offset, length; if(chunkBegin.hasRemaining()){ offset = 0; length = 1; }else if(chunkEnd.hasRemaining()){ offset = 2; length = isOpen() ? 1 : 2; }else if(!isOpen()){ offset = 3; length = 1; }else return peer.flush(); while(length>0){ if(peer.write(buffers, offset, length)==0) return false; if(!buffers[offset].hasRemaining()){ ++offset; --length; } } return true; } @Override protected void _close() throws ChunkException{ if(chunkLength!=0) throw new ChunkException(chunkLength+" more bytes needs to be written"); } }