diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java b/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java index 5b6b738..20f5b15 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java @@ -7,28 +7,56 @@ import java.util.List; import org.bukkit.entity.Player; import me.clip.placeholderapi.PlaceholderAPI; +import world.bentobox.bentobox.BentoBox; public class CheckPapi { /** - * Evaluates the formula by first replacing PAPI placeholders (using the provided Player) - * and then evaluating the resulting expression. The expression is expected to be a series - * of numeric comparisons (using =, <>, <=, >=, <, >) joined by Boolean operators AND and OR. + * Evaluates the given formula by first replacing PAPI placeholders using the provided Player, + * then parsing and evaluating one or more conditions. + *
+ * The formula may contain conditions comparing numeric or string values. + * Operands may contain spaces. The grammar for a condition is: + *
+ * leftOperand operator rightOperand + *+ * where the leftOperand is a sequence of tokens (separated by whitespace) until a valid + * operator is found, and the rightOperand is a sequence of tokens until a boolean operator + * ("AND" or "OR") is encountered or the end of the formula is reached. + *
+ * Supported comparison operators (case sensitive) are: + *
+ * Examples: + *
+ * "%aoneblock_my_island_lifetime_count% >= 1000 AND %aoneblock_my_island_level% >= 100" + * "john smith == tasty bento AND 40 > 20" + ** - * For example: - * "%aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100" - * - * If any placeholder evaluates to a non-numeric value or the formula is malformed, false is returned. - * - * @param player the Player used for placeholder replacement. - * @param formula the formula to evaluate. + * @param player the Player used for placeholder replacement + * @param formula the formula to evaluate * @return true if the formula evaluates to true, false otherwise. */ public static boolean evaluate(Player player, String formula) { - // Replace PAPI placeholders with actual values using the provided Player. + // Replace PAPI placeholders with actual values. String parsedFormula = PlaceholderAPI.setPlaceholders(player, formula); - // Tokenize the parsed formula (tokens are assumed to be separated by whitespace). + // Tokenize the resulting formula by whitespace. List
- * Expression -> Term { OR Term } - * Term -> Factor { AND Factor } - * Factor -> operand operator operand - *- * - * A Factor is expected to be a numeric condition in the form: - * number operator number - * where operator is one of: =, <>, <=, >=, <, or >. + * A simple recursive descent parser that evaluates the formula. + * It supports multi-token operands for conditions. */ private static class Parser { private final List
+ * The left operand is built by collecting tokens until a valid operator is found.
+ * The right operand is built by collecting tokens until a boolean operator ("AND" or "OR")
+ * is encountered or the end of the token list is reached.
*
* @return the boolean result of the condition.
*/
- public boolean parseFactor() {
- // There must be at least three tokens remaining.
- if (pos + 2 >= tokens.size()) {
- throw new RuntimeException("Incomplete condition");
+ public boolean parseCondition() {
+ // Parse left operand.
+ StringBuilder leftSB = new StringBuilder();
+ if (!hasNext()) {
+ BentoBox.getInstance()
+ .logError("Challenges PAPI formula error: Expected left operand but reached end of expression");
+ return false;
}
-
- String leftOperand = next();
+ // Collect tokens for the left operand until an operator is encountered.
+ while (hasNext() && !isOperator(peek())) {
+ if (leftSB.length() > 0) {
+ leftSB.append(" ");
+ }
+ leftSB.append(next());
+ }
+ if (!hasNext()) {
+ throw new RuntimeException("Operator expected after left operand");
+ }
+ // Next token should be an operator.
String operator = next();
- String rightOperand = next();
-
- // Validate operator.
- if (!operator.equals("=") && !operator.equals("<>") && !operator.equals("<=") && !operator.equals(">=")
- && !operator.equals("<") && !operator.equals(">")) {
+ if (!isValidOperator(operator)) {
throw new RuntimeException("Invalid operator: " + operator);
}
+ // Parse right operand.
+ StringBuilder rightSB = new StringBuilder();
+ while (hasNext() && !isBooleanOperator(peek())) {
+ if (rightSB.length() > 0) {
+ rightSB.append(" ");
+ }
+ rightSB.append(next());
+ }
+ String leftOperand = leftSB.toString().trim();
+ String rightOperand = rightSB.toString().trim();
- double leftVal, rightVal;
- try {
- leftVal = Double.parseDouble(leftOperand);
- rightVal = Double.parseDouble(rightOperand);
- } catch (NumberFormatException e) {
- // If either operand is not numeric, return false.
+ if (rightOperand.isEmpty()) {
return false;
}
- // Evaluate the condition.
- switch (operator) {
- case "=":
- return Double.compare(leftVal, rightVal) == 0;
- case "<>":
- return Double.compare(leftVal, rightVal) != 0;
- case "<=":
- return leftVal <= rightVal;
- case ">=":
- return leftVal >= rightVal;
- case "<":
- return leftVal < rightVal;
- case ">":
- return leftVal > rightVal;
- default:
- // This case is never reached.
- return false;
+
+ // Evaluate the condition:
+ // If both operands can be parsed as numbers, use numeric comparison;
+ // otherwise, perform string comparison.
+ Double leftNum = tryParseDouble(leftOperand);
+ Double rightNum = tryParseDouble(rightOperand);
+ if (leftNum != null && rightNum != null) {
+ // Numeric comparison.
+ switch (operator) {
+ case "=":
+ case "==":
+ return Double.compare(leftNum, rightNum) == 0;
+ case "<>":
+ case "!=":
+ return Double.compare(leftNum, rightNum) != 0;
+ case "<=":
+ return leftNum <= rightNum;
+ case ">=":
+ return leftNum >= rightNum;
+ case "<":
+ return leftNum < rightNum;
+ case ">":
+ return leftNum > rightNum;
+ default:
+ BentoBox.getInstance().logError("Challenges PAPI formula error: Unsupported operator: " + operator);
+ return false;
+ }
+ } else {
+ // String comparison.
+ switch (operator) {
+ case "=":
+ return leftOperand.equalsIgnoreCase(rightOperand);
+ case "==":
+ return leftOperand.equals(rightOperand);
+ case "<>":
+ return !leftOperand.equalsIgnoreCase(rightOperand);
+ case "!=":
+ return !leftOperand.equals(rightOperand);
+ case "<=":
+ return leftOperand.compareTo(rightOperand) <= 0;
+ case ">=":
+ return leftOperand.compareTo(rightOperand) >= 0;
+ case "<":
+ return leftOperand.compareTo(rightOperand) < 0;
+ case ">":
+ return leftOperand.compareTo(rightOperand) > 0;
+ default:
+ BentoBox.getInstance().logError("Challenges PAPI formula error: Unsupported operator: " + operator);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Checks if the given token is one of the valid comparison operators.
+ */
+ private boolean isValidOperator(String token) {
+ return token.equals("=") || token.equals("==") || token.equals("<>") || token.equals("!=")
+ || token.equals("<=") || token.equals(">=") || token.equals("<") || token.equals(">");
+ }
+
+ /**
+ * Returns true if the token is a comparison operator.
+ */
+ private boolean isOperator(String token) {
+ return isValidOperator(token);
+ }
+
+ /**
+ * Returns true if the token is a boolean operator ("AND" or "OR").
+ */
+ private boolean isBooleanOperator(String token) {
+ return token.equalsIgnoreCase("AND") || token.equalsIgnoreCase("OR");
+ }
+
+ private boolean isAnd(String token) {
+ return token.equalsIgnoreCase("AND");
+ }
+
+ private boolean isOr(String token) {
+ return token.equalsIgnoreCase("OR");
+ }
+
+ /**
+ * Tries to parse the given string as a Double.
+ * Returns the Double if successful, or null if parsing fails.
+ */
+ private Double tryParseDouble(String s) {
+ try {
+ return Double.parseDouble(s);
+ } catch (NumberFormatException e) {
+ return null;
}
}
}
diff --git a/src/test/java/world/bentobox/challenges/database/object/requirements/CheckPapiTest.java b/src/test/java/world/bentobox/challenges/database/object/requirements/CheckPapiTest.java
new file mode 100644
index 0000000..b001391
--- /dev/null
+++ b/src/test/java/world/bentobox/challenges/database/object/requirements/CheckPapiTest.java
@@ -0,0 +1,102 @@
+package world.bentobox.challenges.database.object.requirements;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import org.bukkit.entity.Player;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(PlaceholderAPI.class)
+public class CheckPapiTest {
+
+ @Mock
+ private Player player;
+
+ @Before
+ public void setUp() {
+ PowerMockito.mockStatic(PlaceholderAPI.class, Mockito.RETURNS_MOCKS);
+ // Return back the input string
+ when(PlaceholderAPI.setPlaceholders(eq(player), anyString()))
+ .thenAnswer((Answer