Implement the custom parser in HologramFormat (remove nashorn engine)

This commit is contained in:
Flowsqy 2022-02-24 00:06:14 +01:00
parent 96291d46c3
commit 05969a2fcc
7 changed files with 468 additions and 278 deletions

View File

@ -1,103 +1,200 @@
package de.epiceric.shopchest.config.hologram;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import de.epiceric.shopchest.ShopChest;
import de.epiceric.shopchest.config.Placeholder;
import de.epiceric.shopchest.config.hologram.condition.Condition;
import de.epiceric.shopchest.config.hologram.line.FormatReplacer;
import de.epiceric.shopchest.config.hologram.line.FormattedLine;
import de.epiceric.shopchest.config.hologram.parser.FormatParser;
import de.epiceric.shopchest.config.hologram.parser.ParserResult;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import de.epiceric.shopchest.ShopChest;
import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class HologramFormat {
/*
TODO Change implementation of this class
-> Deserialize from the configuration and load it some way to avoid String manipulation at each method invocation
-> Rework the current String manipulation process to add complex expression evaluation WITHOUT nashorn engine
*/
public enum Requirement {
VENDOR, AMOUNT, ITEM_TYPE, ITEM_NAME, HAS_ENCHANTMENT, BUY_PRICE,
SELL_PRICE, HAS_POTION_EFFECT, IS_MUSIC_DISC, IS_POTION_EXTENDED, IS_BANNER_PATTERN,
IS_WRITTEN_BOOK, ADMIN_SHOP, NORMAL_SHOP, IN_STOCK, MAX_STACK, CHEST_SPACE, DURABILITY
}
// no "-" sign since no variable can be negative
// e.g.: 100.0 >= 50.0
private static final Pattern SIMPLE_NUMERIC_CONDITION = Pattern.compile("^(\\d+(?:\\.\\d+)?) ([<>][=]?|[=!]=) (\\d+(?:\\.\\d+)?)$");
// e.g.: "STONE" == "DIAMOND_SWORD"
private static final Pattern SIMPLE_STRING_CONDITION = Pattern.compile("^\"([^\"]*)\" ([=!]=) \"([^\"]*)\"$");
private ScriptEngineManager manager = new ScriptEngineManager();
private ScriptEngine engine = manager.getEngineByName("JavaScript");
private ShopChest plugin;
private File configFile;
private YamlConfiguration config;
private final ShopChest plugin;
private HologramLine[] lines;
public HologramFormat(ShopChest plugin) {
this.configFile = new File(plugin.getDataFolder(), "hologram-format.yml");
this.config = YamlConfiguration.loadConfiguration(configFile);
this.plugin = plugin;
}
public void load() {
// Load file
final File configFile = new File(plugin.getDataFolder(), "hologram-format.yml");
final YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
// Get lines
final ConfigurationSection linesSection = config.getConfigurationSection("lines");
if (linesSection == null) {
// TODO Inform that there is no lines section
return;
}
// Get options
final Map<String, ConfigurationSection> optionSections = new HashMap<>();
for (String linesId : linesSection.getKeys(false)) {
final ConfigurationSection lineSection = linesSection.getConfigurationSection(linesId);
if (lineSection == null) {
// TODO Inform that a line must be a section
continue;
}
final ConfigurationSection optionSection = lineSection.getConfigurationSection("options");
if (optionSection == null) {
// TODO Inform that a line must contain an option section
continue;
}
optionSections.put(linesId, optionSection);
}
// Sort lines by id
final List<ConfigurationSection> orderedOptionSections = optionSections.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
// Prepare formatter
final FormatData data = new FormatData();
final FormatParser parser = new FormatParser();
// Deserialize each option
final List<HologramLine> lines = new ArrayList<>();
// For every line
for (ConfigurationSection optionsSection : orderedOptionSections) {
final List<HologramOption> options = new LinkedList<>();
// For every option of the line
for (final String optionKey : optionsSection.getKeys(false)) {
final ConfigurationSection optionSection = optionsSection.getConfigurationSection(optionKey);
if (optionSection == null) {
// TODO Inform that an 'options' key must refer to a section
continue;
}
// Get the requirements
final List<Condition<Map<Requirement, Object>>> requirementConditions = new LinkedList<>();
for (String requirement : optionSection.getStringList("requirements")) {
if (requirement == null) {
continue;
}
final ParserResult<Requirement> result;
try {
result = parser.parse(
requirement,
data.getRequirements(),
data.getRequirementsTypes()
);
} catch (Exception e) {
// TODO Inform that it can not be deserialized
continue;
}
if (result.isCondition()) {
requirementConditions.add(result.getCondition());
continue;
}
// TODO Inform that there is a requirement that is not a condition
}
// Get the format
final String format = optionSection.getString("format");
if (format == null) {
// TODO Inform that format does not exist for this option
continue;
}
final FormattedLine<Placeholder> formattedString = evaluateFormat(format, parser, data);
// Add the option
options.add(new HologramOption(
formattedString,
requirementConditions.isEmpty() ? Collections.emptyList() : requirementConditions
));
// There is no requirement for this option, so it's the last
// (it will always be picked so the next options are skipped)
if (requirementConditions.isEmpty()) {
break;
}
}
if (options.isEmpty()) {
// TODO Inform that this line does not contain any valid option
continue;
}
// Add the line
lines.add(new HologramLine(new ArrayList<>(options)));
}
this.lines = lines.toArray(new HologramLine[0]);
}
private FormattedLine<Placeholder> evaluateFormat(String format, FormatParser parser, FormatData data) {
final FormatReplacer<Placeholder> formatReplacer = new FormatReplacer<>(format);
// Detect and evaluate accolade inner parts
final Map<String, ParserResult<Placeholder>> parsedScripts = new HashMap<>();
final Matcher matcher = Pattern.compile("\\{([^}]+)}").matcher(format);
while (matcher.find()) {
final String withBrackets = matcher.group();
final String script = withBrackets.substring(1, withBrackets.length() - 1);
final ParserResult<Placeholder> result;
try {
result = parser.parse(script, data.getPlaceholders(), data.getPlaceholderTypes());
} catch (Exception e) {
// TODO Inform that the script can not be deserialized
parsedScripts.put(withBrackets, new ParserResult<>(null, null, null, null));
continue;
}
parsedScripts.put(withBrackets, result);
}
// Replace accolade inner parts
for (Map.Entry<String, ParserResult<Placeholder>> entry : parsedScripts.entrySet()) {
final String regex = entry.getKey();
final ParserResult<Placeholder> result = entry.getValue();
if (result.isConstant()) {
formatReplacer.replace(regex, String.valueOf(result.getConstant()));
} else if (result.isValue()) {
formatReplacer.replace(regex, new FormattedLine.ProviderToString<>(result.getValue()));
} else if (result.isCondition()) {
formatReplacer.replace(regex, new FormattedLine.ConditionToString<>(result.getCondition()));
} else if (result.isCalculation()) {
formatReplacer.replace(regex, new FormattedLine.CalculationToString<>(result.getCalculation()));
} else {
formatReplacer.replace(regex, "");
}
}
// Replace classics placeholders
for (Map.Entry<String, Placeholder> entry : data.getPlaceholders().entrySet()) {
formatReplacer.replace(entry.getKey(), new FormattedLine.MapToString<>(entry.getValue()));
}
return formatReplacer.create();
}
/**
* Get the format for the given line of the hologram
* @param line Line of the hologram
*
* @param line Line of the hologram
* @param reqMap Values of the requirements that might be needed by the format (contains {@code null} if not comparable)
* @param plaMap Values of the placeholders that might be needed by the format
* @return The format of the first working option, or an empty String if no option is working
* because of not fulfilled requirements
* @return The format of the first working option, or an empty String if no option is working
* because of not fulfilled requirements
*/
public String getFormat(int line, Map<Requirement, Object> reqMap, Map<Placeholder, Object> plaMap) {
ConfigurationSection options = config.getConfigurationSection("lines." + line + ".options");
// For every option
optionLoop:
for (String key : options.getKeys(false)) {
ConfigurationSection option = options.getConfigurationSection(key);
List<String> requirements = option.getStringList("requirements");
// Check every requirement
for (String sReq : requirements) {
//TODO Maybe remove some loops as every requirements are re evaluated in the #evalRequirement
for (Requirement req : reqMap.keySet()) {
// If the configuration requirement contain a requirement specified by the shop
if (sReq.contains(req.toString())) {
// Then evaluate the requirement.
// If the requirement is not fulfilled, skip this option and go to the next one
if (!evalRequirement(sReq, reqMap)) {
continue optionLoop;
}
// TODO Maybe skip to the next config requirement as the requirement has been found and is valid
}
}
}
// Here, every requirement is fulfilled and this is the first valid option
String format = option.getString("format");
// Evaluate placeholders and return the formatted line
return evalPlaceholder(format, plaMap);
}
// No option matching to que shop requirements
// Returning an empty string
return "";
return lines[line].get(reqMap, plaMap);
}
public void reload() {
config = YamlConfiguration.loadConfiguration(configFile);
lines = null;
load();
}
/**
@ -107,6 +204,8 @@ public class HologramFormat {
// Return whether an option contains STOCK or CHEST_SPACE :
// - In the format
// - In one of its requirement
// TODO Implement this
/*
int count = getLineCount();
for (int i = 0; i < count; i++) {
ConfigurationSection options = config.getConfigurationSection("lines." + i + ".options");
@ -126,7 +225,7 @@ public class HologramFormat {
}
}
}
*/
return false;
}
@ -134,138 +233,25 @@ public class HologramFormat {
* @return Amount of lines in a hologram
*/
public int getLineCount() {
return config.getConfigurationSection("lines").getKeys(false).size();
if (lines == null) {
throw new IllegalStateException("The hologram format is loaded");
}
return lines.length;
}
/**
* @return Configuration of the "hologram-format.yml" file
* @deprecated The configuration is not used during runtime.
* If you invoke this method, you will load the configuration from the disk.
*/
@Deprecated
public YamlConfiguration getConfig() {
return config;
return YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder(), "hologram-format.yml"));
}
/**
* Parse and evaluate a condition
* @param condition Condition to evaluate
* @param values Values of the requirements
* @return Result of the condition
*/
public boolean evalRequirement(String condition, Map<Requirement, Object> values) {
String cond = condition;
// Double-check the presence of a requirement (WTF ?)
for (HologramFormat.Requirement req : HologramFormat.Requirement.values()) {
if (cond.contains(req.toString()) && values.containsKey(req)) {
Object val = values.get(req);
String sVal = String.valueOf(val);
if (val instanceof String && !(sVal.startsWith("\"") && sVal.endsWith("\""))) {
sVal = String.format("\"%s\"", sVal);
}
cond = cond.replace(req.toString(), sVal);
}
}
// Evaluate three basic condition : Direct Boolean, Math comparison and String equality
if (cond.equals("true")) {
// e.g.: ADMIN_SHOP
return true;
} else if (cond.equals("false")) {
return false;
} else {
char firstChar = cond.charAt(0);
// numeric cond: first char must be a digit (no variable can be negative)
if (firstChar >= '0' && firstChar <= '9') {
Matcher matcher = SIMPLE_NUMERIC_CONDITION.matcher(cond);
if (matcher.find()) {
double a, b;
Operator operator;
try {
a = Double.parseDouble(matcher.group(1));
operator = Operator.from(matcher.group(2));
b = Double.parseDouble(matcher.group(3));
return operator.compare(a, b);
} catch (IllegalArgumentException ignored) {
// should not happen, since regex checked that there is valid number and valid operator
}
}
}
// string cond: first char must be a: "
if (firstChar == '"') {
Matcher matcher = SIMPLE_STRING_CONDITION.matcher(cond);
if (matcher.find()) {
String a, b;
Operator operator;
try {
a = matcher.group(1);
operator = Operator.from(matcher.group(2));
b = matcher.group(3);
return operator.compare(a, b);
} catch (IllegalArgumentException | UnsupportedOperationException ignored) {
// should not happen, since regex checked that there is valid operator
}
}
}
// complex comparison
// Like && and || or other arithmetic operations
try {
return (boolean) engine.eval(cond);
} catch (ScriptException e) {
plugin.debug("Failed to eval condition: " + condition);
plugin.debug(e);
return false;
}
}
}
/**
* Parse and evaluate a condition
* @param string Message or hologram format whose containing scripts to execute
* @param values Values of the placeholders
* @return Result of the condition
*/
public String evalPlaceholder(String string, Map<Placeholder, Object> values) {
// Detect and evaluate accolade inner parts
try {
Matcher matcher = Pattern.compile("\\{([^}]+)}").matcher(string);
String newString = string;
while (matcher.find()) {
String withBrackets = matcher.group();
String script = withBrackets.substring(1, withBrackets.length() - 1);
for (Placeholder placeholder : values.keySet()) {
if (script.contains(placeholder.toString())) {
Object val = values.get(placeholder);
String sVal = String.valueOf(val);
if (val instanceof String && !(sVal.startsWith("\"") && sVal.endsWith("\""))) {
sVal = String.format("\"%s\"", sVal);
}
script = script.replace(placeholder.toString(), sVal);
}
}
String result = String.valueOf(engine.eval(script));
newString = newString.replace(withBrackets, result);
}
return newString;
} catch (ScriptException e) {
plugin.debug("Failed to eval placeholder script in string: " + string);
plugin.debug(e);
}
return string;
public enum Requirement {
VENDOR, AMOUNT, ITEM_TYPE, ITEM_NAME, HAS_ENCHANTMENT, BUY_PRICE,
SELL_PRICE, HAS_POTION_EFFECT, IS_MUSIC_DISC, IS_POTION_EXTENDED, IS_BANNER_PATTERN,
IS_WRITTEN_BOOK, ADMIN_SHOP, NORMAL_SHOP, IN_STOCK, MAX_STACK, CHEST_SPACE, DURABILITY
}
}

