diff --git a/build.gradle.kts b/build.gradle.kts index 36c1891..5e835da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,8 @@ plugins { `java-library` `maven-publish` // id("com.github.hierynomus.license") version "0.16.1" - id("io.github.goooler.shadow") version "8.1.7" + id("com.gradleup.shadow") version "9.3.1" + id("me.champeau.jmh") version "0.7.2" } group = "me.clip" @@ -41,12 +42,13 @@ dependencies { compileOnly("dev.folia:folia-api:1.21.11-R0.1-SNAPSHOT") compileOnlyApi("org.jetbrains:annotations:23.0.0") - testImplementation("org.openjdk.jmh:jmh-core:1.32") - testImplementation("org.openjdk.jmh:jmh-generator-annprocess:1.32") - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.2") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") -} + jmh("org.openjdk.jmh:jmh-core:1.37") + jmh("org.openjdk.jmh:jmh-generator-annprocess:1.37") + jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.37") + testImplementation("org.junit.jupiter:junit-jupiter:6.0.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2617362..aa28adb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/jmh/java/me/clip/placeholderapi/Values.java b/src/jmh/java/me/clip/placeholderapi/Values.java new file mode 100644 index 0000000..ff06e7d --- /dev/null +++ b/src/jmh/java/me/clip/placeholderapi/Values.java @@ -0,0 +1,94 @@ +/* + * This file is part of PlaceholderAPI + * + * PlaceholderAPI + * Copyright (c) 2015 - 2026 PlaceholderAPI Team + * + * PlaceholderAPI free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlaceholderAPI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.clip.placeholderapi; + +import com.google.common.collect.ImmutableMap; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import me.clip.placeholderapi.replacer.CharsReplacer; +import me.clip.placeholderapi.replacer.OldCharsReplacer; +import me.clip.placeholderapi.replacer.Replacer; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface Values { + + String SMALL_TEXT = "My name is %player_name%"; + String LARGE_TEXT = "My name is %player_name% and my location is (%player_x%, %player_y%, %player_z%), this placeholder is invalid %server_name%"; + + ImmutableMap PLACEHOLDERS = ImmutableMap.builder() + .put("player", new MockPlayerPlaceholderExpansion()) + .build(); + + + Replacer CHARS_REPLACER = new CharsReplacer(Replacer.Closure.PERCENT); + Replacer OLD_CHARS_REPLACER = new OldCharsReplacer(Replacer.Closure.PERCENT); + + final class MockPlayerPlaceholderExpansion extends PlaceholderExpansion { + + public static final String PLAYER_X = "10"; + public static final String PLAYER_Y = "20"; + public static final String PLAYER_Z = "30"; + public static final String PLAYER_NAME = "Sxtanna"; + + + @NotNull + @Override + public String getIdentifier() { + return "player"; + } + + @NotNull + @Override + public String getAuthor() { + return "Sxtanna"; + } + + @NotNull + @Override + public String getVersion() { + return "1.0"; + } + + @Override + public String onRequest(@Nullable final OfflinePlayer player, @NotNull final String params) { + final String[] parts = params.split("_"); + if (parts.length == 0) { + return null; + } + + switch (parts[0]) { + case "name": + return PLAYER_NAME; + case "x": + return PLAYER_X; + case "y": + return PLAYER_Y; + case "z": + return PLAYER_Z; + } + + return null; + } + + } + +} diff --git a/src/jmh/java/me/clip/placeholderapi/replacer/OldCharsReplacer.java b/src/jmh/java/me/clip/placeholderapi/replacer/OldCharsReplacer.java new file mode 100644 index 0000000..2941f21 --- /dev/null +++ b/src/jmh/java/me/clip/placeholderapi/replacer/OldCharsReplacer.java @@ -0,0 +1,136 @@ +package me.clip.placeholderapi.replacer; + +/* + * This file is part of PlaceholderAPI + * + * PlaceholderAPI + * Copyright (c) 2015 - 2026 PlaceholderAPI Team + * + * PlaceholderAPI free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlaceholderAPI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import java.util.Locale; +import java.util.function.Function; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class OldCharsReplacer implements Replacer { + + @NotNull + private final Closure closure; + + public OldCharsReplacer(@NotNull final Closure closure) { + this.closure = closure; + } + + @NotNull + @Override + public String apply(@NotNull final String text, @Nullable final OfflinePlayer player, + @NotNull final Function lookup) { + final char[] chars = text.toCharArray(); + final StringBuilder builder = new StringBuilder(text.length()); + + final StringBuilder identifier = new StringBuilder(); + final StringBuilder parameters = new StringBuilder(); + + for (int i = 0; i < chars.length; i++) { + final char l = chars[i]; + + if (l != closure.head || i + 1 >= chars.length) { + builder.append(l); + continue; + } + + boolean identified = false; + boolean invalid = true; + boolean hadSpace = false; + + while (++i < chars.length) { + final char p = chars[i]; + + if (p == ' ' && !identified) { + hadSpace = true; + break; + } + if (p == closure.tail) { + invalid = false; + break; + } + + if (p == '_' && !identified) { + identified = true; + continue; + } + + if (identified) { + parameters.append(p); + } else { + identifier.append(p); + } + } + + final String identifierString = identifier.toString(); + final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT); + final String parametersString = parameters.toString(); + + identifier.setLength(0); + parameters.setLength(0); + + if (invalid) { + builder.append(closure.head).append(identifierString); + + if (identified) { + builder.append('_').append(parametersString); + } + + if (hadSpace) { + builder.append(' '); + } + continue; + } + + final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString); + if (placeholder == null) { + builder.append(closure.head).append(identifierString); + + if (identified) { + builder.append('_'); + } + + builder.append(parametersString).append(closure.tail); + continue; + } + + final String replacement = placeholder.onRequest(player, parametersString); + if (replacement == null) { + builder.append(closure.head).append(identifierString); + + if (identified) { + builder.append('_'); + } + + builder.append(parametersString).append(closure.tail); + continue; + } + + builder.append(replacement); + } + + return builder.toString(); + } + +} \ No newline at end of file diff --git a/src/jmh/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java b/src/jmh/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java new file mode 100644 index 0000000..7469f64 --- /dev/null +++ b/src/jmh/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java @@ -0,0 +1,66 @@ +/* + * This file is part of PlaceholderAPI + * + * PlaceholderAPI + * Copyright (c) 2015 - 2026 PlaceholderAPI Team + * + * PlaceholderAPI free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlaceholderAPI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.clip.placeholderapi.replacer; + +import me.clip.placeholderapi.Values; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.jetbrains.annotations.Nullable; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode({Mode.AverageTime, Mode.Throughput}) +@Fork(value = 3, warmups = 1) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +public class ReplacerBenchmarks { + + private Function expansionFunction; + + @Setup + public void setup() { + this.expansionFunction = Values.PLACEHOLDERS::get; + } + + @Benchmark + public void measureCharsReplacerSmallText(final Blackhole blackhole) { + blackhole.consume(Values.CHARS_REPLACER.apply(Values.SMALL_TEXT, null, expansionFunction)); + } + + @Benchmark + public void measureCharsReplacerLargeText(final Blackhole blackhole) { + blackhole.consume(Values.CHARS_REPLACER.apply(Values.LARGE_TEXT, null, expansionFunction)); + } + + @Benchmark + public void measureCharsReplacerSmallTextOld(final Blackhole blackhole) { + blackhole.consume(Values.OLD_CHARS_REPLACER.apply(Values.SMALL_TEXT, null, expansionFunction)); + } + + @Benchmark + public void measureCharsReplacerLargeTextOld(final Blackhole blackhole) { + blackhole.consume(Values.OLD_CHARS_REPLACER.apply(Values.LARGE_TEXT, null, expansionFunction)); + } +} \ No newline at end of file diff --git a/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java b/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java index c05f814..258cb92 100644 --- a/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java +++ b/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java @@ -20,12 +20,7 @@ package me.clip.placeholderapi; -import com.google.common.collect.ImmutableSet; - -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -85,7 +80,11 @@ public final class PlaceholderAPI { @NotNull public static List setPlaceholders(final OfflinePlayer player, @NotNull final List text) { - return text.stream().map(line -> setPlaceholders(player, line)).collect(Collectors.toList()); + final List result = new ArrayList<>(text.size()); + for (final String line : text) { + result.add(setPlaceholders(player, line)); + } + return result; } /** @@ -140,8 +139,11 @@ public final class PlaceholderAPI { @NotNull public static List<@NotNull String> setBracketPlaceholders(final OfflinePlayer player, @NotNull final List<@NotNull String> text) { - return text.stream().map(line -> setBracketPlaceholders(player, line)) - .collect(Collectors.toList()); + final List result = new ArrayList<>(text.size()); + for (final String line : text) { + result.add(setBracketPlaceholders(player, line)); + } + return result; } /** @@ -179,14 +181,14 @@ public final class PlaceholderAPI { * @param text Text to parse the placeholders in * @return The text containing the parsed relational placeholders */ - public static String setRelationalPlaceholders(Player one, Player two, String text) { + public static String setRelationalPlaceholders(final Player one, final Player two, @NotNull String text) { final Matcher matcher = RELATIONAL_PLACEHOLDER_PATTERN.matcher(text); while (matcher.find()) { final String format = matcher.group(2); - final int index = format.indexOf("_"); + final int index = format.indexOf('_'); - if (index <= 0 || index >= format.length()) { + if (index <= 0) { continue; } @@ -218,9 +220,12 @@ public final class PlaceholderAPI { * @param text text to parse the placeholder values to * @return The text containing the parsed relational placeholders */ - public static List setRelationalPlaceholders(Player one, Player two, List text) { - return text.stream().map(line -> setRelationalPlaceholders(one, two, line)) - .collect(Collectors.toList()); + public static List setRelationalPlaceholders(final Player one, final Player two, final @NotNull List text) { + final List result = new ArrayList<>(text.size()); + for (final String line : text) { + result.add(setRelationalPlaceholders(one, two, line)); + } + return result; } /** @@ -241,8 +246,7 @@ public final class PlaceholderAPI { */ @NotNull public static Set getRegisteredIdentifiers() { - return ImmutableSet - .copyOf(PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getIdentifiers()); + return PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getIdentifiers(); } /** @@ -279,8 +283,15 @@ public final class PlaceholderAPI { * @param text String to check * @return true if String contains any matches to the normal placeholder pattern, false otherwise */ - public static boolean containsPlaceholders(String text) { - return text != null && PLACEHOLDER_PATTERN.matcher(text).find(); + public static boolean containsPlaceholders(final String text) { + if (text == null) { + return false; + } + final int firstPercent = text.indexOf('%'); + if (firstPercent == -1) { + return false; + } + return text.indexOf('%', firstPercent + 1) != -1; } /** @@ -290,8 +301,15 @@ public final class PlaceholderAPI { * @param text String to check * @return true if String contains any matches to the bracket placeholder pattern, false otherwise */ - public static boolean containsBracketPlaceholders(String text) { - return text != null && BRACKET_PLACEHOLDER_PATTERN.matcher(text).find(); + public static boolean containsBracketPlaceholders(final String text) { + if (text == null) { + return false; + } + final int openBracket = text.indexOf('{'); + if (openBracket == -1) { + return false; + } + return text.indexOf('}', openBracket + 1) != -1; } // === Deprecated API === diff --git a/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java b/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java index ff54463..9cf638e 100644 --- a/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java +++ b/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java @@ -114,7 +114,7 @@ public final class LocalExpansionManager implements Listener { @NotNull @Unmodifiable - public Collection getIdentifiers() { + public Set getIdentifiers() { expansionsLock.lock(); try { return ImmutableSet.copyOf(expansions.keySet()); @@ -136,12 +136,7 @@ public final class LocalExpansionManager implements Listener { @Nullable public PlaceholderExpansion getExpansion(@NotNull final String identifier) { - expansionsLock.lock(); - try { - return expansions.get(identifier.toLowerCase(Locale.ROOT)); - } finally { - expansionsLock.unlock(); - } + return expansions.get(identifier.toLowerCase(Locale.ROOT)); } @NotNull diff --git a/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java b/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java index a0fe4dc..e07ef00 100644 --- a/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java +++ b/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java @@ -36,102 +36,113 @@ public final class CharsReplacer implements Replacer { this.closure = closure; } - + /** + * Translates placeholders within the provided text using a high-performance + * character-scanning approach. + * *

The method identifies placeholders delimited by the defined {@link Closure} + * (e.g., %identifier_params% or {identifier_params}). If a placeholder is + * successfully identified, the provided lookup function is used to fetch the + * corresponding {@link PlaceholderExpansion}.

+ * + * @param text The raw text containing potential placeholders to be replaced. + * @param player The {@link OfflinePlayer} to contextually parse the placeholders against. + * May be {@code null} if no player context is available. + * @param lookup A function that maps a lowercase identifier string to a registered + * {@link PlaceholderExpansion}. + * @return A string with all valid placeholders replaced by their respective values. + * Returns the original text if no placeholders are found. + */ @NotNull @Override public String apply(@NotNull final String text, @Nullable final OfflinePlayer player, @NotNull final Function lookup) { - final char[] chars = text.toCharArray(); - final StringBuilder builder = new StringBuilder(text.length()); + final char head = closure.head; + int startPlaceholder = text.indexOf(head); - final StringBuilder identifier = new StringBuilder(); - final StringBuilder parameters = new StringBuilder(); + if (startPlaceholder == -1) { + return text; + } - for (int i = 0; i < chars.length; i++) { - final char l = chars[i]; + final int length = text.length(); + final StringBuilder builder = new StringBuilder(length + (length >> 3)); + int cursor = 0; - if (l != closure.head || i + 1 >= chars.length) { - builder.append(l); - continue; + final char tail = closure.tail; + + loop: do { + // Append plain text preceding the placeholder + if (startPlaceholder > cursor) { + builder.append(text, cursor, startPlaceholder); } - boolean identified = false; - boolean invalid = true; - boolean hadSpace = false; + final int endPlaceholder = text.indexOf(tail, startPlaceholder + 1); - while (++i < chars.length) { - final char p = chars[i]; + if (endPlaceholder == -1) { + builder.append(text, startPlaceholder, length); + return builder.toString(); + } - if (p == ' ' && !identified) { - hadSpace = true; - break; - } - if (p == closure.tail && identified) { - invalid = false; - break; - } - if (p == closure.tail) { - identifier.append(p); - break; + int underscoreIndex = -1; + + for (int i = startPlaceholder + 1; i < endPlaceholder; i++) { + final char current = text.charAt(i); + + if (current == ' ') { + // Invalid placeholder (contains space). + // Treat the opening symbol as literal text and search for the next one. + builder.append(head); + cursor = startPlaceholder + 1; + startPlaceholder = text.indexOf(head, cursor); + + // Safety check: If no more placeholders exist, break to finalize + if (startPlaceholder == -1) { + break loop; + } + continue loop; } - if (p == '_' && !identified) { - identified = true; - continue; - } - - if (identified) { - parameters.append(p); - } else { - identifier.append(p); + if (current == '_' && underscoreIndex == -1) { + underscoreIndex = i; } } - final String identifierString = identifier.toString(); - final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT); - final String parametersString = parameters.toString(); + String identifier; + String parameters = ""; - identifier.setLength(0); - parameters.setLength(0); - - if (invalid) { - builder.append(closure.head).append(identifierString); - - if (identified) { - builder.append('_').append(parametersString); + if (underscoreIndex != -1) { + identifier = text.substring(startPlaceholder + 1, underscoreIndex); + if (underscoreIndex + 1 < endPlaceholder) { + parameters = text.substring(underscoreIndex + 1, endPlaceholder); } - - if (hadSpace) { - builder.append(' '); - } - continue; + } else { + identifier = text.substring(startPlaceholder + 1, endPlaceholder); } - final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString); - if (placeholder == null) { - builder.append(closure.head).append(identifierString); + final PlaceholderExpansion expansion = lookup.apply(identifier.toLowerCase(Locale.ROOT)); + String replacement = null; - if (identified) { - builder.append('_'); - } - - builder.append(parametersString).append(closure.tail); - continue; + if (expansion != null) { + replacement = expansion.onRequest(player, parameters); } - final String replacement = placeholder.onRequest(player, parametersString); - if (replacement == null) { - builder.append(closure.head).append(identifierString); - - if (identified) { - builder.append('_'); + if (replacement != null) { + builder.append(replacement); + } else { + // Fallback: Restore original placeholder format + builder.append(head).append(identifier); + if (underscoreIndex != -1) { + builder.append('_').append(parameters); } - - builder.append(parametersString).append(closure.tail); - continue; + builder.append(tail); } - builder.append(replacement); + cursor = endPlaceholder + 1; + startPlaceholder = text.indexOf(head, cursor); + + } while (startPlaceholder != -1); + + if (cursor < length) { + builder.append(text, cursor, length); } return builder.toString(); diff --git a/src/test/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java b/src/test/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java deleted file mode 100644 index 1283299..0000000 --- a/src/test/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is part of PlaceholderAPI - * - * PlaceholderAPI - * Copyright (c) 2015 - 2026 PlaceholderAPI Team - * - * PlaceholderAPI free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * PlaceholderAPI is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package me.clip.placeholderapi.replacer; - -import me.clip.placeholderapi.Values; -import org.openjdk.jmh.annotations.Benchmark; - -public class ReplacerBenchmarks { - - @Benchmark - public void measureCharsReplacerSmallText() { - Values.CHARS_REPLACER.apply(Values.SMALL_TEXT, null, Values.PLACEHOLDERS::get); - } - - @Benchmark - public void measureCharsReplacerLargeText() { - Values.CHARS_REPLACER.apply(Values.LARGE_TEXT, null, Values.PLACEHOLDERS::get); - } - -} \ No newline at end of file