Skip to content

Commit 9cec79c

Browse files
authored
BE: Messages: Format non-string headers (#1077)
1 parent 80de2e7 commit 9cec79c

File tree

3 files changed

+195
-1
lines changed

3 files changed

+195
-1
lines changed

api/src/main/java/io/kafbat/ui/serdes/ConsumerRecordDeserializer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.kafbat.ui.model.TopicMessageDTO;
44
import io.kafbat.ui.model.TopicMessageDTO.TimestampTypeEnum;
55
import io.kafbat.ui.serde.api.Serde;
6+
import io.kafbat.ui.util.ContentUtils;
67
import java.time.Instant;
78
import java.time.OffsetDateTime;
89
import java.time.ZoneId;
@@ -68,7 +69,7 @@ private void fillHeaders(TopicMessageDTO message, ConsumerRecord<Bytes, Bytes> r
6869
.forEachRemaining(header ->
6970
headers.put(
7071
header.key(),
71-
header.value() != null ? new String(header.value()) : null
72+
ContentUtils.convertToString(header.value())
7273
));
7374
message.setHeaders(headers);
7475
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package io.kafbat.ui.util;
2+
3+
import java.nio.ByteBuffer;
4+
import java.nio.CharBuffer;
5+
import java.nio.charset.CharsetDecoder;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.List;
8+
import java.util.regex.Pattern;
9+
10+
/**
11+
* Inspired by: https://github.com/tchiotludo/akhq/blob/dev/src/main/java/org/akhq/utils/ContentUtils.java
12+
*/
13+
public class ContentUtils {
14+
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
15+
16+
private static final CharsetDecoder UTF8_DECODER = StandardCharsets.UTF_8.newDecoder();
17+
18+
private ContentUtils() {
19+
}
20+
21+
/**
22+
* Detects if bytes contain a UTF-8 string or something else.
23+
* @param value the bytes to test for a UTF-8 encoded {@code java.lang.String} value
24+
* @return true, if the byte[] contains a UTF-8 encode {@code java.lang.String}
25+
*/
26+
public static boolean isValidUtf8(byte[] value) {
27+
// Any data exceeding 10KB will be treated as a string.
28+
if (value.length > 10_000) {
29+
return true;
30+
}
31+
try {
32+
CharBuffer decode = UTF8_DECODER.decode(ByteBuffer.wrap(value));
33+
return decode.chars().allMatch(ContentUtils::isValidUtf8);
34+
} catch (Exception e) {
35+
return false;
36+
}
37+
}
38+
39+
public static boolean isValidUtf8(int c) {
40+
// SKIP NULL Symbols
41+
if (c == 0) {
42+
return false;
43+
}
44+
// Well known symbols
45+
if (Character.isAlphabetic(c)
46+
|| Character.isDigit(c)
47+
|| Character.isWhitespace(c)
48+
|| Character.isEmoji(c)
49+
) {
50+
return true;
51+
}
52+
// We could read only whitespace controls like
53+
return !Character.isISOControl(c);
54+
}
55+
56+
/**
57+
* Converts bytes to long.
58+
*
59+
* @param value the bytes to convert in to a long
60+
* @return the long build from the given bytes
61+
*/
62+
public static Long asLong(byte[] value) {
63+
return value != null ? ByteBuffer.wrap(value).getLong() : null;
64+
}
65+
66+
/**
67+
* Converts the given bytes to {@code int}.
68+
*
69+
* @param value the bytes to convert into a {@code int}
70+
* @return the {@code int} build from the given bytes
71+
*/
72+
public static Integer asInt(byte[] value) {
73+
return value != null ? ByteBuffer.wrap(value).getInt() : null;
74+
}
75+
76+
/**
77+
* Converts the given bytes to {@code short}.
78+
*
79+
* @param value the bytes to convert into a {@code short}
80+
* @return the {@code short} build from the given bytes
81+
*/
82+
public static Short asShort(byte[] value) {
83+
return value != null ? ByteBuffer.wrap(value).getShort() : null;
84+
}
85+
86+
/**
87+
* Converts the given bytes either into a {@code java.lang.string}, {@code int},
88+
* {@code long} or {@code short} depending on the content it contains.
89+
* @param value the bytes to convert
90+
* @return the value as an {@code java.lang.string}, {@code int}, {@code long} or {@code short}
91+
*/
92+
public static String convertToString(byte[] value) {
93+
String valueAsString = null;
94+
95+
if (value != null) {
96+
try {
97+
if (ContentUtils.isValidUtf8(value)) {
98+
valueAsString = new String(value);
99+
} else {
100+
if (value.length == Long.BYTES) {
101+
valueAsString = String.valueOf(ContentUtils.asLong(value));
102+
} else if (value.length == Integer.BYTES) {
103+
valueAsString = String.valueOf(ContentUtils.asInt(value));
104+
} else if (value.length == Short.BYTES) {
105+
valueAsString = String.valueOf(ContentUtils.asShort(value));
106+
} else {
107+
valueAsString = bytesToHex(value);
108+
}
109+
}
110+
} catch (Exception ex) {
111+
// Show the header as hexadecimal string
112+
valueAsString = bytesToHex(value);
113+
}
114+
}
115+
return valueAsString;
116+
}
117+
118+
// https://stackoverflow.com/questions/9655181/java-convert-a-byte-array-to-a-hex-string
119+
public static String bytesToHex(byte[] bytes) {
120+
byte[] hexChars = new byte[bytes.length * 2];
121+
for (int j = 0; j < bytes.length; j++) {
122+
int v = bytes[j] & 0xFF;
123+
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
124+
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
125+
}
126+
return new String(hexChars, StandardCharsets.UTF_8);
127+
}
128+
129+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package io.kafbat.ui.util;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.nio.ByteBuffer;
6+
import java.nio.charset.StandardCharsets;
7+
import org.apache.commons.lang3.RandomStringUtils;
8+
import org.junit.jupiter.api.Test;
9+
10+
public class ContentUtilsTest {
11+
12+
private static byte[] toBytes(Short value) {
13+
ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES);
14+
buffer.putShort(value);
15+
return buffer.array();
16+
}
17+
18+
private static byte[] toBytes(Integer value) {
19+
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
20+
buffer.putInt(value);
21+
return buffer.array();
22+
}
23+
24+
private static byte[] toBytes(Long value) {
25+
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
26+
buffer.putLong(value);
27+
return buffer.array();
28+
}
29+
30+
@Test
31+
void testHeaderValueStringUtf8() {
32+
String testValue = "Test";
33+
34+
assertEquals(testValue, ContentUtils.convertToString(testValue.getBytes(StandardCharsets.UTF_8)));
35+
}
36+
37+
@Test
38+
void testHeaderValueInteger() {
39+
int testValue = 1;
40+
assertEquals(String.valueOf(testValue), ContentUtils.convertToString(toBytes(testValue)));
41+
}
42+
43+
@Test
44+
void testHeaderValueLong() {
45+
long testValue = 111L;
46+
47+
assertEquals(String.valueOf(testValue), ContentUtils.convertToString(toBytes(testValue)));
48+
}
49+
50+
@Test
51+
void testHeaderValueShort() {
52+
short testValue = 10;
53+
54+
assertEquals(String.valueOf(testValue), ContentUtils.convertToString(toBytes(testValue)));
55+
}
56+
57+
@Test
58+
void testHeaderValueLongStringUtf8() {
59+
String testValue = RandomStringUtils.random(10000, true, false);
60+
61+
assertEquals(testValue, ContentUtils.convertToString(testValue.getBytes(StandardCharsets.UTF_8)));
62+
}
63+
64+
}

0 commit comments

Comments
 (0)