diff --git a/common/src/main/java/me/lucko/luckperms/common/calculators/PermissionCalculator.java b/common/src/main/java/me/lucko/luckperms/common/calculators/PermissionCalculator.java index 2bce7a3b..e631bbef 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculators/PermissionCalculator.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculators/PermissionCalculator.java @@ -64,7 +64,7 @@ public class PermissionCalculator { Tristate result = lookupCache.get(permission); // log this permission lookup to the verbose handler - plugin.getVerboseHandler().offer(objectName, permission, result); + plugin.getVerboseHandler().offerCheckData(objectName, permission, result); // return the result return result; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/VerboseCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/VerboseCommand.java index da6feabf..22b16b83 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/VerboseCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/VerboseCommand.java @@ -36,6 +36,7 @@ import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.utils.Predicates; +import me.lucko.luckperms.common.verbose.VerboseFilter; import me.lucko.luckperms.common.verbose.VerboseListener; import net.kyori.text.Component; @@ -71,14 +72,14 @@ public class VerboseCommand extends SingleCommand { String filter = filters.isEmpty() ? "" : filters.stream().collect(Collectors.joining(" ")); - if (!VerboseListener.isValidFilter(filter)) { + if (!VerboseFilter.isValidFilter(filter)) { Message.VERBOSE_INVALID_FILTER.send(sender, filter); return CommandResult.FAILURE; } boolean notify = !mode.equals("record"); - plugin.getVerboseHandler().register(sender, filter, notify); + plugin.getVerboseHandler().registerListener(sender, filter, notify); if (notify) { if (!filter.equals("")) { @@ -98,7 +99,7 @@ public class VerboseCommand extends SingleCommand { } if (mode.equals("off") || mode.equals("false") || mode.equals("paste")) { - VerboseListener listener = plugin.getVerboseHandler().unregister(sender.getUuid()); + VerboseListener listener = plugin.getVerboseHandler().unregisterListener(sender.getUuid()); if (mode.equals("paste")) { if (listener == null) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/sender/AbstractSender.java b/common/src/main/java/me/lucko/luckperms/common/commands/sender/AbstractSender.java index cb3c26f1..46dd578d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/sender/AbstractSender.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/sender/AbstractSender.java @@ -136,4 +136,9 @@ public final class AbstractSender implements Sender { return this.uuid.equals(Constants.IMPORT_UUID); } + @Override + public boolean isValid() { + return ref.get() != null; + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/sender/Sender.java b/common/src/main/java/me/lucko/luckperms/common/commands/sender/Sender.java index 792cd355..63b4ee1a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/sender/Sender.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/sender/Sender.java @@ -112,4 +112,11 @@ public interface Sender { */ boolean isImport(); + /** + * Gets whether this sender is still valid & receiving messages. + * + * @return if this sender is valid + */ + boolean isValid(); + } diff --git a/common/src/main/java/me/lucko/luckperms/common/data/Importer.java b/common/src/main/java/me/lucko/luckperms/common/data/Importer.java index d3961f50..c7fed613 100644 --- a/common/src/main/java/me/lucko/luckperms/common/data/Importer.java +++ b/common/src/main/java/me/lucko/luckperms/common/data/Importer.java @@ -38,7 +38,6 @@ import me.lucko.luckperms.common.commands.sender.Sender; import me.lucko.luckperms.common.commands.utils.Util; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.utils.DateUtil; -import me.lucko.luckperms.common.utils.FakeSender; import java.util.ArrayList; import java.util.HashMap; @@ -58,7 +57,7 @@ public class Importer implements Runnable { private final Set notify; private final List commands; private final Map cmdResult; - private final FakeSender fake; + private final ImporterSender fake; private long lastMsg = 0; private int executing = -1; @@ -82,7 +81,7 @@ public class Importer implements Runnable { .collect(Collectors.toList()); this.cmdResult = new HashMap<>(); - this.fake = new FakeSender(commandManager.getPlugin(), this::logMessage); + this.fake = new ImporterSender(commandManager.getPlugin(), this::logMessage); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/utils/FakeSender.java b/common/src/main/java/me/lucko/luckperms/common/data/ImporterSender.java similarity index 94% rename from common/src/main/java/me/lucko/luckperms/common/utils/FakeSender.java rename to common/src/main/java/me/lucko/luckperms/common/data/ImporterSender.java index 29229af5..4390cb0a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/utils/FakeSender.java +++ b/common/src/main/java/me/lucko/luckperms/common/data/ImporterSender.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.utils; +package me.lucko.luckperms.common.data; import lombok.AllArgsConstructor; @@ -40,7 +40,7 @@ import java.util.UUID; import java.util.function.Consumer; @AllArgsConstructor -public class FakeSender implements Sender { +public class ImporterSender implements Sender { private final LuckPermsPlugin plugin; private final Consumer messageConsumer; @@ -93,4 +93,9 @@ public class FakeSender implements Sender { public boolean isImport() { return true; } + + @Override + public boolean isValid() { + return true; + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java b/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java index b1a2f491..8216a4b3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java @@ -35,24 +35,28 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * An immutable and sorted version of TreeNode * - * Entries in the children map are sorted first by whether they have any children, and then alphabetically + * Entries in the children map are sorted first by whether they have + * any children, and then alphabetically */ public class ImmutableTreeNode implements Comparable { private Map children = null; - public ImmutableTreeNode(Map children) { + public ImmutableTreeNode(Stream> children) { if (children != null) { - LinkedHashMap sortedMap = children.entrySet().stream() + LinkedHashMap sortedMap = children .sorted((o1, o2) -> { + // sort first by if the node has any children int childStatus = o1.getValue().compareTo(o2.getValue()); if (childStatus != 0) { return childStatus; } + // then alphabetically return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()); }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); @@ -65,6 +69,13 @@ public class ImmutableTreeNode implements Comparable { return Optional.ofNullable(children); } + /** + * Gets the node endings of each branch of the tree at this stage + * + * The key represents the depth of the node. + * + * @return the node endings + */ public List> getNodeEndings() { if (children == null) { return Collections.emptyList(); @@ -80,6 +91,7 @@ public class ImmutableTreeNode implements Comparable { results.addAll(node.getValue().getNodeEndings().stream() .map(e -> Maps.immutableEntry( e.getKey() + 1, // increment level + // add this node's key infront of the child value node.getKey() + "." + e.getValue()) ) .collect(Collectors.toList())); diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionVault.java b/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionVault.java index 6b1e2360..abd1c665 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionVault.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionVault.java @@ -30,9 +30,12 @@ import lombok.NonNull; import lombok.Setter; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; import java.util.List; import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; @@ -42,8 +45,14 @@ import java.util.concurrent.Executor; public class PermissionVault implements Runnable { private static final Splitter DOT_SPLIT = Splitter.on('.').omitEmptyStrings(); + // the root node in the tree @Getter private final TreeNode rootNode; + + // the known permissions already in the vault + private final Set knownPermissions; + + // a queue of permission strings to be processed by the tree private final Queue queue; @Setter @@ -51,6 +60,7 @@ public class PermissionVault implements Runnable { public PermissionVault(Executor executor) { rootNode = new TreeNode(); + knownPermissions = ConcurrentHashMap.newKeySet(3000); queue = new ConcurrentLinkedQueue<>(); executor.execute(this); @@ -61,7 +71,10 @@ public class PermissionVault implements Runnable { while (true) { for (String e; (e = queue.poll()) != null; ) { try { - insert(e.toLowerCase()); + String s = e.toLowerCase(); + if (knownPermissions.add(s)) { + insert(s); + } } catch (Exception ex) { ex.printStackTrace(); } @@ -81,13 +94,19 @@ public class PermissionVault implements Runnable { queue.offer(permission); } + public Set getKnownPermissions() { + return ImmutableSet.copyOf(knownPermissions); + } + public int getSize() { return rootNode.getDeepSize(); } private void insert(String permission) { + // split the permission up into parts List parts = DOT_SPLIT.splitToList(permission); + // insert the permission into the node structure TreeNode current = rootNode; for (String part : parts) { current = current.getChildMap().computeIfAbsent(part, s -> new TreeNode()); diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java index 0b58d0ec..12654db4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java @@ -25,13 +25,14 @@ package me.lucko.luckperms.common.treeview; +import com.google.common.collect.Maps; + import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** - * Represents one "branch" of the node tree + * Represents one "branch" or "level" of the node tree */ public class TreeNode { private Map children = null; @@ -60,7 +61,12 @@ public class TreeNode { if (children == null) { return new ImmutableTreeNode(null); } else { - return new ImmutableTreeNode(children.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().makeImmutableCopy()))); + return new ImmutableTreeNode(children.entrySet().stream() + .map(e -> Maps.immutableEntry( + e.getKey(), + e.getValue().makeImmutableCopy() + )) + ); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java index ec939e3e..123d425b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java @@ -42,62 +42,188 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +/** + * A readable view of a branch of {@link TreeNode}s. + */ public class TreeView { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); + // the root of the tree private final String rootPosition; - private final int maxLevels; + // how many levels / branches to display + private final int maxLevel; + + // the actual tree object private final ImmutableTreeNode view; - public TreeView(PermissionVault source, String rootPosition, int maxLevels) { + public TreeView(PermissionVault source, String rootPosition, int maxLevel) { this.rootPosition = rootPosition; - this.maxLevels = maxLevels; + this.maxLevel = maxLevel; - Optional root = findRoot(source); + Optional root = findRoot(rootPosition, source); this.view = root.map(TreeNode::makeImmutableCopy).orElse(null); } + /** + * Gets if this TreeView has any content. + * + * @return true if the treeview has data + */ public boolean hasData() { return view != null; } + /** + * Finds the root of the tree node at the given position + * + * @param source the node source + * @return the root, if it exists + */ + private static Optional findRoot(String rootPosition, PermissionVault source) { + // get the root of the permission vault + TreeNode root = source.getRootNode(); + + // just return the root + if (rootPosition.equals(".")) { + return Optional.of(root); + } + + // get the parts of the node + List parts = Splitter.on('.').omitEmptyStrings().splitToList(rootPosition); + + // for each part + for (String part : parts) { + + // check the current root has some children + if (!root.getChildren().isPresent()) { + return Optional.empty(); + } + + // get the current roots children + Map branch = root.getChildren().get(); + + // get the new root + root = branch.get(part); + if (root == null) { + return Optional.empty(); + } + } + + return Optional.of(root); + } + + /** + * Converts the view to a readable list + * + *

The list contains KV pairs, where the key is the tree padding/structure, + * and the value is the actual permission.

+ * + * @return a list of the nodes in this view + */ + private List> asTreeList() { + // work out the prefix to apply + // since the view is relative, we need to prepend this to all permissions + String prefix = rootPosition.equals(".") ? "" : (rootPosition + "."); + + + List> ret = new ArrayList<>(); + + // iterate the node endings in the view + for (Map.Entry s : view.getNodeEndings()) { + // don't include the node if it exceeds the max level + if (s.getKey() >= maxLevel) { + continue; + } + + // generate the tree padding characters from the node level + String treeStructure = Strings.repeat("│ ", s.getKey()) + "├── "; + // generate the permission, using the prefix and the node + String permission = prefix + s.getValue(); + + ret.add(Maps.immutableEntry(treeStructure, permission)); + } + + return ret; + } + + /** + * Uploads the data contained in this TreeView to a paste, and returns the URL. + * + * @param version the plugin version string + * @return the url, or null + * @see PasteUtils#paste(String, List) + */ public String uploadPasteData(String version) { + // only paste if there is actually data here if (!hasData()) { throw new IllegalStateException(); } + // get the data contained in the view in a list form + // for each entry, the key is the padding tree characters + // and the value is the actual permission string List> ret = asTreeList(); - ImmutableList.Builder builder = getPasteHeader(version, "none", ret.size()); - builder.add("```"); + // build the header of the paste + ImmutableList.Builder builder = getPasteHeader(version, "none", ret.size()); + + // add the tree data + builder.add("```"); for (Map.Entry e : ret) { builder.add(e.getKey() + e.getValue()); } - builder.add("```"); + + // clear the initial data map ret.clear(); + // upload the return the data return PasteUtils.paste("LuckPerms Permission Tree", ImmutableList.of(Maps.immutableEntry("luckperms-tree.md", builder.build().stream().collect(Collectors.joining("\n"))))); } + /** + * Uploads the data contained in this TreeView to a paste, and returns the URL. + * + *

Unlike {@link #uploadPasteData(String)}, this method will check each permission + * against a corresponding user, and colorize the output depending on the check results.

+ * + * @param version the plugin version string + * @param username the username of the reference user + * @param checker the permission data instance to check against + * @return the url, or null + * @see PasteUtils#paste(String, List) + */ public String uploadPasteData(String version, String username, PermissionData checker) { + // only paste if there is actually data here if (!hasData()) { throw new IllegalStateException(); } + // get the data contained in the view in a list form + // for each entry, the key is the padding tree characters + // and the value is the actual permission string List> ret = asTreeList(); - ImmutableList.Builder builder = getPasteHeader(version, username, ret.size()); - builder.add("```diff"); + // build the header of the paste + ImmutableList.Builder builder = getPasteHeader(version, username, ret.size()); + + // add the tree data + builder.add("```diff"); for (Map.Entry e : ret) { + + // lookup a permission value for the node Tristate tristate = checker.getPermissionValue(e.getValue()); + + // append the data to the paste builder.add(getTristateDiffPrefix(tristate) + e.getKey() + e.getValue()); } - builder.add("```"); + + // clear the initial data map ret.clear(); + // upload the return the data return PasteUtils.paste("LuckPerms Permission Tree", ImmutableList.of(Maps.immutableEntry("luckperms-tree.md", builder.build().stream().collect(Collectors.joining("\n"))))); } @@ -123,52 +249,9 @@ public class TreeView { .add("### Metadata") .add("| Selection | Max Recursion | Reference User | Size | Produced at |") .add("|-----------|---------------|----------------|------|-------------|") - .add("| " + selection + " | " + maxLevels + " | " + referenceUser + " | **" + size + "** | " + date + " |") + .add("| " + selection + " | " + maxLevel + " | " + referenceUser + " | **" + size + "** | " + date + " |") .add("") .add("### Output"); } - private Optional findRoot(PermissionVault source) { - TreeNode root = source.getRootNode(); - - if (rootPosition.equals(".")) { - return Optional.of(root); - } - - List parts = Splitter.on('.').omitEmptyStrings().splitToList(rootPosition); - for (String part : parts) { - - if (!root.getChildren().isPresent()) { - return Optional.empty(); - } - - Map branch = root.getChildren().get(); - - root = branch.get(part); - if (root == null) { - return Optional.empty(); - } - } - - return Optional.of(root); - } - - private List> asTreeList() { - String prefix = rootPosition.equals(".") ? "" : (rootPosition + "."); - List> ret = new ArrayList<>(); - - for (Map.Entry s : view.getNodeEndings()) { - if (s.getKey() >= maxLevels) { - continue; - } - - String treeStructure = Strings.repeat("│ ", s.getKey()) + "├── "; - String node = prefix + s.getValue(); - - ret.add(Maps.immutableEntry(treeStructure, node)); - } - - return ret; - } - } diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeViewBuilder.java b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeViewBuilder.java index 09b333ac..0f69bc47 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeViewBuilder.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeViewBuilder.java @@ -28,6 +28,9 @@ package me.lucko.luckperms.common.treeview; import lombok.Setter; import lombok.experimental.Accessors; +/** + * Builds a {@link TreeView}. + */ @Accessors(fluent = true) public class TreeViewBuilder { public static TreeViewBuilder newBuilder() { diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/CheckData.java b/common/src/main/java/me/lucko/luckperms/common/verbose/CheckData.java index 01b7c784..e0705ddd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/CheckData.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/CheckData.java @@ -30,12 +30,26 @@ import lombok.Getter; import me.lucko.luckperms.api.Tristate; +/** + * Holds the data from a permission check + */ @Getter @AllArgsConstructor public class CheckData { - private final String checked; - private final String node; - private final Tristate value; + /** + * The name of the entity which was checked + */ + private final String checkTarget; + + /** + * The permission which was checked for + */ + private final String permission; + + /** + * The result of the permission check + */ + private final Tristate result; } diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseFilter.java b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseFilter.java new file mode 100644 index 00000000..4031e99f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseFilter.java @@ -0,0 +1,141 @@ +package me.lucko.luckperms.common.verbose; + +import lombok.experimental.UtilityClass; + +import me.lucko.luckperms.common.utils.Scripting; + +import java.util.StringTokenizer; + +import javax.script.ScriptEngine; + +/** + * Tests verbose filters + */ +@UtilityClass +public class VerboseFilter { + + /** + * Evaluates whether the passed check data passes the filter + * + * @param data the check data + * @param filter the filter + * @return if the check data passes the filter + */ + public static boolean passesFilter(CheckData data, String filter) { + if (filter.equals("")) { + return true; + } + + // get the script engine + ScriptEngine engine = Scripting.getScriptEngine(); + if (engine == null) { + return false; + } + + // tokenize the filter + StringTokenizer tokenizer = new StringTokenizer(filter, " |&()!", true); + + // build an expression which can be evaluated by the javascript engine + StringBuilder expressionBuilder = new StringBuilder(); + + // read the tokens + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + + // if the token is a delimiter, just append it to the expression + if (isDelim(token)) { + expressionBuilder.append(token); + + } else { + + // if the token is not a delimiter, it must be a string. + // we replace non-delimiters with a boolean depending on if the string matches the check data. + boolean value = data.getCheckTarget().equalsIgnoreCase(token) || + data.getPermission().toLowerCase().startsWith(token.toLowerCase()) || + data.getResult().name().equalsIgnoreCase(token); + + expressionBuilder.append(value); + } + } + + // build the expression + String expression = expressionBuilder.toString().replace("&", "&&").replace("|", "||"); + + // evaluate the expression using the script engine + try { + String result = engine.eval(expression).toString(); + if (!result.equals("true") && !result.equals("false")) { + throw new IllegalArgumentException(expression + " - " + result); + } + + return Boolean.parseBoolean(result); + + } catch (Throwable t) { + t.printStackTrace(); + } + + return false; + } + + /** + * Tests whether a filter is valid + * + * @param filter the filter to test + * @return true if the filter is valid + */ + public static boolean isValidFilter(String filter) { + if (filter.equals("")) { + return true; + } + + // get the script engine + ScriptEngine engine = Scripting.getScriptEngine(); + if (engine == null) { + return false; + } + + // tokenize the filter + StringTokenizer tokenizer = new StringTokenizer(filter, " |&()!", true); + + // build an expression which can be evaluated by the javascript engine + StringBuilder expressionBuilder = new StringBuilder(); + + // read the tokens + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + + // if the token is a delimiter, just append it to the expression + if (isDelim(token)) { + expressionBuilder.append(token); + } else { + expressionBuilder.append("true"); // dummy result + } + } + + // build the expression + String expression = expressionBuilder.toString().replace("&", "&&").replace("|", "||"); + + // evaluate the expression using the script engine + try { + String result = engine.eval(expression).toString(); + if (!result.equals("true") && !result.equals("false")) { + throw new IllegalArgumentException(expression + " - " + result); + } + + return true; + + } catch (Throwable t) { + return false; + } + } + + private static boolean isDelim(String token) { + return token.equals(" ") || + token.equals("|") || + token.equals("&") || + token.equals("(") || + token.equals(")") || + token.equals("!"); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java index a4320ea3..f0848839 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java @@ -37,13 +37,22 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; +/** + * Accepts {@link CheckData} and passes it onto registered {@link VerboseListener}s. + */ public class VerboseHandler implements Runnable { private final String pluginVersion; + // the listeners currently registered private final Map listeners; + + // a queue of check data private final Queue queue; + + // if there are any listeners currently registered private boolean listening = false; + // if the handler should shutdown @Setter private boolean shutdown = false; @@ -55,31 +64,67 @@ public class VerboseHandler implements Runnable { executor.execute(this); } - public void offer(String checked, String node, Tristate value) { + /** + * Offers check data to the handler, to be eventually passed onto listeners. + * + *

The check data is added to a queue to be processed later, to avoid blocking + * the main thread each time a permission check is made.

+ * + * @param checkTarget the target of the permission check + * @param permission the permission which was checked for + * @param result the result of the permission check + */ + public void offerCheckData(String checkTarget, String permission, Tristate result) { + // don't bother even processing the check if there are no listeners registered if (!listening) { return; } - queue.offer(new CheckData(checked, node, value)); + // add the check data to a queue to be processed later. + queue.offer(new CheckData(checkTarget, permission, result)); } - public void register(Sender sender, String filter, boolean notify) { + /** + * Registers a new listener for the given player. + * + * @param sender the sender to notify, if notify is true + * @param filter the filter string + * @param notify if the sender should be notified in chat on each check + */ + public void registerListener(Sender sender, String filter, boolean notify) { listening = true; listeners.put(sender.getUuid(), new VerboseListener(pluginVersion, sender, filter, notify)); } - public VerboseListener unregister(UUID uuid) { + /** + * Removes a listener for a given player + * + * @param uuid the players uuid + * @return the existing listener, if one was actually registered + */ + public VerboseListener unregisterListener(UUID uuid) { + // immediately flush, so the listener gets all current data flush(); + VerboseListener ret = listeners.remove(uuid); + + // stop listening if there are no listeners left if (listeners.isEmpty()) { listening = false; } + return ret; } @Override public void run() { while (true) { + // remove listeners where the sender is no longer valid + listeners.values().removeIf(l -> !l.getNotifiedSender().isValid()); + if (listeners.isEmpty()) { + listening = false; + } + flush(); if (shutdown) { @@ -92,6 +137,9 @@ public class VerboseHandler implements Runnable { } } + /** + * Flushes the current check data to the listeners. + */ public synchronized void flush() { for (CheckData e; (e = queue.poll()) != null; ) { for (VerboseListener listener : listeners.values()) { diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java index 6d9a1379..d4549e36 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.common.verbose; +import lombok.Getter; import lombok.RequiredArgsConstructor; import com.google.common.collect.ImmutableList; @@ -35,49 +36,59 @@ import me.lucko.luckperms.common.commands.sender.Sender; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.utils.DateUtil; import me.lucko.luckperms.common.utils.PasteUtils; -import me.lucko.luckperms.common.utils.Scripting; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import java.util.stream.Collectors; -import javax.script.ScriptEngine; - +/** + * Accepts and processes {@link CheckData}, passed from the {@link VerboseHandler}. + */ @RequiredArgsConstructor public class VerboseListener { - private static final int DATA_TRUNCATION = 10000; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); - private static final Function TRISTATE_COLOR = tristate -> { - switch (tristate) { - case TRUE: - return "&2"; - case FALSE: - return "&c"; - default: - return "&7"; - } - }; + // how much data should we store before stopping. + private static final int DATA_TRUNCATION = 10000; + + // the time when the listener was first registered private final long startTime = System.currentTimeMillis(); + // the version of the plugin. (used when we paste data to gist) private final String pluginVersion; - private final Sender holder; + + // the sender to notify each time the listener processes a check which passes the filter + @Getter + private final Sender notifiedSender; + + // the filter string private final String filter; + + // if we should notify the sender private final boolean notify; + // the number of checks we have processed private final AtomicInteger counter = new AtomicInteger(0); - private final AtomicInteger matchedCounter = new AtomicInteger(0); - private final List results = new ArrayList<>(); + // the number of checks we have processed and accepted, based on the filter rules for this + // listener + private final AtomicInteger matchedCounter = new AtomicInteger(0); + + // the checks which passed the filter, up to a max size of #DATA_TRUNCATION + private final List results = new ArrayList<>(DATA_TRUNCATION / 10); + + /** + * Accepts and processes check data. + * + * @param data the data to process + */ public void acceptData(CheckData data) { counter.incrementAndGet(); - if (!matches(data, filter)) { + if (!VerboseFilter.passesFilter(data, filter)) { return; } matchedCounter.incrementAndGet(); @@ -87,98 +98,23 @@ public class VerboseListener { } if (notify) { - Message.VERBOSE_LOG.send(holder, "&a" + data.getChecked() + "&7 -- &a" + data.getNode() + "&7 -- " + TRISTATE_COLOR.apply(data.getValue()) + data.getValue().name().toLowerCase() + ""); + Message.VERBOSE_LOG.send(notifiedSender, "&a" + data.getCheckTarget() + "&7 -- &a" + data.getPermission() + "&7 -- " + getTristateColor(data.getResult()) + data.getResult().name().toLowerCase() + ""); } } - private static boolean matches(CheckData data, String filter) { - if (filter.equals("")) { - return true; - } - - ScriptEngine engine = Scripting.getScriptEngine(); - if (engine == null) { - return false; - } - - StringTokenizer tokenizer = new StringTokenizer(filter, " |&()!", true); - StringBuilder expression = new StringBuilder(); - - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (!isDelim(token)) { - boolean b = data.getChecked().equalsIgnoreCase(token) || - data.getNode().toLowerCase().startsWith(token.toLowerCase()) || - data.getValue().name().equalsIgnoreCase(token); - - token = "" + b; - } - - expression.append(token); - } - - try { - String exp = expression.toString().replace("&", "&&").replace("|", "||"); - String result = engine.eval(exp).toString(); - if (!result.equals("true") && !result.equals("false")) { - throw new IllegalArgumentException(exp + " - " + result); - } - - return Boolean.parseBoolean(result); - - } catch (Throwable t) { - t.printStackTrace(); - } - - return false; - } - - public static boolean isValidFilter(String filter) { - if (filter.equals("")) { - return true; - } - - ScriptEngine engine = Scripting.getScriptEngine(); - if (engine == null) { - return false; - } - - StringTokenizer tokenizer = new StringTokenizer(filter, " |&()!", true); - StringBuilder expression = new StringBuilder(); - - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (!isDelim(token)) { - token = "true"; // dummy result - } - - expression.append(token); - } - - try { - String exp = expression.toString().replace("&", "&&").replace("|", "||"); - String result = engine.eval(exp).toString(); - if (!result.equals("true") && !result.equals("false")) { - throw new IllegalArgumentException(exp + " - " + result); - } - - return true; - - } catch (Throwable t) { - return false; - } - } - - private static boolean isDelim(String token) { - return token.equals(" ") || token.equals("|") || token.equals("&") || token.equals("(") || token.equals(")") || token.equals("!"); - } - + /** + * Uploads the captured data in this listener to a paste and returns the url + * + * @return the url + * @see PasteUtils#paste(String, List) + */ public String uploadPasteData() { long now = System.currentTimeMillis(); String startDate = DATE_FORMAT.format(new Date(startTime)); String endDate = DATE_FORMAT.format(new Date(now)); long secondsTaken = (now - startTime) / 1000L; String duration = DateUtil.formatTime(secondsTaken); + String filter = this.filter; if (filter == null || filter.equals("")){ filter = "any"; @@ -186,7 +122,7 @@ public class VerboseListener { filter = "`" + filter + "`"; } - ImmutableList.Builder output = ImmutableList.builder() + ImmutableList.Builder prettyOutput = ImmutableList.builder() .add("## Verbose Checking Output") .add("#### This file was automatically generated by [LuckPerms](https://github.com/lucko/LuckPerms) " + pluginVersion) .add("") @@ -197,33 +133,33 @@ public class VerboseListener { .add("| End Time | " + endDate + " |") .add("| Duration | " + duration +" |") .add("| Count | **" + matchedCounter.get() + "** / " + counter + " |") - .add("| User | " + holder.getName() + " |") + .add("| User | " + notifiedSender.getName() + " |") .add("| Filter | " + filter + " |") .add(""); if (matchedCounter.get() > results.size()) { - output.add("**WARN:** Result set exceeded max size of " + DATA_TRUNCATION + ". The output below was truncated to " + DATA_TRUNCATION + " entries."); - output.add(""); + prettyOutput.add("**WARN:** Result set exceeded max size of " + DATA_TRUNCATION + ". The output below was truncated to " + DATA_TRUNCATION + " entries."); + prettyOutput.add(""); } - output.add("### Output") + prettyOutput.add("### Output") .add("Format: `` `` ``") .add("") .add("___") .add(""); - ImmutableList.Builder data = ImmutableList.builder() + ImmutableList.Builder csvOutput = ImmutableList.builder() .add("User,Permission,Result"); - results.stream() - .peek(c -> output.add("`" + c.getChecked() + "` - " + c.getNode() + " - **" + c.getValue().toString() + "** ")) - .forEach(c -> data.add(escapeCommas(c.getChecked()) + "," + escapeCommas(c.getNode()) + "," + c.getValue().name().toLowerCase())); - + results.forEach(c -> { + prettyOutput.add("`" + c.getCheckTarget() + "` - " + c.getPermission() + " - **" + c.getResult().toString() + "** "); + csvOutput.add(escapeCommas(c.getCheckTarget()) + "," + escapeCommas(c.getPermission()) + "," + c.getResult().name().toLowerCase()); + }); results.clear(); List> content = ImmutableList.of( - Maps.immutableEntry("luckperms-verbose.md", output.build().stream().collect(Collectors.joining("\n"))), - Maps.immutableEntry("raw-data.csv", data.build().stream().collect(Collectors.joining("\n"))) + Maps.immutableEntry("luckperms-verbose.md", prettyOutput.build().stream().collect(Collectors.joining("\n"))), + Maps.immutableEntry("raw-data.csv", csvOutput.build().stream().collect(Collectors.joining("\n"))) ); return PasteUtils.paste("LuckPerms Verbose Checking Output", content); @@ -233,4 +169,15 @@ public class VerboseListener { return s.contains(",") ? "\"" + s + "\"" : s; } + private static String getTristateColor(Tristate tristate) { + switch (tristate) { + case TRUE: + return "&2"; + case FALSE: + return "&c"; + default: + return "&7"; + } + } + } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index 72436c42..4d1dd39b 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -45,6 +45,7 @@ import me.lucko.luckperms.common.config.LuckPermsConfiguration; import me.lucko.luckperms.common.constants.CommandPermission; import me.lucko.luckperms.common.contexts.ContextManager; import me.lucko.luckperms.common.contexts.LuckPermsCalculator; +import me.lucko.luckperms.common.data.ImporterSender; import me.lucko.luckperms.common.dependencies.DependencyManager; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.NoopLocaleManager; @@ -66,7 +67,6 @@ import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.common.tasks.UpdateTask; import me.lucko.luckperms.common.treeview.PermissionVault; -import me.lucko.luckperms.common.utils.FakeSender; import me.lucko.luckperms.common.utils.UuidCache; import me.lucko.luckperms.common.verbose.VerboseHandler; import me.lucko.luckperms.sponge.calculators.SpongeCalculatorFactory; @@ -497,7 +497,7 @@ public class LPSpongePlugin implements LuckPermsPlugin { @Override public Sender getConsoleSender() { if (!game.isServerAvailable()) { - return new FakeSender(this, s -> logger.info(s)); + return new ImporterSender(this, s -> logger.info(s)); } return getSenderFactory().wrap(game.getServer().getConsole()); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsPermissionDescription.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsPermissionDescription.java index 2af9413e..579b8cc7 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsPermissionDescription.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsPermissionDescription.java @@ -45,7 +45,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; @RequiredArgsConstructor -@EqualsAndHashCode(of = {"id", "description", "owner"}) +@EqualsAndHashCode(of = "id") @ToString(of = {"id", "description", "owner"}) public final class LuckPermsPermissionDescription implements LPPermissionDescription { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java index 30bd1a3f..1d68fb48 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java @@ -73,6 +73,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -202,7 +203,19 @@ public class LuckPermsService implements LPPermissionService { @Override public ImmutableSet getDescriptions() { - return ImmutableSet.copyOf(descriptionSet); + Set descriptions = new HashSet<>(descriptionSet); + + // collect known values from the permission vault + for (String knownPermission : plugin.getPermissionVault().getKnownPermissions()) { + LPPermissionDescription desc = new LuckPermsPermissionDescription(this, knownPermission, null, null); + + // don't override plugin defined values + if (!descriptions.contains(desc)) { + descriptions.add(desc); + } + } + + return ImmutableSet.copyOf(descriptions); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/PersistedSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/PersistedSubject.java index 342e00ad..1afa22d3 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/PersistedSubject.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/PersistedSubject.java @@ -247,7 +247,7 @@ public class PersistedSubject implements LPSubject { public Tristate getPermissionValue(@NonNull ImmutableContextSet contexts, @NonNull String node) { try (Timing ignored = service.getPlugin().getTimings().time(LPTiming.INTERNAL_SUBJECT_GET_PERMISSION_VALUE)) { Tristate t = permissionLookupCache.get(PermissionLookup.of(node, contexts)); - service.getPlugin().getVerboseHandler().offer("local:" + getParentCollection().getIdentifier() + "/" + identifier, node, t); + service.getPlugin().getVerboseHandler().offerCheckData("local:" + getParentCollection().getIdentifier() + "/" + identifier, node, t); return t; } }