1919import java .io .IOException ;
2020import java .nio .ByteBuffer ;
2121import java .nio .charset .StandardCharsets ;
22+ import java .util .ArrayList ;
2223import java .util .Arrays ;
2324import java .util .Collections ;
2425import java .util .HashMap ;
2526import java .util .LinkedHashSet ;
2627import java .util .List ;
2728import java .util .Map ;
2829import java .util .Set ;
30+ import java .util .stream .Collectors ;
2931
3032import com .fasterxml .jackson .core .JsonProcessingException ;
3133import com .fasterxml .jackson .core .type .TypeReference ;
3234import com .fasterxml .jackson .databind .ObjectMapper ;
3335import org .apache .kafka .common .header .Header ;
3436import org .apache .kafka .common .header .Headers ;
3537import org .apache .kafka .common .header .internals .RecordHeader ;
38+ import org .assertj .core .util .Streams ;
3639
3740import org .springframework .messaging .MessageHeaders ;
3841import org .springframework .util .Assert ;
4851 * @author Gary Russell
4952 * @author Artem Bilan
5053 * @author Soby Chacko
54+ * @author Grzegorz Poznachowski
5155 *
5256 * @since 1.3
53- *
5457 */
5558public class DefaultKafkaHeaderMapper extends AbstractKafkaHeaderMapper {
5659
60+ private static final String ITERABLE_HEADER_TYPE_PATTERN = "%s#%s" ;
61+
5762 private static final String JAVA_LANG_STRING = "java.lang.String" ;
5863
5964 private static final Set <String > TRUSTED_ARRAY_TYPES = Set .of (
@@ -96,6 +101,7 @@ public class DefaultKafkaHeaderMapper extends AbstractKafkaHeaderMapper {
96101 * {@code "!id", "!timestamp" and "*"}. In addition, most of the headers in
97102 * {@link KafkaHeaders} are never mapped as headers since they represent data in
98103 * consumer/producer records.
104+ *
99105 * @see #DefaultKafkaHeaderMapper(ObjectMapper)
100106 */
101107 public DefaultKafkaHeaderMapper () {
@@ -110,6 +116,7 @@ public DefaultKafkaHeaderMapper() {
110116 * {@code "!id", "!timestamp" and "*"}. In addition, most of the headers in
111117 * {@link KafkaHeaders} are never mapped as headers since they represent data in
112118 * consumer/producer records.
119+ *
113120 * @param objectMapper the object mapper.
114121 * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
115122 */
@@ -128,6 +135,7 @@ public DefaultKafkaHeaderMapper(ObjectMapper objectMapper) {
128135 * generally should not map the {@code "id" and "timestamp"} headers. Note:
129136 * most of the headers in {@link KafkaHeaders} are ever mapped as headers since they
130137 * represent data in consumer/producer records.
138+ *
131139 * @param patterns the patterns.
132140 * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
133141 */
@@ -143,8 +151,9 @@ public DefaultKafkaHeaderMapper(String... patterns) {
143151 * you generally should not map the {@code "id" and "timestamp"} headers. Note: most
144152 * of the headers in {@link KafkaHeaders} are never mapped as headers since they
145153 * represent data in consumer/producer records.
154+ *
146155 * @param objectMapper the object mapper.
147- * @param patterns the patterns.
156+ * @param patterns the patterns.
148157 * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
149158 */
150159 public DefaultKafkaHeaderMapper (ObjectMapper objectMapper , String ... patterns ) {
@@ -160,6 +169,7 @@ private DefaultKafkaHeaderMapper(boolean outbound, ObjectMapper objectMapper, St
160169
161170 /**
162171 * Create an instance for inbound mapping only with pattern matching.
172+ *
163173 * @param patterns the patterns to match.
164174 * @return the header mapper.
165175 * @since 2.8.8
@@ -170,8 +180,9 @@ public static DefaultKafkaHeaderMapper forInboundOnlyWithMatchers(String... patt
170180
171181 /**
172182 * Create an instance for inbound mapping only with pattern matching.
183+ *
173184 * @param objectMapper the object mapper.
174- * @param patterns the patterns to match.
185+ * @param patterns the patterns to match.
175186 * @return the header mapper.
176187 * @since 2.8.8
177188 */
@@ -181,6 +192,7 @@ public static DefaultKafkaHeaderMapper forInboundOnlyWithMatchers(ObjectMapper o
181192
182193 /**
183194 * Return the object mapper.
195+ *
184196 * @return the mapper.
185197 */
186198 protected ObjectMapper getObjectMapper () {
@@ -189,6 +201,7 @@ protected ObjectMapper getObjectMapper() {
189201
190202 /**
191203 * Provide direct access to the trusted packages set for subclasses.
204+ *
192205 * @return the trusted packages.
193206 * @since 2.2
194207 */
@@ -198,6 +211,7 @@ protected Set<String> getTrustedPackages() {
198211
199212 /**
200213 * Provide direct access to the toString() classes by subclasses.
214+ *
201215 * @return the toString() classes.
202216 * @since 2.2
203217 */
@@ -214,6 +228,7 @@ protected boolean isEncodeStrings() {
214228 * raw String value is converted to a byte array using the configured charset. Set to
215229 * true if a consumer of the outbound record is using Spring for Apache Kafka version
216230 * less than 2.3
231+ *
217232 * @param encodeStrings true to encode (default false).
218233 * @since 2.3
219234 */
@@ -234,6 +249,7 @@ public void setEncodeStrings(boolean encodeStrings) {
234249 * If any of the supplied packages is {@code "*"}, all packages are trusted.
235250 * If a class for a non-trusted package is encountered, the header is returned to the
236251 * application with value of type {@link NonTrustedHeaderType}.
252+ *
237253 * @param packagesToTrust the packages to trust.
238254 */
239255 public void addTrustedPackages (String ... packagesToTrust ) {
@@ -253,6 +269,7 @@ public void addTrustedPackages(String... packagesToTrust) {
253269 /**
254270 * Add class names that the outbound mapper should perform toString() operations on
255271 * before mapping.
272+ *
256273 * @param classNames the class names.
257274 * @since 2.2
258275 */
@@ -264,32 +281,15 @@ public void addToStringClasses(String... classNames) {
264281 public void fromHeaders (MessageHeaders headers , Headers target ) {
265282 final Map <String , String > jsonHeaders = new HashMap <>();
266283 final ObjectMapper headerObjectMapper = getObjectMapper ();
267- headers .forEach ((key , rawValue ) -> {
268- if (matches (key , rawValue )) {
269- Object valueToAdd = headerValueToAddOut (key , rawValue );
270- if (valueToAdd instanceof byte []) {
271- target .add (new RecordHeader (key , (byte []) valueToAdd ));
284+ headers .forEach ((key , value ) -> {
285+ if (matches (key , value )) {
286+ if (value instanceof List <?> values ) {
287+ for (int i = 0 ; i < values .size (); i ++) {
288+ resolveHeader (key , values .get (i ), target , jsonHeaders , i );
289+ }
272290 }
273291 else {
274- try {
275- String className = valueToAdd .getClass ().getName ();
276- boolean encodeToJson = this .encodeStrings ;
277- if (this .toStringClasses .contains (className )) {
278- valueToAdd = valueToAdd .toString ();
279- className = JAVA_LANG_STRING ;
280- encodeToJson = true ;
281- }
282- if (!encodeToJson && valueToAdd instanceof String ) {
283- target .add (new RecordHeader (key , ((String ) valueToAdd ).getBytes (getCharset ())));
284- }
285- else {
286- target .add (new RecordHeader (key , headerObjectMapper .writeValueAsBytes (valueToAdd )));
287- }
288- jsonHeaders .put (key , className );
289- }
290- catch (Exception e ) {
291- logger .error (e , () -> "Could not map " + key + " with type " + rawValue .getClass ().getName ());
292- }
292+ resolveHeader (key , value , target , jsonHeaders , null );
293293 }
294294 }
295295 });
@@ -303,34 +303,84 @@ public void fromHeaders(MessageHeaders headers, Headers target) {
303303 }
304304 }
305305
306- @ Override
307- public void toHeaders (Headers source , final Map <String , Object > headers ) {
308- final Map <String , String > jsonTypes = decodeJsonTypes (source );
309- source .forEach (header -> {
310- String headerName = header .key ();
311- if (headerName .equals (KafkaHeaders .DELIVERY_ATTEMPT ) && matchesForInbound (headerName )) {
312- headers .put (headerName , ByteBuffer .wrap (header .value ()).getInt ());
313- }
314- else if (headerName .equals (KafkaHeaders .LISTENER_INFO ) && matchesForInbound (headerName )) {
315- headers .put (headerName , new String (header .value (), getCharset ()));
316- }
317- else if (headerName .equals (KafkaUtils .KEY_DESERIALIZER_EXCEPTION_HEADER ) ||
318- headerName .equals (KafkaUtils .VALUE_DESERIALIZER_EXCEPTION_HEADER )) {
319- headers .put (headerName , header );
320- }
321- else if (!(headerName .equals (JSON_TYPES )) && matchesForInbound (headerName )) {
322- if (jsonTypes .containsKey (headerName )) {
323- String requestedType = jsonTypes .get (headerName );
324- populateJsonValueHeader (header , requestedType , headers );
306+ private void resolveHeader (String headerName , Object value , Headers target , Map <String , String > jsonHeaders , Integer headerIndex ) {
307+ Object valueToAdd = headerValueToAddOut (headerName , value );
308+ if (valueToAdd instanceof byte [] byteArray ) {
309+ target .add (new RecordHeader (headerName , byteArray ));
310+ }
311+ else {
312+ try {
313+ String className = valueToAdd .getClass ().getName ();
314+ boolean encodeToJson = this .encodeStrings ;
315+ if (this .toStringClasses .contains (className )) {
316+ valueToAdd = valueToAdd .toString ();
317+ className = JAVA_LANG_STRING ;
318+ encodeToJson = true ;
319+ }
320+ if (!encodeToJson && valueToAdd instanceof String stringValue ) {
321+ target .add (new RecordHeader (headerName , stringValue .getBytes (getCharset ())));
325322 }
326323 else {
327- headers . put ( headerName , headerValueToAddIn ( header ));
324+ target . add ( new RecordHeader ( headerName , this . objectMapper . writeValueAsBytes ( valueToAdd ) ));
328325 }
326+ jsonHeaders .put (headerIndex == null ?
327+ headerName :
328+ ITERABLE_HEADER_TYPE_PATTERN .formatted (headerName , headerIndex ), className );
329329 }
330- });
330+ catch (Exception e ) {
331+ logger .error (e , () -> "Could not map " + headerName + " with type " + value .getClass ().getName ());
332+ }
333+ }
334+ }
335+
336+ @ Override
337+ public void toHeaders (Headers source , final Map <String , Object > target ) {
338+ final Map <String , String > jsonTypes = decodeJsonTypes (source );
339+
340+ Streams .stream (source )
341+ .collect (Collectors .groupingBy (Header ::key ))
342+ .forEach ((headerName , headers ) -> {
343+ Header lastHeader = headers .get (headers .size () - 1 );
344+ if (headerName .equals (KafkaUtils .KEY_DESERIALIZER_EXCEPTION_HEADER ) ||
345+ headerName .equals (KafkaUtils .VALUE_DESERIALIZER_EXCEPTION_HEADER )) {
346+ target .put (headerName , lastHeader );
347+ }
348+ else if (headerName .equals (KafkaHeaders .DELIVERY_ATTEMPT ) && matchesForInbound (headerName )) {
349+ target .put (headerName , ByteBuffer .wrap (lastHeader .value ()).getInt ());
350+ }
351+ else if (headerName .equals (KafkaHeaders .LISTENER_INFO ) && matchesForInbound (headerName )) {
352+ target .put (headerName , new String (lastHeader .value (), getCharset ()));
353+ }
354+ else if (!(headerName .equals (JSON_TYPES )) && matchesForInbound (headerName )) {
355+ if (headers .size () == 1 ) {
356+ if (jsonTypes .containsKey (headerName )) {
357+ String requestedType = jsonTypes .get (headerName );
358+ target .put (headerName , resolveJsonValueHeader (headers .get (0 ), requestedType ));
359+ }
360+ else {
361+ target .put (headerName , headerValueToAddIn (headers .get (0 )));
362+ }
363+ }
364+ else {
365+ List <Object > valueList = new ArrayList <>();
366+ for (int i = 0 ; i < headers .size (); i ++) {
367+ var jsonTypeIterableHeader = ITERABLE_HEADER_TYPE_PATTERN .formatted (headerName , i );
368+ if (jsonTypes .containsKey (jsonTypeIterableHeader )) {
369+ String requestedType = jsonTypes .get (jsonTypeIterableHeader );
370+ valueList .add (resolveJsonValueHeader (headers .get (i ), requestedType ));
371+ }
372+ else {
373+ valueList .add (headerValueToAddIn (headers .get (i )));
374+ }
375+ }
376+ Collections .reverse (valueList );
377+ target .put (headerName , valueList );
378+ }
379+ }
380+ });
331381 }
332382
333- private void populateJsonValueHeader (Header header , String requestedType , Map < String , Object > headers ) {
383+ private Object resolveJsonValueHeader (Header header , String requestedType ) {
334384 Class <?> type = Object .class ;
335385 boolean trusted = false ;
336386 try {
@@ -343,22 +393,21 @@ private void populateJsonValueHeader(Header header, String requestedType, Map<St
343393 logger .error (e , () -> "Could not load class for header: " + header .key ());
344394 }
345395 if (String .class .equals (type ) && (header .value ().length == 0 || header .value ()[0 ] != '"' )) {
346- headers . put ( header . key (), new String (header .value (), getCharset () ));
396+ return new String (header .value (), getCharset ());
347397 }
348398 else {
349399 if (trusted ) {
350400 try {
351- Object value = decodeValue (header , type );
352- headers .put (header .key (), value );
401+ return decodeValue (header , type );
353402 }
354403 catch (IOException e ) {
355404 logger .error (e , () ->
356405 "Could not decode json type: " + requestedType + " for key: " + header .key ());
357- headers . put ( header . key (), header .value () );
406+ return header .value ();
358407 }
359408 }
360409 else {
361- headers . put ( header . key (), new NonTrustedHeaderType (header .value (), requestedType ) );
410+ return new NonTrustedHeaderType (header .value (), requestedType );
362411 }
363412 }
364413 }
0 commit comments