51
51
* chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
52
52
* chunk-ext-name = token
53
53
* chunk-ext-val = token / quoted-string
54
+ *
55
+ * trailer-part = *( header-field CRLF )
54
56
* </pre>
55
57
*
56
58
* @see ChunkedEncodedInputStream
@@ -60,9 +62,12 @@ public class ChunkedEncodedPublisher implements Publisher<ByteBuffer> {
60
62
private static final byte [] CRLF = {'\r' , '\n' };
61
63
private static final byte SEMICOLON = ';' ;
62
64
private static final byte EQUALS = '=' ;
65
+ private static final byte COLON = ':' ;
66
+ private static final byte COMMA = ',' ;
63
67
64
68
private final Publisher <ByteBuffer > wrapped ;
65
69
private final List <ChunkExtensionProvider > extensions = new ArrayList <>();
70
+ private final List <TrailerProvider > trailers = new ArrayList <>();
66
71
private final int chunkSize ;
67
72
private ByteBuffer chunkBuffer ;
68
73
private final boolean addEmptyTrailingChunk ;
@@ -71,6 +76,7 @@ public ChunkedEncodedPublisher(Builder b) {
71
76
this .wrapped = b .publisher ;
72
77
this .chunkSize = b .chunkSize ;
73
78
this .extensions .addAll (b .extensions );
79
+ this .trailers .addAll (b .trailers );
74
80
this .addEmptyTrailingChunk = b .addEmptyTrailingChunk ;
75
81
}
76
82
@@ -125,10 +131,9 @@ public Publisher<ByteBuffer> map(Publisher<ByteBuffer> upstream, Function<? supe
125
131
return subscriber -> upstream .subscribe (MappingSubscriber .create (subscriber , mapper ));
126
132
}
127
133
128
- // TODO: Trailing checksum
129
134
private ByteBuffer encodeChunk (ByteBuffer byteBuffer ) {
130
135
int contentLen = byteBuffer .remaining ();
131
- byte [] chunkSizeHex = Integer .toHexString (contentLen ).getBytes (StandardCharsets .UTF_8 );
136
+ byte [] chunkSizeHex = Integer .toHexString (byteBuffer . remaining () ).getBytes (StandardCharsets .UTF_8 );
132
137
133
138
List <Pair <byte [], byte []>> chunkExtensions = this .extensions .stream ()
134
139
.map (e -> {
@@ -138,24 +143,54 @@ private ByteBuffer encodeChunk(ByteBuffer byteBuffer) {
138
143
139
144
int extensionsLength = calculateExtensionsLength (chunkExtensions );
140
145
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 ()
156
+ // + 2 for each CRLF that ends the header-field
157
+ .mapToInt (t -> t .remaining () + 2 )
158
+ .sum ();
159
+
160
+ int encodedLen = chunkSizeHex .length + extensionsLength + CRLF .length + contentLen + trailerLen + CRLF .length ;
161
+
162
+ if (isTrailerChunk ) {
163
+ encodedLen += CRLF .length ;
164
+ }
142
165
143
166
ByteBuffer encoded = ByteBuffer .allocate (encodedLen );
144
- encoded .put (chunkSizeHex );
145
167
146
- chunkExtensions .forEach (p -> {
168
+ encoded .put (chunkSizeHex ); // chunk-size
169
+ chunkExtensions .forEach (p -> { // chunk-ext
147
170
encoded .put (SEMICOLON );
148
171
encoded .put (p .left ());
149
172
if (p .right () != null && p .right ().length > 0 ) {
150
173
encoded .put (EQUALS );
151
174
encoded .put (p .right ());
152
175
}
153
176
});
154
-
155
- encoded .put (CRLF );
156
- encoded .put (byteBuffer );
157
177
encoded .put (CRLF );
158
178
179
+ // chunk-data
180
+ if (byteBuffer .hasRemaining ()) {
181
+ encoded .put (byteBuffer );
182
+ encoded .put (CRLF );
183
+ }
184
+
185
+ if (isTrailerChunk ) {
186
+ // trailer-part
187
+ trailerData .forEach (t -> {
188
+ encoded .put (t );
189
+ encoded .put (CRLF );
190
+ });
191
+ encoded .put (CRLF );
192
+ }
193
+
159
194
encoded .flip ();
160
195
161
196
return encoded ;
@@ -174,6 +209,46 @@ private int calculateExtensionsLength(List<Pair<byte[], byte[]>> chunkExtensions
174
209
}).sum ();
175
210
}
176
211
212
+ private List <ByteBuffer > getTrailerData () {
213
+ List <ByteBuffer > data = new ArrayList <>();
214
+
215
+ for (TrailerProvider provider : trailers ) {
216
+ Pair <String , List <String >> trailer = provider .get ();
217
+
218
+ byte [] key = trailer .left ().getBytes (StandardCharsets .UTF_8 );
219
+ List <byte []> values = trailer .right ()
220
+ .stream ().map (v -> v .getBytes (StandardCharsets .UTF_8 ))
221
+ .collect (Collectors .toList ());
222
+
223
+ if (values .isEmpty ()) {
224
+ throw new RuntimeException (String .format ("Trailing header '%s' has no values" , trailer .left ()));
225
+ }
226
+
227
+ int valuesLen = values .stream ().mapToInt (v -> v .length ).sum ();
228
+ // name:value1,value2,..
229
+ int size = key .length
230
+ + 1 // colon
231
+ + valuesLen
232
+ + values .size () - 1 ; // commas
233
+
234
+ ByteBuffer trailerData = ByteBuffer .allocate (size );
235
+
236
+ trailerData .put (key );
237
+ trailerData .put (COLON );
238
+
239
+ for (int i = 0 ; i < values .size (); ++i ) {
240
+ trailerData .put (values .get (i ));
241
+ if (i + 1 != values .size ()) {
242
+ trailerData .put (COMMA );
243
+ }
244
+ }
245
+
246
+ trailerData .flip ();
247
+ data .add (trailerData );
248
+ }
249
+ return data ;
250
+ }
251
+
177
252
private class ChunkingSubscriber extends DelegatingSubscriber <ByteBuffer , Iterable <ByteBuffer >> {
178
253
protected ChunkingSubscriber (Subscriber <? super Iterable <ByteBuffer >> subscriber ) {
179
254
super (subscriber );
@@ -222,6 +297,7 @@ public static class Builder {
222
297
private int chunkSize ;
223
298
private boolean addEmptyTrailingChunk ;
224
299
private final List <ChunkExtensionProvider > extensions = new ArrayList <>();
300
+ private final List <TrailerProvider > trailers = new ArrayList <>();
225
301
226
302
public Builder publisher (Publisher <ByteBuffer > publisher ) {
227
303
this .publisher = publisher ;
@@ -243,6 +319,11 @@ public Builder addExtension(ChunkExtensionProvider extension) {
243
319
return this ;
244
320
}
245
321
322
+ public Builder addTrailer (TrailerProvider trailerProvider ) {
323
+ this .trailers .add (trailerProvider );
324
+ return this ;
325
+ }
326
+
246
327
public ChunkedEncodedPublisher build () {
247
328
return new ChunkedEncodedPublisher (this );
248
329
}
0 commit comments