5151 * chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
5252 * chunk-ext-name = token
5353 * chunk-ext-val = token / quoted-string
54+ *
55+ * trailer-part = *( header-field CRLF )
5456 * </pre>
5557 *
5658 * @see ChunkedEncodedInputStream
@@ -60,9 +62,12 @@ public class ChunkedEncodedPublisher implements Publisher<ByteBuffer> {
6062 private static final byte [] CRLF = {'\r' , '\n' };
6163 private static final byte SEMICOLON = ';' ;
6264 private static final byte EQUALS = '=' ;
65+ private static final byte COLON = ':' ;
66+ private static final byte COMMA = ',' ;
6367
6468 private final Publisher <ByteBuffer > wrapped ;
6569 private final List <ChunkExtensionProvider > extensions = new ArrayList <>();
70+ private final List <TrailerProvider > trailers = new ArrayList <>();
6671 private final int chunkSize ;
6772 private ByteBuffer chunkBuffer ;
6873 private final boolean addEmptyTrailingChunk ;
@@ -71,6 +76,7 @@ public ChunkedEncodedPublisher(Builder b) {
7176 this .wrapped = b .publisher ;
7277 this .chunkSize = b .chunkSize ;
7378 this .extensions .addAll (b .extensions );
79+ this .trailers .addAll (b .trailers );
7480 this .addEmptyTrailingChunk = b .addEmptyTrailingChunk ;
7581 }
7682
@@ -125,7 +131,6 @@ public Publisher<ByteBuffer> map(Publisher<ByteBuffer> upstream, Function<? supe
125131 return subscriber -> upstream .subscribe (MappingSubscriber .create (subscriber , mapper ));
126132 }
127133
128- // TODO: Trailing checksum
129134 private ByteBuffer encodeChunk (ByteBuffer byteBuffer ) {
130135 int contentLen = byteBuffer .remaining ();
131136 byte [] chunkSizeHex = Integer .toHexString (contentLen ).getBytes (StandardCharsets .UTF_8 );
@@ -138,24 +143,46 @@ private ByteBuffer encodeChunk(ByteBuffer byteBuffer) {
138143
139144 int extensionsLength = calculateExtensionsLength (chunkExtensions );
140145
141- int encodedLen = chunkSizeHex .length + extensionsLength + CRLF .length + contentLen + CRLF .length ;
146+ boolean isTrailerChunk = contentLen == 0 ;
147+
148+ List <ByteBuffer > trailerData ;
149+ if (isTrailerChunk ) {
150+ trailerData = getTrailerData ();
151+ } else {
152+ trailerData = Collections .emptyList ();
153+ }
154+
155+ int trailerLen = trailerData .stream ().mapToInt (t -> t .remaining () + 2 ).sum ();
156+
157+ int encodedLen = chunkSizeHex .length + extensionsLength + CRLF .length + contentLen + trailerLen + CRLF .length ;
142158
143159 ByteBuffer encoded = ByteBuffer .allocate (encodedLen );
144- encoded .put (chunkSizeHex );
145160
146- chunkExtensions .forEach (p -> {
161+ encoded .put (chunkSizeHex ); // chunk-size
162+ chunkExtensions .forEach (p -> { // chunk-ext
147163 encoded .put (SEMICOLON );
148164 encoded .put (p .left ());
149165 if (p .right () != null && p .right ().length > 0 ) {
150166 encoded .put (EQUALS );
151167 encoded .put (p .right ());
152168 }
153169 });
154-
155- encoded .put (CRLF );
156- encoded .put (byteBuffer );
157170 encoded .put (CRLF );
158171
172+ // chunk-data
173+ if (byteBuffer .hasRemaining ()) {
174+ encoded .put (byteBuffer );
175+ encoded .put (CRLF );
176+ }
177+
178+ if (isTrailerChunk ) {
179+ // trailer-part
180+ trailerData .forEach (t -> {
181+ encoded .put (t );
182+ encoded .put (CRLF );
183+ });
184+ }
185+
159186 encoded .flip ();
160187
161188 return encoded ;
@@ -174,6 +201,41 @@ private int calculateExtensionsLength(List<Pair<byte[], byte[]>> chunkExtensions
174201 }).sum ();
175202 }
176203
204+ private List <ByteBuffer > getTrailerData () {
205+ List <ByteBuffer > data = new ArrayList <>();
206+
207+ for (TrailerProvider provider : trailers ) {
208+ Pair <String , List <String >> trailer = provider .get ();
209+
210+ byte [] key = trailer .left ().getBytes (StandardCharsets .UTF_8 );
211+ List <byte []> values = trailer .right ()
212+ .stream ().map (v -> v .getBytes (StandardCharsets .UTF_8 ))
213+ .collect (Collectors .toList ());
214+ int valuesLen = values .stream ().mapToInt (v -> v .length ).sum ();
215+ // name:value1,value2,..
216+ int size = key .length
217+ + 1 // colon
218+ + valuesLen
219+ + values .size () - 1 ; // commas
220+
221+ ByteBuffer trailerData = ByteBuffer .allocate (size );
222+
223+ trailerData .put (key );
224+ trailerData .put (COLON );
225+
226+ for (int i = 0 ; i < values .size (); ++i ) {
227+ trailerData .put (values .get (i ));
228+ if (i + 1 != values .size ()) {
229+ trailerData .put (COMMA );
230+ }
231+ }
232+
233+ trailerData .flip ();
234+ data .add (trailerData );
235+ }
236+ return data ;
237+ }
238+
177239 private class ChunkingSubscriber extends DelegatingSubscriber <ByteBuffer , Iterable <ByteBuffer >> {
178240 protected ChunkingSubscriber (Subscriber <? super Iterable <ByteBuffer >> subscriber ) {
179241 super (subscriber );
@@ -222,6 +284,7 @@ public static class Builder {
222284 private int chunkSize ;
223285 private boolean addEmptyTrailingChunk ;
224286 private final List <ChunkExtensionProvider > extensions = new ArrayList <>();
287+ private final List <TrailerProvider > trailers = new ArrayList <>();
225288
226289 public Builder publisher (Publisher <ByteBuffer > publisher ) {
227290 this .publisher = publisher ;
@@ -243,6 +306,11 @@ public Builder addExtension(ChunkExtensionProvider extension) {
243306 return this ;
244307 }
245308
309+ public Builder addTrailer (TrailerProvider trailerProvider ) {
310+ this .trailers .add (trailerProvider );
311+ return this ;
312+ }
313+
246314 public ChunkedEncodedPublisher build () {
247315 return new ChunkedEncodedPublisher (this );
248316 }
0 commit comments