Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.navercorp.pinpoint.bootstrap.plugin.jdbc.StringMaker;
import com.navercorp.pinpoint.bootstrap.plugin.jdbc.UnKnownDatabaseInfo;
import com.navercorp.pinpoint.common.trace.ServiceType;
import com.navercorp.pinpoint.common.util.KeyValueTokenizer;
import com.navercorp.pinpoint.common.util.StringUtils;

import java.util.Arrays;
Expand Down Expand Up @@ -73,10 +74,10 @@ private DatabaseInfo parseURL(String url) {
String host = DEFAULT_HOST;
String port = DEFAULT_PORT;
if (StringUtils.hasText(hostPort)) {
String[] hostPortInfo = hostPort.split(":", 2);
host = hostPortInfo[0];
if (hostPortInfo.length > 1) {
port = hostPortInfo[1];
KeyValueTokenizer.KeyValue hostPortInfo = KeyValueTokenizer.tokenize(hostPort, ":");
host = hostPortInfo.getKey();
if (!hostPortInfo.getValue().isEmpty()) {
port = hostPortInfo.getValue();
Comment on lines +78 to +80
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check after calling KeyValueTokenizer.tokenize(). The tokenize method returns null when the delimiter is not found in the text. Without a null check, this will throw a NullPointerException when calling getKey() or getValue() on a null result. Add a null check similar to the one used in RedisKVPubChannelProvider at line 40.

Suggested change
host = hostPortInfo.getKey();
if (!hostPortInfo.getValue().isEmpty()) {
port = hostPortInfo.getValue();
if (hostPortInfo != null) {
host = hostPortInfo.getKey();
if (!hostPortInfo.getValue().isEmpty()) {
port = hostPortInfo.getValue();
}
} else {
host = hostPort;

Copilot uses AI. Check for mistakes.
}
}

Expand Down Expand Up @@ -122,11 +123,11 @@ private Map<String, String> parseQuery(String propString) {
if (!StringUtils.hasText(v)) {
continue;
}
String[] kv = v.split("=", 2);
if (kv.length > 1) {
map.put(kv[0], kv[1]);
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(v, "=");
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check after calling KeyValueTokenizer.tokenize(). The tokenize method returns null when the delimiter is not found in the text. In the parseQuery method, if a query parameter doesn't contain an "=" sign, tokenize will return null, causing a NullPointerException when calling getKey() or getValue(). Add a null check or handle the case where the delimiter is absent.

Suggested change
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(v, "=");
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(v, "=");
if (keyValue == null) {
continue;
}

Copilot uses AI. Check for mistakes.
if (keyValue != null) {
map.put(keyValue.getKey(), keyValue.getValue());
} else {
map.put(kv[0], StringUtils.EMPTY_STRING);
logger.info("Parse failed " + v) ;
}
}
return map;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.navercorp.pinpoint.profiler.plugin;

import com.navercorp.pinpoint.common.util.KeyValueTokenizer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand Down Expand Up @@ -53,10 +54,10 @@ private boolean isLoadedClass(String classname, ClassLoader cl) {

private void parseRequirementList(List<String> packageRequirementList, List<String> packageList, List<String> requirementList) {
for (String packageWithRequirement : packageRequirementList) {
String[] split = packageWithRequirement.split(":", 2);
if (split.length == 2) {
packageList.add(split[0]);
requirementList.add(split[1]);
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(packageWithRequirement, ":");
if (keyValue != null) {
packageList.add(keyValue.getKey());
requirementList.add(keyValue.getValue());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.navercorp.pinpoint.collector.grpc.channelz.service;

import com.navercorp.pinpoint.common.util.KeyValueTokenizer;

import javax.annotation.Nullable;
import java.util.Collection;

Expand Down Expand Up @@ -48,16 +50,16 @@ private SocketEntry(String remoteAddr, Integer localPort, long socketId) {
* @return entry of socket index
*/
public static SocketEntry compose(Object remote, Object local, long socketId) {
String remoteAddr = split(remote)[0];
String localPort = split(local)[1];
String remoteAddr = split(remote).getKey();
String localPort = split(local).getValue();
Comment on lines +53 to +54
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException when KeyValueTokenizer.tokenize() returns null. The split() method can return null when obj is null or when the tokenize method doesn't find the delimiter. In the compose method at lines 53-54, calling getKey() or getValue() on a null result will throw a NullPointerException. Add null checks after the split() calls.

Suggested change
String remoteAddr = split(remote).getKey();
String localPort = split(local).getValue();
KeyValueTokenizer.KeyValue remoteKeyValue = split(remote);
KeyValueTokenizer.KeyValue localKeyValue = split(local);
if (remoteKeyValue == null || localKeyValue == null) {
throw new IllegalArgumentException("Invalid remote or local address: remote=" + remote + ", local=" + local);
}
String remoteAddr = remoteKeyValue.getKey();
String localPort = localKeyValue.getValue();

Copilot uses AI. Check for mistakes.
return new SocketEntry(remoteAddr.substring(1), parse(localPort), socketId);
}

private static String[] split(Object obj) {
private static KeyValueTokenizer.KeyValue split(Object obj) {
if (obj == null) {
return null;
}
return obj.toString().split(":", 2);
return KeyValueTokenizer.tokenize(obj.toString(), ":");
}

private static Integer parse(String str) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.navercorp.pinpoint.common.util;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class KeyValueTokenizerTest {

@Test
void tokenize() {
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize("key=value", "=");
assertEquals("key", keyValue.getKey());
assertEquals("value", keyValue.getValue());
}

@Test
void tokenize2() {
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize("key==value", "==");
assertEquals("key", keyValue.getKey());
assertEquals("value", keyValue.getValue());
}

@Test
void tokenize_emptyValue() {
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize("key=", "=");
assertEquals("key", keyValue.getKey());
assertEquals("", keyValue.getValue());
}

@Test
void tokenize_emptyKeyValue() {
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize("=", "=");
assertEquals("", keyValue.getKey());
assertEquals("", keyValue.getValue());
}

@Test
void tokenize_empty() {
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize("", "=");
Assertions.assertNull(keyValue);
}
}
Comment on lines +8 to +43
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The KeyValueTokenizerTest is missing a test case for when a null text parameter is passed to the tokenize method. According to line 32 of KeyValueTokenizer.java, the method will throw a NullPointerException if text is null. A test should be added to document this expected behavior, either verifying that a NullPointerException is thrown or that the method handles null gracefully.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.navercorp.pinpoint.common.util;
import java.util.Objects;

public class KeyValueTokenizer {

public static final TokenFactory<KeyValue> KEY_VALUE_FACTORY = new TokenFactory<KeyValue>() {
public KeyValue accept(String key, String value) {
return new KeyValue(key, value);
}
};

public static final TokenFactory<KeyValue> KEY_VALUE_TRIM_FACTORY = new TokenFactory<KeyValue>() {
public KeyValue accept(String key, String value) {
return new KeyValue(key.trim(), value.trim());
}
};

public static KeyValue tokenize(String text, String delimiter) {
return tokenize(text, delimiter, KEY_VALUE_FACTORY);
}

/**
* Tokenizes the given text into a key and value using the specified delimiter and token factory.
*
* @param text the text to tokenize
* @param delimiter the delimiter separating key and value
* @param factory the factory used to create the token
* @param <T> the type of token to return
* @return the parsed token, or {@code null} if the delimiter is not found in the text
Comment on lines +25 to +29
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The javadoc comment on line 29 states that the method returns "null if the delimiter is not found in the text", but this important behavior is not consistently documented or handled by all callers. Consider adding an @throws NullPointerException annotation for the text parameter, and potentially adding a note in the javadoc that callers should check for null return values.

Suggested change
* @param text the text to tokenize
* @param delimiter the delimiter separating key and value
* @param factory the factory used to create the token
* @param <T> the type of token to return
* @return the parsed token, or {@code null} if the delimiter is not found in the text
* @param text the text to tokenize; must not be {@code null}
* @param delimiter the delimiter separating key and value
* @param factory the factory used to create the token
* @param <T> the type of token to return
* @return the parsed token, or {@code null} if the delimiter is not found in the text; callers should
* check for a {@code null} return value
* @throws NullPointerException if {@code text} is {@code null}

Copilot uses AI. Check for mistakes.
*/
public static <T> T tokenize(String text, String delimiter, TokenFactory<T> factory) {
Objects.requireNonNull(text, "text");

final int delimiterIndex = text.indexOf(delimiter);
if (delimiterIndex == -1) {
return null;
}

final String key = text.substring(0, delimiterIndex);

final int delimiterLength = delimiter.length();
if (delimiterIndex == text.length() - delimiterLength) {
return factory.accept(key, "");
}
String value = text.substring(delimiterIndex + delimiterLength);
return factory.accept(key, value);
}


public interface TokenFactory<V> {
V accept(String key, String value);
}

public static class KeyValue {
private final String key;
private final String value;

public KeyValue(String key, String value) {
this.key = Objects.requireNonNull(key, "key");
this.value = Objects.requireNonNull(value, "value");
}

public String getKey() {
return key;
}

public String getValue() {
return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.common.primitives.Ints;
import com.navercorp.pinpoint.common.server.util.StringPrecondition;
import com.navercorp.pinpoint.common.timeseries.window.TimePrecision;
import com.navercorp.pinpoint.common.util.KeyValueTokenizer;
import com.navercorp.pinpoint.common.util.StringUtils;
import com.navercorp.pinpoint.metric.web.util.QueryParameter;

Expand Down Expand Up @@ -197,8 +198,8 @@ public Builder addAllFilters(Collection<String> strings) {
return self();
}
for (String string : strings) {
String[] tag = string.split(":", 2);
filterByAttributes.put(tag[0], tag[1]);
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(string, ":");
filterByAttributes.put(keyValue.getKey(), keyValue.getValue());
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check after calling KeyValueTokenizer.tokenize(). The tokenize method returns null when the delimiter is not found. If a filter string doesn't contain a ":" delimiter, this will throw a NullPointerException when calling getKey() or getValue() on the null result. Add validation to ensure the keyValue is not null before using it.

Suggested change
filterByAttributes.put(keyValue.getKey(), keyValue.getValue());
if (keyValue != null) {
filterByAttributes.put(keyValue.getKey(), keyValue.getValue());
}

Copilot uses AI. Check for mistakes.
}
return self();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@


import com.navercorp.pinpoint.common.util.CollectionUtils;
import com.navercorp.pinpoint.common.util.KeyValueTokenizer;
import com.navercorp.pinpoint.metric.common.model.Tag;
import org.apache.commons.lang3.StringUtils;

Expand All @@ -33,7 +34,14 @@
public class TagUtils {

private static final Pattern MULTI_VALUE_FIELD_PATTERN = Pattern.compile("[\\[\\]\"]");
private static final Pattern JSON_TAG_STRING_PATTERN = Pattern.compile("[{}\"]");
private static final String JSON_TAG_STRING = "{}\"";

public static final KeyValueTokenizer.TokenFactory<Tag> TAG_FACTORY = new KeyValueTokenizer.TokenFactory<>() {
@Override
public Tag accept(String key, String value) {
return new Tag(key, value);
}
};

private TagUtils() {
}
Expand Down Expand Up @@ -69,16 +77,7 @@ public static List<Tag> parseTags(String tagStrings) {

public static Tag parseTag(String tagString) {
Objects.requireNonNull(tagString, "tagString");

String[] tag = StringUtils.split(tagString, ":", 2);

if (tag.length == 1) {
return new Tag(tag[0], "");
} else if (tag.length == 2) {
return new Tag(tag[0], tag[1]);
} else {
throw new IllegalArgumentException("tagString:" + tagString);
}
return KeyValueTokenizer.tokenize(tagString, ":", TAG_FACTORY);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check after calling KeyValueTokenizer.tokenize(). The tokenize method can return null when the delimiter is not found. The parseTag method should handle this case, either by throwing an appropriate exception or by returning a default Tag. This could cause a NullPointerException to be thrown from this method.

Suggested change
return KeyValueTokenizer.tokenize(tagString, ":", TAG_FACTORY);
Tag tag = KeyValueTokenizer.tokenize(tagString, ":", TAG_FACTORY);
if (tag == null) {
throw new IllegalArgumentException("Invalid tag format, expected 'key:value' but was: " + tagString);
}
return tag;

Copilot uses AI. Check for mistakes.
}

private static String[] parseMultiValueFieldList(String string) {
Expand All @@ -87,10 +86,11 @@ private static String[] parseMultiValueFieldList(String string) {
}

public static String toTagString(String jsonTagString) {
if (jsonTagString.equals("{}")) {
if ("{}".equals(jsonTagString)) {
return "";
}
return JSON_TAG_STRING_PATTERN.matcher(jsonTagString).replaceAll("");

return org.springframework.util.StringUtils.deleteAny(jsonTagString, JSON_TAG_STRING);
}

public static String toTagString(List<Tag> tagList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public void parseTagTest() {
Assertions.assertEquals(tagList, result);
}

@Test
public void parseTagTest_emptyValue() {
Tag tag = TagUtils.parseTag("A:");

Assertions.assertEquals("A", tag.getName());
Assertions.assertEquals("", tag.getValue());
}
Comment on lines +44 to +50
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite is missing a test case for when parseTag is called with a string that doesn't contain a colon delimiter. This is particularly important since the refactoring changed the behavior from the old implementation (which used StringUtils.split and handled missing delimiters by treating the entire string as the key with an empty value) to KeyValueTokenizer.tokenize (which returns null when the delimiter is not found). A test should verify the expected behavior in this case.

Copilot uses AI. Check for mistakes.

@Test
public void parseTagsListTest() {
List<Tag> tagList = List.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.navercorp.pinpoint.channel.PubChannel;
import com.navercorp.pinpoint.channel.PubChannelProvider;
import com.navercorp.pinpoint.common.util.KeyValueTokenizer;
import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;
Expand All @@ -35,12 +36,12 @@ class RedisKVPubChannelProvider implements PubChannelProvider {

@Override
public PubChannel getPubChannel(String key) {
String[] words = key.split(":", 2);
if (words.length != 2) {
throw new IllegalArgumentException("the key must contain expire duration");
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(key, ":");
if (keyValue == null) {
throw new IllegalArgumentException("the key must contain ':' key:" + key);
}
Duration expire = Duration.parse(words[0]);
return new RedisKVPubChannel(this.template, expire.toMillis(), words[1]);
Duration expire = Duration.parse(keyValue.getKey());
return new RedisKVPubChannel(this.template, expire.toMillis(), keyValue.getValue());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.navercorp.pinpoint.channel.SubChannel;
import com.navercorp.pinpoint.channel.SubChannelProvider;
import com.navercorp.pinpoint.common.util.KeyValueTokenizer;
import org.springframework.data.redis.core.RedisTemplate;
import reactor.core.scheduler.Scheduler;

Expand All @@ -38,12 +39,12 @@ class RedisKVSubChannelProvider implements SubChannelProvider {

@Override
public SubChannel getSubChannel(String key) {
String[] words = key.split(":", 2);
if (words.length != 2) {
throw new IllegalArgumentException("the key must contain period");
KeyValueTokenizer.KeyValue keyValue = KeyValueTokenizer.tokenize(key, ":");
if (keyValue == null) {
throw new IllegalArgumentException("the key must contain ':' key:" + key);
}
Duration period = Duration.parse(words[0]);
return new RedisKVSubChannel(this.template, this.scheduler, period, words[1]);
Duration period = Duration.parse(keyValue.getKey());
return new RedisKVSubChannel(this.template, this.scheduler, period, keyValue.getValue());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ private Application newApplication(String nodeKey) {
if (!NODE_KEY_VALIDATION_PATTERN.matcher(nodeKey).matches()) {
throw new IllegalArgumentException("Invalid node key format: " + nodeKey);
}
String[] parts = NODE_DELIMITER_PATTERN.split(nodeKey);
String[] parts = NODE_DELIMITER_PATTERN.split(nodeKey, 2);
String applicationName = parts[0];
String serviceTypeName = parts[1];

Expand Down Expand Up @@ -345,7 +345,7 @@ public LinkHistogramSummaryView getLinkTimeHistogramData(
if (!LINK_KEY_VALIDATION_PATTERN.matcher(linkKey).matches()) {
throw new IllegalArgumentException("Invalid linkKey format: expected 'fromApp~toApp' but got: " + linkKey);
}
String[] parts = LINK_DELIMITER_PATTERN.split(linkKey);
String[] parts = LINK_DELIMITER_PATTERN.split(linkKey, 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid linkKey format: expected 'fromApp~toApp' but got: " + linkKey);
}
Expand Down
Loading