Skip to content

Instantly share code, notes, and snippets.

@iseki0
Last active January 18, 2024 11:32
Show Gist options
  • Save iseki0/832804c2924952aca048c6c7625f1039 to your computer and use it in GitHub Desktop.
Save iseki0/832804c2924952aca048c6c7625f1039 to your computer and use it in GitHub Desktop.
import java.util.Objects;
record ServerTiming(String name, String desc, double dur) {
public ServerTiming {
Objects.requireNonNull(name);
}
public ServerTiming(String name, String desc) {
this(name, desc, Double.NaN);
}
public ServerTiming(String name) {
this(name, null, Double.NaN);
}
public ServerTiming(String name, double dur) {
this(name, null, dur);
}
private static String quoteString(String s) {
if (s.isEmpty()) return "\"\"";
var ec = (int) s.chars().filter(i -> i == '"' || i == '\\').count();
if (ec == 0) return "\"" + s + "\"";
var builder = new StringBuilder(ec + s.length() + 2);
builder.append('"');
s.chars().forEach(i -> {
if (i == '"' || i == '\\') {
builder.append('\\');
}
builder.append(i);
});
builder.append('"');
assert builder.capacity() == ec + s.length() + 2;
return builder.toString();
}
@Override
public String toString() {
if (desc == null) {
if (Double.isNaN(dur)) {
return name;
}
return name + ";dur=" + dur;
}
if (Double.isNaN(dur)) {
return name + ";desc=" + quoteString(desc);
}
return name + ";desc=" + quoteString(desc) + ";dur=" + dur;
}
}
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class ServerTimingHeaderTest {
private val tc = listOf(
"cache;desc=\"Cache\\\" Read\";dur=23.2" to listOf(ServerTimingItem("cache", "Cache\" Read", 23.2)),
"cache;desc=\"Cache Read\",a,;dur=23.2" to listOf(
ServerTimingItem("cache", "Cache Read"),
ServerTimingItem("a"),
),
"db;dur=53, app ; dur=47.2" to listOf(
ServerTimingItem("db", 53.0),
ServerTimingItem("app", 47.2),
),
"db;du=, app;; ;dur=47.2;k\"" to listOf(
ServerTimingItem("db"),
ServerTimingItem("app", 47.2),
),
)
@Test
fun testParse() {
for ((t, r) in tc) {
val parsed = ServerTimingItem.parse(t)
assertEquals(r, parsed, t)
println(parsed)
}
}
}
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public record ServerTimingItem(@NotNull String name, @Nullable String desc, double dur, boolean hasDur) {
public static final String HTTP_HEADER = "Server-Timing";
public static final String TAO_HTTP_HEADER = "Timing-Allow-Origin";
public ServerTimingItem(@NotNull String name, String desc, double dur) {
this(name, desc, dur, true);
}
public ServerTimingItem(@NotNull String name, double dur) {
this(name, null, dur, true);
}
public ServerTimingItem(@NotNull String name, String desc) {
this(name, desc, 0, false);
}
public ServerTimingItem(@NotNull String name) {
this(name, null, 0, false);
}
private static ArrayList<ServerTimingItem> parse0(String input) {
var off = 0;
var list = new ArrayList<ServerTimingItem>();
while (off < input.length()) {
var nameToken = tokenize(input, off);
if (!Token.typeIs(nameToken, TokenType.Text)) {
off = offsetAfterComma(input, nameToken);
continue;
}
String desc = null;
double dur = 0;
boolean hasDur = false;
var semiToken = tokenize(input, nameToken.end);
while (Token.typeIs(semiToken, TokenType.Semi)) {
var p = tokenize(input, semiToken.end);
if (!Token.typeIs(p, TokenType.Text)) {
semiToken = tokenizeUntilSP(input, p);
continue;
}
var eq = tokenize(input, p.end);
if (!Token.typeIs(eq, TokenType.Eq)) {
semiToken = tokenizeUntilSP(input, eq);
continue;
}
var v = tokenize(input, eq.end);
if (Token.typeIs(v, TokenType.Text) || Token.typeIs(v, TokenType.QuotedText)) {
var vt = v.type == TokenType.QuotedText ? unquoteString(v.getText(input)) : v.getText(input);
if (p.matchString(input, "desc")) {
desc = vt;
} else if (p.matchString(input, "dur")) {
try {
dur = Double.parseDouble(vt);
hasDur = true;
} catch (NumberFormatException ignored) {
}
}
}
semiToken = tokenizeUntilSP(input, v);
}
list.add(new ServerTimingItem(nameToken.getText(input), desc, dur, hasDur));
off = offsetAfterComma(input, semiToken);
}
return list;
}
private static Token tokenizeUntilSP(String input, Token token) {
while (token != null && token.type != TokenType.Comma && token.type != TokenType.Semi)
token = tokenize(input, token.end);
return token;
}
private static int offsetAfterComma(String input, Token token) {
while (token != null && token.type != TokenType.Comma) token = tokenize(input, token.end);
return token == null ? input.length() : token.end;
}
private static Token tokenize(String input, int off) {
if (off >= input.length()) return null;
var ch = input.charAt(off);
switch (ch) {
case ',', ';', '=':
return new Token(off, off + 1, switch (ch) {
case ';' -> TokenType.Semi;
case ',' -> TokenType.Comma;
case '=' -> TokenType.Eq;
default -> throw new AssertionError("?");
});
}
if (Character.isSpaceChar(ch)) {
while (off < input.length() && Character.isSpaceChar(input.charAt(off))) off++;
return tokenize(input, off);
}
if (ch == '"') {
var p = off + 1;
while (true) {
if (p >= input.length()) {
return new Token(off, input.length(), TokenType.QuotedText);
}
if (input.charAt(p) == '"') {
break;
}
if (input.charAt(p) == '\\') {
p++;
}
p++;
}
return new Token(off, p + 1, TokenType.QuotedText);
}
var p = off + 1;
while (true) {
if (p >= input.length()) {
return new Token(off, input.length(), TokenType.Text);
}
ch = input.charAt(p);
if (Character.isSpaceChar(ch) || ch == ',' || ch == ';' || ch == '=' || ch == '"') {
return new Token(off, p, TokenType.Text);
}
p++;
}
}
public static @NotNull String toString(@NotNull List<@NotNull ServerTimingItem> list) {
return list.stream().map(ServerTimingItem::toString).collect(Collectors.joining(","));
}
private static String unquoteString(@NotNull String s) {
if (s.length() < 2) return s;
if (s.charAt(0) != '"' || s.charAt(s.length() - 1) != '"') return s;
if (s.length() == 2) return "";
if (s.indexOf('\\') == -1) return s.substring(1, s.length() - 1);
var sb = new StringBuilder(s.length() - 2);
for (int i = 1; i < s.length() - 1; i++) {
if (s.charAt(i) == '\\' && i + 1 < s.length() - 1) {
i++;
}
sb.append(s.charAt(i));
}
return sb.toString();
}
private static String quoteString(String s) {
if (s.isEmpty()) return "\"\"";
if (s.indexOf('"') == -1 && s.indexOf('\\') == -1) {
return "\"" + s + "\"";
}
var builder = new StringBuilder(s.length() + 2);
builder.append('"');
for (int i = 0; i < s.length(); i++) {
var ch = s.charAt(i);
if (ch == '"') {
builder.append("\\\"");
} else if (ch == '\\') {
builder.append("\\\\");
} else builder.append(ch);
}
builder.append('"');
return builder.toString();
}
/**
* Parse HTTP header Server-Timing.
*
* @param header the value of HTTP header {@code Server-Timing}
* @return list of parsed items, the list is modifiable
* @see <a href="https://www.w3.org/TR/server-timing/#the-server-timing-header-field">W3: The Server-Timing Header Field</a>
*/
public static @NotNull List<@NotNull ServerTimingItem> parse(String header) {
return parse0(header);
}
@Override
public String toString() {
return name + (desc == null ? "" : (";desc=" + quoteString(desc))) + (hasDur ? ";dur=" + dur : "");
}
enum TokenType {
Text, QuotedText, Eq, Semi, Comma,
}
record Token(int begin, int end, TokenType type) {
static boolean typeIs(Token token, TokenType type) {
return token != null && token.type == type;
}
boolean matchString(String input, String target) {
return input.regionMatches(begin, target, 0, end - begin);
}
String getText(String input) {
return input.substring(begin, end);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment