Last active
January 18, 2024 11:32
-
-
Save iseki0/832804c2924952aca048c6c7625f1039 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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