Skip to content

Commit 91629c6

Browse files
committed
ISSUE-919 Best efforts to convert header into string
1 parent b241abe commit 91629c6

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package io.kafbat.ui.util;
2+
3+
import java.nio.ByteBuffer;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.regex.Pattern;
6+
7+
/**
8+
* Inspired by: https://github.com/tchiotludo/akhq/blob/dev/src/main/java/org/akhq/utils/ContentUtils.java
9+
*/
10+
public class ContentUtils {
11+
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
12+
13+
private static final Pattern UTF8_PATTERN = Pattern.compile("\\A(\n"
14+
+ " [\\x09\\x0A\\x0D\\x20-\\x7E] # ASCII\\n"
15+
+ "| [\\xC2-\\xDF][\\x80-\\xBF] # non-overlong 2-byte\n"
16+
+ "| \\xE0[\\xA0-\\xBF][\\x80-\\xBF] # excluding overlongs\n"
17+
+ "| [\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2} # straight 3-byte\n"
18+
+ "| \\xED[\\x80-\\x9F][\\x80-\\xBF] # excluding surrogates\n"
19+
+ "| \\xF0[\\x90-\\xBF][\\x80-\\xBF]{2} # planes 1-3\n"
20+
+ "| [\\xF1-\\xF3][\\x80-\\xBF]{3} # planes 4-15\n"
21+
+ "| \\xF4[\\x80-\\x8F][\\x80-\\xBF]{2} # plane 16\n"
22+
+ ")*\\z", Pattern.COMMENTS);
23+
24+
private ContentUtils() {
25+
}
26+
27+
/**
28+
* Detects if bytes contain a UTF-8 string or something else
29+
* Source: https://stackoverflow.com/questions/1193200/how-can-i-check-whether-a-byte-array-contains-a-unicode-string-in-java
30+
* @param value the bytes to test for a UTF-8 encoded {@code java.lang.String} value
31+
* @return true, if the byte[] contains a UTF-8 encode {@code java.lang.String}
32+
*/
33+
public static boolean isValidUtf8(byte[] value) {
34+
//If the array is too long, it throws a StackOverflowError due to the regex, so we assume it is a String.
35+
if (value.length <= 1000) {
36+
String phonyString = new String(value, StandardCharsets.ISO_8859_1);
37+
return UTF8_PATTERN.matcher(phonyString).matches();
38+
}
39+
return true;
40+
}
41+
42+
/**
43+
* Converts bytes to long.
44+
*
45+
* @param value the bytes to convert in to a long
46+
* @return the long build from the given bytes
47+
*/
48+
public static Long asLong(byte[] value) {
49+
return value != null ? ByteBuffer.wrap(value).getLong() : null;
50+
}
51+
52+
/**
53+
* Converts the given bytes to {@code int}.
54+
*
55+
* @param value the bytes to convert into a {@code int}
56+
* @return the {@code int} build from the given bytes
57+
*/
58+
public static Integer asInt(byte[] value) {
59+
return value != null ? ByteBuffer.wrap(value).getInt() : null;
60+
}
61+
62+
/**
63+
* Converts the given bytes to {@code short}.
64+
*
65+
* @param value the bytes to convert into a {@code short}
66+
* @return the {@code short} build from the given bytes
67+
*/
68+
public static Short asShort(byte[] value) {
69+
return value != null ? ByteBuffer.wrap(value).getShort() : null;
70+
}
71+
72+
/**
73+
* Converts the given bytes either into a {@code java.lang.string}, {@code int},
74+
* {@code long} or {@code short} depending on the content it contains.
75+
* @param value the bytes to convert
76+
* @return the value as an {@code java.lang.string}, {@code int}, {@code long} or {@code short}
77+
*/
78+
public static String convertToString(byte[] value) {
79+
String valueAsString = null;
80+
81+
if (value != null) {
82+
try {
83+
if (ContentUtils.isValidUtf8(value)) {
84+
valueAsString = new String(value);
85+
} else {
86+
try {
87+
valueAsString = String.valueOf(ContentUtils.asLong(value));
88+
} catch (Exception e) {
89+
try {
90+
valueAsString = String.valueOf(ContentUtils.asInt(value));
91+
} catch (Exception ex) {
92+
valueAsString = String.valueOf(ContentUtils.asShort(value));
93+
}
94+
}
95+
}
96+
} catch (Exception ex) {
97+
// Show the header as hexadecimal string
98+
valueAsString = bytesToHex(value);
99+
}
100+
}
101+
return valueAsString;
102+
}
103+
104+
// https://stackoverflow.com/questions/9655181/java-convert-a-byte-array-to-a-hex-string
105+
public static String bytesToHex(byte[] bytes) {
106+
byte[] hexChars = new byte[bytes.length * 2];
107+
for (int j = 0; j < bytes.length; j++) {
108+
int v = bytes[j] & 0xFF;
109+
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
110+
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
111+
}
112+
return new String(hexChars, StandardCharsets.UTF_8);
113+
}
114+
115+
}
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)