comparator = Comparator.comparingInt(range -> range.start);
+ strings.sort(comparator);
+ regexes.sort(comparator);
+ templateText.sort(comparator);
+ comments.sort(comparator);
+ }
+ }
+}
diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java
new file mode 100644
index 000000000..41b49eb8f
--- /dev/null
+++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptPlaceholderValue.java
@@ -0,0 +1,35 @@
+package com.bencodez.advancedcore.api.javascript;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Carries custom placeholder output through an authored JavaScript segment as
+ * inert data until the JavaScript binder can bind or escape it safely.
+ */
+public final class JavascriptPlaceholderValue {
+ private static final String PREFIX = "%__advancedcore_bound_";
+ private static final String SUFFIX = "%";
+
+ private JavascriptPlaceholderValue() {
+ }
+
+ public static String encode(String value) {
+ String safeValue = value == null ? "" : value;
+ String encoded = Base64.getUrlEncoder().withoutPadding()
+ .encodeToString(safeValue.getBytes(StandardCharsets.UTF_8));
+ return PREFIX + encoded + SUFFIX;
+ }
+
+ static String decode(String token) {
+ if (token == null || !token.startsWith(PREFIX) || !token.endsWith(SUFFIX)) {
+ return null;
+ }
+ String encoded = token.substring(PREFIX.length(), token.length() - SUFFIX.length());
+ try {
+ return new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException ignored) {
+ return null;
+ }
+ }
+}
diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java
new file mode 100644
index 000000000..4a438c5d6
--- /dev/null
+++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/javascript/JavascriptTextTemplate.java
@@ -0,0 +1,150 @@
+package com.bencodez.advancedcore.api.javascript;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Splits configured text into literal text and JavaScript segments before any
+ * placeholder replacement occurs.
+ *
+ * Only {@code [Javascript=...]} markers present in the original input become
+ * executable segments. Marker-looking text produced later by a placeholder or
+ * JavaScript result is neutralized and remains ordinary text.
+ */
+public final class JavascriptTextTemplate {
+ private static final String MARKER = "[Javascript=";
+
+ private final List segments;
+
+ private JavascriptTextTemplate(List segments) {
+ this.segments = segments;
+ }
+
+ public static JavascriptTextTemplate parse(String source) {
+ List segments = new ArrayList<>();
+ if (source == null || source.isEmpty()) {
+ segments.add(Segment.text(source));
+ return new JavascriptTextTemplate(segments);
+ }
+
+ int cursor = 0;
+ while (cursor < source.length()) {
+ int start = indexOfIgnoreCase(source, MARKER, cursor);
+ if (start < 0) {
+ segments.add(Segment.text(source.substring(cursor)));
+ break;
+ }
+
+ if (start > cursor) {
+ segments.add(Segment.text(source.substring(cursor, start)));
+ }
+
+ int bodyStart = start + MARKER.length();
+ int end = source.indexOf(']', bodyStart);
+ if (end < 0) {
+ segments.add(Segment.text(source.substring(start)));
+ break;
+ }
+
+ segments.add(Segment.javascript(source.substring(start, bodyStart),
+ source.substring(bodyStart, end)));
+ cursor = end + 1;
+ }
+
+ if (segments.isEmpty()) {
+ segments.add(Segment.text(source));
+ }
+ return new JavascriptTextTemplate(segments);
+ }
+
+ /**
+ * Transforms literal and JavaScript content while preserving authored marker
+ * boundaries.
+ */
+ public String transform(Function textTransformer,
+ Function javascriptTransformer) {
+ StringBuilder result = new StringBuilder();
+ for (Segment segment : segments) {
+ if (segment.javascript) {
+ result.append(segment.markerPrefix)
+ .append(apply(javascriptTransformer, segment.value))
+ .append(']');
+ } else {
+ result.append(neutralizeGeneratedMarkers(apply(textTransformer, segment.value)));
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Evaluates authored JavaScript segments and returns plain text. Any marker
+ * produced by transformed text or a script result is neutralized so a second
+ * processing pass cannot execute it.
+ */
+ public String evaluate(Function textTransformer,
+ Function javascriptEvaluator) {
+ StringBuilder result = new StringBuilder();
+ for (Segment segment : segments) {
+ String value = segment.javascript
+ ? apply(javascriptEvaluator, segment.value)
+ : apply(textTransformer, segment.value);
+ result.append(value);
+ }
+ return neutralizeGeneratedMarkers(result.toString());
+ }
+
+ public static String neutralizeGeneratedMarkers(String text) {
+ if (text == null || text.isEmpty()) {
+ return text;
+ }
+
+ StringBuilder result = new StringBuilder(text.length());
+ int cursor = 0;
+ while (cursor < text.length()) {
+ int start = indexOfIgnoreCase(text, MARKER, cursor);
+ if (start < 0) {
+ result.append(text, cursor, text.length());
+ break;
+ }
+ result.append(text, cursor, start + MARKER.length() - 1).append(" =");
+ cursor = start + MARKER.length();
+ }
+ return result.toString();
+ }
+
+ private static String apply(Function transformer, String value) {
+ String transformed = transformer == null ? value : transformer.apply(value);
+ return transformed == null ? "" : transformed;
+ }
+
+ private static int indexOfIgnoreCase(String text, String target, int fromIndex) {
+ int max = text.length() - target.length();
+ for (int i = Math.max(0, fromIndex); i <= max; i++) {
+ if (text.regionMatches(true, i, target, 0, target.length())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static final class Segment {
+ private final boolean javascript;
+ private final String markerPrefix;
+ private final String value;
+
+ private Segment(boolean javascript, String markerPrefix, String value) {
+ this.javascript = javascript;
+ this.markerPrefix = markerPrefix;
+ this.value = value;
+ }
+
+ private static Segment text(String value) {
+ return new Segment(false, null, value == null ? "" : value);
+ }
+
+ private static Segment javascript(String markerPrefix, String value) {
+ return new Segment(true, markerPrefix, value);
+ }
+ }
+}
diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java
index 8e74bb82d..c5718f8c1 100644
--- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java
+++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java
@@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
+import java.util.function.Function;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
@@ -10,6 +11,8 @@
import com.bencodez.advancedcore.AdvancedCorePlugin;
import com.bencodez.advancedcore.api.javascript.JavascriptEngine;
+import com.bencodez.advancedcore.api.javascript.JavascriptPlaceholderValue;
+import com.bencodez.advancedcore.api.javascript.JavascriptTextTemplate;
import com.bencodez.advancedcore.api.user.AdvancedCoreUser;
import com.bencodez.simpleapi.messages.MessageAPI;
@@ -209,8 +212,9 @@ public static ArrayList replaceJavascript(Player player, ArrayList replacePlaceHolder(ArrayList list, HashMap placeholders) {
@@ -292,21 +258,16 @@ public static ArrayList replacePlaceHolder(ArrayList list, HashM
}
public static String replacePlaceHolder(String str, HashMap placeholders) {
- if (placeholders != null) {
- for (Entry entry : placeholders.entrySet()) {
- str = replacePlaceHolder(str, entry.getKey(), entry.getValue());
- }
- }
- return str;
+ return replacePlaceHolder(str, placeholders, true);
}
public static String replacePlaceHolder(String str, HashMap placeholders, boolean ignoreCase) {
- if (placeholders != null) {
- for (Entry entry : placeholders.entrySet()) {
- str = replacePlaceHolder(str, entry.getKey(), entry.getValue(), ignoreCase);
- }
+ if (str == null || placeholders == null || placeholders.isEmpty()) {
+ return str;
}
- return str;
+ return JavascriptTextTemplate.parse(str).transform(
+ value -> replacePlaceHolderMapRaw(value, placeholders, ignoreCase),
+ value -> replacePlaceHolderMapEncoded(value, placeholders, ignoreCase));
}
/**
@@ -322,14 +283,44 @@ public static String replacePlaceHolder(String str, String toReplace, String rep
}
public static String replacePlaceHolder(String str, String toReplace, String replaceWith, boolean ignoreCase) {
+ if (str == null) {
+ return null;
+ }
+ return JavascriptTextTemplate.parse(str).transform(
+ value -> replacePlaceHolderRaw(value, toReplace, replaceWith, ignoreCase),
+ value -> replacePlaceHolderRaw(value, toReplace, JavascriptPlaceholderValue.encode(replaceWith),
+ ignoreCase));
+ }
+
+ private static String replacePlaceHolderMapRaw(String str, HashMap placeholders,
+ boolean ignoreCase) {
+ String result = str;
+ for (Entry entry : placeholders.entrySet()) {
+ result = replacePlaceHolderRaw(result, entry.getKey(), entry.getValue(), ignoreCase);
+ }
+ return result;
+ }
+
+ private static String replacePlaceHolderMapEncoded(String str, HashMap placeholders,
+ boolean ignoreCase) {
+ String result = str;
+ for (Entry entry : placeholders.entrySet()) {
+ result = replacePlaceHolderRaw(result, entry.getKey(), JavascriptPlaceholderValue.encode(entry.getValue()),
+ ignoreCase);
+ }
+ return result;
+ }
+
+ private static String replacePlaceHolderRaw(String str, String toReplace, String replaceWith, boolean ignoreCase) {
+ String safeReplacement = replaceWith == null ? "" : replaceWith;
if (ignoreCase) {
- return MessageAPI.replaceIgnoreCase(MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", replaceWith),
- "\\{" + toReplace + "\\}", replaceWith);
+ return MessageAPI.replaceIgnoreCase(
+ MessageAPI.replaceIgnoreCase(str, "%" + toReplace + "%", safeReplacement),
+ "\\{" + toReplace + "\\}", safeReplacement);
}
str = str.replaceAll("\\{", "%");
str = str.replaceAll("\\}", "%");
- str = str.replace("%" + toReplace + "%", replaceWith);
- return str;
+ return str.replace("%" + toReplace + "%", safeReplacement);
}
public static ArrayList replacePlaceHolders(ArrayList list, Player p) {
@@ -349,11 +340,21 @@ public static ArrayList replacePlaceHolders(OfflinePlayer player, ArrayL
}
public static String replacePlaceHolders(OfflinePlayer player, String text) {
- if (player == null) {
+ return replacePlaceHolders(player, text, false);
+ }
+
+ private static String replacePlaceHolders(OfflinePlayer player, String text,
+ boolean replaceJavascriptSegments) {
+ if (player == null || text == null || text.isEmpty()) {
return text;
}
if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) {
- return PlaceholderAPI.setPlaceholders(player, text);
+ Function replacement = value -> PlaceholderAPI.setPlaceholders(player, value);
+ Function javascriptReplacement = replaceJavascriptSegments
+ ? value -> JavascriptTextTemplate.neutralizeGeneratedMarkers(replacement.apply(value))
+ : Function.identity();
+ return JavascriptTextTemplate.parse(text).transform(
+ replacement, javascriptReplacement);
}
return text;
}
@@ -366,13 +367,7 @@ public static String replacePlaceHolders(OfflinePlayer player, String text) {
* @return the string
*/
public static String replacePlaceHolders(Player player, String text) {
- if (player == null) {
- return text;
- }
- if (AdvancedCorePlugin.getInstance().isPlaceHolderAPIEnabled()) {
- return PlaceholderAPI.setPlaceholders(player, text);
- }
- return text;
+ return replacePlaceHolders((OfflinePlayer) player, text);
}
}
diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java
index 3af8e73a5..d80fcd9f6 100644
--- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java
+++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/RewardJavascript.java
@@ -14,7 +14,6 @@
import com.bencodez.advancedcore.api.inventory.editgui.valuetypes.EditGUIValueList;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.advancedcore.api.javascript.JavascriptEngine;
-import com.bencodez.advancedcore.api.messages.PlaceholderUtils;
import com.bencodez.advancedcore.api.rewards.DefinedReward;
import com.bencodez.advancedcore.api.rewards.Reward;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
@@ -37,10 +36,10 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) {
public String onRewardRequest(Reward reward, AdvancedCoreUser user, ArrayList list,
HashMap placeholders) {
if (!list.isEmpty()) {
- JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer());
+ JavascriptEngine engine = new JavascriptEngine().addPlayer(user.getOfflinePlayer())
+ .addPlaceholders(placeholders);
for (String script : list) {
- String expression = PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), script);
- engine.execute(PlaceholderUtils.replacePlaceHolder(expression, placeholders));
+ engine.execute(script);
}
}
return null;
@@ -61,9 +60,8 @@ public String onRewardRequested(Reward reward, AdvancedCoreUser user, Configurat
HashMap placeholders) {
if (section.getBoolean("Enabled")) {
String expression = section.getString("Expression");
- expression = PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(), expression);
- if (new JavascriptEngine().addPlayer(user.getOfflinePlayer())
- .getBooleanValue(PlaceholderUtils.replacePlaceHolder(expression, placeholders))) {
+ if (new JavascriptEngine().addPlayer(user.getOfflinePlayer()).addPlaceholders(placeholders)
+ .getBooleanValue(expression)) {
new RewardBuilder(section, "TrueRewards").withPrefix(reward.getName() + ".Javascript").send(user);
} else {
new RewardBuilder(section, "FalseRewards").withPrefix(reward.getName() + ".Javascript").send(user);
diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java
index 22be9c421..7bf479980 100644
--- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java
+++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/builtin/requirements/RequirementJavascript.java
@@ -8,7 +8,6 @@
import com.bencodez.advancedcore.api.inventory.editgui.valuetypes.EditGUIValueString;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.advancedcore.api.javascript.JavascriptEngine;
-import com.bencodez.advancedcore.api.messages.PlaceholderUtils;
import com.bencodez.advancedcore.api.rewards.Reward;
import com.bencodez.advancedcore.api.rewards.RewardEditData;
import com.bencodez.advancedcore.api.rewards.RewardHandler;
@@ -29,8 +28,7 @@ public static void register(RewardHandler handler, AdvancedCorePlugin plugin) {
public boolean onRequirementsRequest(Reward reward, AdvancedCoreUser user, String expression,
RewardOptions rewardOptions) {
return expression.equals("") || new JavascriptEngine().addPlayer(user.getOfflinePlayer())
- .getBooleanValue(PlaceholderUtils.replacePlaceHolders(user.getOfflinePlayer(),
- PlaceholderUtils.replacePlaceHolder(expression, rewardOptions.getPlaceholders())));
+ .addPlaceholders(rewardOptions.getPlaceholders()).getBooleanValue(expression);
}
}.priority(90).addEditButton(new EditGUIButton(new ItemBuilder("DETECTOR_RAIL"),
new EditGUIValueString("JavascriptExpression", null) {
diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java
new file mode 100644
index 000000000..20c75a0c0
--- /dev/null
+++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/javascript/JavascriptEngineConcurrencyTest.java
@@ -0,0 +1,79 @@
+package com.bencodez.advancedcore.api.javascript;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.Reader;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+
+import javax.script.AbstractScriptEngine;
+import javax.script.Bindings;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngineFactory;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+
+import org.junit.jupiter.api.Test;
+
+class JavascriptEngineConcurrencyTest {
+
+ @Test
+ void sharedEngineBindingsAndEvaluationAreAtomic() throws Exception {
+ LockCheckingScriptEngine engine = new LockCheckingScriptEngine();
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ CountDownLatch start = new CountDownLatch(1);
+
+ try {
+ Future