View File

@ -0,0 +1,25 @@
package de.epiceric.shopchest.config.hologram;
import de.epiceric.shopchest.config.Placeholder;
import java.util.List;
import java.util.Map;
public class HologramLine {
private final List<HologramOption> options;
public HologramLine(List<HologramOption> options) {
this.options = options;
}
public final String get(Map<HologramFormat.Requirement, Object> reqMap, Map<Placeholder, Object> plaMap) {
for (HologramOption option : options) {
if (option.isValid(reqMap)) {
return option.getFormat(plaMap);
}
}
return "";
}
}

View File

@ -0,0 +1,36 @@
package de.epiceric.shopchest.config.hologram;
import de.epiceric.shopchest.config.Placeholder;
import de.epiceric.shopchest.config.hologram.condition.Condition;
import de.epiceric.shopchest.config.hologram.line.FormattedLine;
import java.util.List;
import java.util.Map;
public class HologramOption {
private final FormattedLine<Placeholder> formattedString;
private final List<Condition<Map<HologramFormat.Requirement, Object>>> requirements;
public HologramOption(FormattedLine<Placeholder> formattedString, List<Condition<Map<HologramFormat.Requirement, Object>>> requirements) {
this.formattedString = formattedString;
this.requirements = requirements;
}
public boolean isValid(Map<HologramFormat.Requirement, Object> requirementsValues) {
if (requirementsValues == null) {
return true;
}
for (Condition<Map<HologramFormat.Requirement, Object>> condition : requirements) {
if (!condition.test(requirementsValues)) {
return false;
}
}
return true;
}
public String getFormat(Map<Placeholder, Object> placeholderValues) {
return formattedString.get(placeholderValues);
}
}

View File

@ -1,75 +0,0 @@
package de.epiceric.shopchest.config.hologram;
public enum Operator {
EQUAL("==") {
@Override
public boolean compare(double a, double b) {
return Double.compare(a, b) == 0;
}
@Override
public boolean compare(String a, String b) {
return a.equals(b);
}
},
NOT_EQUAL("!=") {
@Override
public boolean compare(double a, double b) {
return Double.compare(a, b) != 0;
}
@Override
public boolean compare(String a, String b) {
return !a.equals(b);
}
},
GREATER_THAN(">") {
@Override
public boolean compare(double a, double b) {
return a > b;
}
},
GREATER_THAN_OR_EQUAL(">=") {
@Override
public boolean compare(double a, double b) {
return a >= b;
}
},
LESS_THAN("<") {
@Override
public boolean compare(double a, double b) {
return a < b;
}
},
LESS_THAN_OR_EQUAL("<=") {
@Override
public boolean compare(double a, double b) {
return a <= b;
}
};
private final String symbol;
Operator(String symbol) {
this.symbol = symbol;
}
public static Operator from(String symbol) {
for (Operator operator : values()) {
if (operator.symbol.equals(symbol)) {
return operator;
}
}
throw new IllegalArgumentException();
}
public abstract boolean compare(double a, double b);
public boolean compare(String a, String b) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,132 @@
package de.epiceric.shopchest.config.hologram.line;
import de.epiceric.shopchest.config.hologram.provider.ConstantProvider;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FormatReplacer<P> {
private List<Token<P>> tokens;
public FormatReplacer(String original) {
Objects.requireNonNull(original);
this.tokens = new ArrayList<>(Collections.singletonList(new Token.StringToken<>(original)));
}
public FormatReplacer<P> replace(String regex, String replacement) {
final List<Token<P>> outputTokens = new ArrayList<>();
for (final Token<P> token : tokens) {
if (token.isString()) {
outputTokens.add(new Token.StringToken<>(token.getString().replace(regex, replacement)));
} else {
outputTokens.add(token);
}
}
this.tokens = outputTokens;
return this;
}
public FormatReplacer<P> replace(String regex, Function<Map<P, Object>, String> dynamic) {
final List<Token<P>> outputTokens = new ArrayList<>();
for (final Token<P> token : tokens) {
if (token.isString()) {
outputTokens.addAll(getTokenReplacement(token.getString(), regex, dynamic));
} else {
outputTokens.add(token);
}
}
this.tokens = outputTokens;
return this;
}
public FormattedLine<P> create() {
final List<Function<Map<P, Object>, String>> components = new ArrayList<>();
tokens.stream().map(Token::getDynamics).map(Arrays::asList).forEach(components::addAll);
return new FormattedLine<>(components);
}
private List<Token<P>> getTokenReplacement(String input, String regex, Function<Map<P, Object>, String> dynamic) {
final List<Token<P>> tokens = new ArrayList<>();
final Matcher matcher = Pattern.compile(regex, Pattern.LITERAL).matcher(input);
if (matcher.find()) {
int cursor = 0;
do {
final String pre = input.substring(cursor, matcher.start());
if (!pre.isEmpty()) {
tokens.add(new Token.StringToken<>(pre));
}
tokens.add(new Token.DynamicsToken<>(dynamic));
cursor = matcher.end();
} while (matcher.find());
final String end = input.substring(cursor);
if (!end.isEmpty()) {
tokens.add(new Token.StringToken<>(end));
}
return tokens;
}
tokens.add(new Token.StringToken<>(input));
return tokens;
}
public interface Token<P> {
boolean isString();
String getString();
Function<Map<P, Object>, String> getDynamics();
final class DynamicsToken<P> implements Token<P> {
private final Function<Map<P, Object>, String> dynamics;
public DynamicsToken(Function<Map<P, Object>, String> dynamics) {
this.dynamics = dynamics;
}
@Override
public boolean isString() {
return false;
}
@Override
public String getString() {
throw new UnsupportedOperationException();
}
@Override
public Function<Map<P, Object>, String> getDynamics() {
return dynamics;
}
}
final class StringToken<P> implements Token<P> {
private final String string;
public StringToken(String string) {
this.string = string;
}
@Override
public boolean isString() {
return true;
}
@Override
public String getString() {
return string;
}
@Override
public Function<Map<P, Object>, String> getDynamics() {
return new ConstantProvider<>(string);
}
}
}
}

View File

@ -0,0 +1,85 @@
package de.epiceric.shopchest.config.hologram.line;
import de.epiceric.shopchest.config.hologram.calculation.Calculation;
import de.epiceric.shopchest.config.hologram.condition.Condition;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class FormattedLine<P> {
private final List<Function<Map<P, Object>, String>> tokens;
public FormattedLine(List<Function<Map<P, Object>, String>> tokens) {
this.tokens = tokens;
}
public String get(Map<P, Object> values) {
final StringBuilder output = new StringBuilder();
for (Function<Map<P, Object>, String> token : tokens) {
output.append(token.apply(values));
}
return output.toString();
}
public final static class MapToString<P> implements Function<Map<P, Object>, String> {
private final P key;
public MapToString(P key) {
this.key = key;
}
@Override
public String apply(Map<P, Object> values) {
final Object o = values.get(key);
return o == null ? "" : o.toString();
}
}
public final static class ProviderToString<P> implements Function<Map<P, Object>, String> {
private final Function<Map<P, Object>, ?> provider;
public ProviderToString(Function<Map<P, Object>, ?> provider) {
this.provider = provider;
}
@Override
public String apply(Map<P, Object> pObjectMap) {
return String.valueOf(provider);
}
}
public final static class ConditionToString<P> implements Function<Map<P, Object>, String> {
private final Condition<Map<P, Object>> condition;
public ConditionToString(Condition<Map<P, Object>> condition) {
this.condition = condition;
}
@Override
public String apply(Map<P, Object> values) {
return String.valueOf(condition.test(values));
}
}
public final static class CalculationToString<P> implements Function<Map<P, Object>, String> {
private final Calculation<Map<P, Object>> calculation;
public CalculationToString(Calculation<Map<P, Object>> calculation) {
this.calculation = calculation;
}
@Override
public String apply(Map<P, Object> values) {
return String.valueOf(calculation.apply(values));
}
}
}

View File

@ -19,6 +19,7 @@ public class Token<T> {
public final static TokenType<List<Token<?>>> NODE = new TokenType<>("Node");
public final static TokenType<Calculation<?>> CALCULATION = new TokenType<>("Calculation");
public final static TokenType<Condition<?>> CONDITION = new TokenType<>("Condition");
private final TokenType<T> type;
private final T value;