Cleanup verbose & treeview packages. Return known permissions from the PermissionVault as Sponge PermissionDescriptions

This commit is contained in:
Luck 2017-08-13 12:07:05 +02:00
parent 1d5e3205ac
commit d98b464ce9
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
19 changed files with 500 additions and 197 deletions

View File

@ -64,7 +64,7 @@ public class PermissionCalculator {
Tristate result = lookupCache.get(permission); Tristate result = lookupCache.get(permission);
// log this permission lookup to the verbose handler // log this permission lookup to the verbose handler
plugin.getVerboseHandler().offer(objectName, permission, result); plugin.getVerboseHandler().offerCheckData(objectName, permission, result);
// return the result // return the result
return result; return result;

View File

@ -36,6 +36,7 @@ import me.lucko.luckperms.common.locale.LocaleManager;
import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.Message;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.utils.Predicates; import me.lucko.luckperms.common.utils.Predicates;
import me.lucko.luckperms.common.verbose.VerboseFilter;
import me.lucko.luckperms.common.verbose.VerboseListener; import me.lucko.luckperms.common.verbose.VerboseListener;
import net.kyori.text.Component; import net.kyori.text.Component;
@ -71,14 +72,14 @@ public class VerboseCommand extends SingleCommand {
String filter = filters.isEmpty() ? "" : filters.stream().collect(Collectors.joining(" ")); String filter = filters.isEmpty() ? "" : filters.stream().collect(Collectors.joining(" "));
if (!VerboseListener.isValidFilter(filter)) { if (!VerboseFilter.isValidFilter(filter)) {
Message.VERBOSE_INVALID_FILTER.send(sender, filter); Message.VERBOSE_INVALID_FILTER.send(sender, filter);
return CommandResult.FAILURE; return CommandResult.FAILURE;
} }
boolean notify = !mode.equals("record"); boolean notify = !mode.equals("record");
plugin.getVerboseHandler().register(sender, filter, notify); plugin.getVerboseHandler().registerListener(sender, filter, notify);
if (notify) { if (notify) {
if (!filter.equals("")) { if (!filter.equals("")) {
@ -98,7 +99,7 @@ public class VerboseCommand extends SingleCommand {
} }
if (mode.equals("off") || mode.equals("false") || mode.equals("paste")) { 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 (mode.equals("paste")) {
if (listener == null) { if (listener == null) {

View File

@ -136,4 +136,9 @@ public final class AbstractSender<T> implements Sender {
return this.uuid.equals(Constants.IMPORT_UUID); return this.uuid.equals(Constants.IMPORT_UUID);
} }
@Override
public boolean isValid() {
return ref.get() != null;
}
} }

View File

@ -112,4 +112,11 @@ public interface Sender {
*/ */
boolean isImport(); boolean isImport();
/**
* Gets whether this sender is still valid & receiving messages.
*
* @return if this sender is valid
*/
boolean isValid();
} }

View File

@ -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.commands.utils.Util;
import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.Message;
import me.lucko.luckperms.common.utils.DateUtil; import me.lucko.luckperms.common.utils.DateUtil;
import me.lucko.luckperms.common.utils.FakeSender;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -58,7 +57,7 @@ public class Importer implements Runnable {
private final Set<Sender> notify; private final Set<Sender> notify;
private final List<String> commands; private final List<String> commands;
private final Map<Integer, Result> cmdResult; private final Map<Integer, Result> cmdResult;
private final FakeSender fake; private final ImporterSender fake;
private long lastMsg = 0; private long lastMsg = 0;
private int executing = -1; private int executing = -1;
@ -82,7 +81,7 @@ public class Importer implements Runnable {
.collect(Collectors.toList()); .collect(Collectors.toList());
this.cmdResult = new HashMap<>(); this.cmdResult = new HashMap<>();
this.fake = new FakeSender(commandManager.getPlugin(), this::logMessage); this.fake = new ImporterSender(commandManager.getPlugin(), this::logMessage);
} }
@Override @Override

View File

@ -23,7 +23,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package me.lucko.luckperms.common.utils; package me.lucko.luckperms.common.data;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -40,7 +40,7 @@ import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
@AllArgsConstructor @AllArgsConstructor
public class FakeSender implements Sender { public class ImporterSender implements Sender {
private final LuckPermsPlugin plugin; private final LuckPermsPlugin plugin;
private final Consumer<String> messageConsumer; private final Consumer<String> messageConsumer;
@ -93,4 +93,9 @@ public class FakeSender implements Sender {
public boolean isImport() { public boolean isImport() {
return true; return true;
} }
@Override
public boolean isValid() {
return true;
}
} }

View File

@ -35,24 +35,28 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* An immutable and sorted version of TreeNode * 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<ImmutableTreeNode> { public class ImmutableTreeNode implements Comparable<ImmutableTreeNode> {
private Map<String, ImmutableTreeNode> children = null; private Map<String, ImmutableTreeNode> children = null;
public ImmutableTreeNode(Map<String, ImmutableTreeNode> children) { public ImmutableTreeNode(Stream<Map.Entry<String, ImmutableTreeNode>> children) {
if (children != null) { if (children != null) {
LinkedHashMap<String, ImmutableTreeNode> sortedMap = children.entrySet().stream() LinkedHashMap<String, ImmutableTreeNode> sortedMap = children
.sorted((o1, o2) -> { .sorted((o1, o2) -> {
// sort first by if the node has any children
int childStatus = o1.getValue().compareTo(o2.getValue()); int childStatus = o1.getValue().compareTo(o2.getValue());
if (childStatus != 0) { if (childStatus != 0) {
return childStatus; return childStatus;
} }
// then alphabetically
return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()); return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey());
}) })
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
@ -65,6 +69,13 @@ public class ImmutableTreeNode implements Comparable<ImmutableTreeNode> {
return Optional.ofNullable(children); 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<Map.Entry<Integer, String>> getNodeEndings() { public List<Map.Entry<Integer, String>> getNodeEndings() {
if (children == null) { if (children == null) {
return Collections.emptyList(); return Collections.emptyList();
@ -80,6 +91,7 @@ public class ImmutableTreeNode implements Comparable<ImmutableTreeNode> {
results.addAll(node.getValue().getNodeEndings().stream() results.addAll(node.getValue().getNodeEndings().stream()
.map(e -> Maps.immutableEntry( .map(e -> Maps.immutableEntry(
e.getKey() + 1, // increment level e.getKey() + 1, // increment level
// add this node's key infront of the child value
node.getKey() + "." + e.getValue()) node.getKey() + "." + e.getValue())
) )
.collect(Collectors.toList())); .collect(Collectors.toList()));

View File

@ -30,9 +30,12 @@ import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -42,8 +45,14 @@ import java.util.concurrent.Executor;
public class PermissionVault implements Runnable { public class PermissionVault implements Runnable {
private static final Splitter DOT_SPLIT = Splitter.on('.').omitEmptyStrings(); private static final Splitter DOT_SPLIT = Splitter.on('.').omitEmptyStrings();
// the root node in the tree
@Getter @Getter
private final TreeNode rootNode; private final TreeNode rootNode;
// the known permissions already in the vault
private final Set<String> knownPermissions;
// a queue of permission strings to be processed by the tree
private final Queue<String> queue; private final Queue<String> queue;
@Setter @Setter
@ -51,6 +60,7 @@ public class PermissionVault implements Runnable {
public PermissionVault(Executor executor) { public PermissionVault(Executor executor) {
rootNode = new TreeNode(); rootNode = new TreeNode();
knownPermissions = ConcurrentHashMap.newKeySet(3000);
queue = new ConcurrentLinkedQueue<>(); queue = new ConcurrentLinkedQueue<>();
executor.execute(this); executor.execute(this);
@ -61,7 +71,10 @@ public class PermissionVault implements Runnable {
while (true) { while (true) {
for (String e; (e = queue.poll()) != null; ) { for (String e; (e = queue.poll()) != null; ) {
try { try {
insert(e.toLowerCase()); String s = e.toLowerCase();
if (knownPermissions.add(s)) {
insert(s);
}
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
@ -81,13 +94,19 @@ public class PermissionVault implements Runnable {
queue.offer(permission); queue.offer(permission);
} }
public Set<String> getKnownPermissions() {
return ImmutableSet.copyOf(knownPermissions);
}
public int getSize() { public int getSize() {
return rootNode.getDeepSize(); return rootNode.getDeepSize();
} }
private void insert(String permission) { private void insert(String permission) {
// split the permission up into parts
List<String> parts = DOT_SPLIT.splitToList(permission); List<String> parts = DOT_SPLIT.splitToList(permission);
// insert the permission into the node structure
TreeNode current = rootNode; TreeNode current = rootNode;
for (String part : parts) { for (String part : parts) {
current = current.getChildMap().computeIfAbsent(part, s -> new TreeNode()); current = current.getChildMap().computeIfAbsent(part, s -> new TreeNode());

View File

@ -25,13 +25,14 @@
package me.lucko.luckperms.common.treeview; package me.lucko.luckperms.common.treeview;
import com.google.common.collect.Maps;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; 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 { public class TreeNode {
private Map<String, TreeNode> children = null; private Map<String, TreeNode> children = null;
@ -60,7 +61,12 @@ public class TreeNode {
if (children == null) { if (children == null) {
return new ImmutableTreeNode(null); return new ImmutableTreeNode(null);
} else { } 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()
))
);
} }
} }
} }

View File

@ -42,62 +42,188 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* A readable view of a branch of {@link TreeNode}s.
*/
public class TreeView { public class TreeView {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); 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 String rootPosition;
private final int maxLevels;
// how many levels / branches to display
private final int maxLevel;
// the actual tree object
private final ImmutableTreeNode view; private final ImmutableTreeNode view;
public TreeView(PermissionVault source, String rootPosition, int maxLevels) { public TreeView(PermissionVault source, String rootPosition, int maxLevel) {
this.rootPosition = rootPosition; this.rootPosition = rootPosition;
this.maxLevels = maxLevels; this.maxLevel = maxLevel;
Optional<TreeNode> root = findRoot(source); Optional<TreeNode> root = findRoot(rootPosition, source);
this.view = root.map(TreeNode::makeImmutableCopy).orElse(null); 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() { public boolean hasData() {
return view != null; 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<TreeNode> 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<String> 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<String, TreeNode> 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
*
* <p>The list contains KV pairs, where the key is the tree padding/structure,
* and the value is the actual permission.</p>
*
* @return a list of the nodes in this view
*/
private List<Map.Entry<String, String>> 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<Map.Entry<String, String>> ret = new ArrayList<>();
// iterate the node endings in the view
for (Map.Entry<Integer, String> 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) { public String uploadPasteData(String version) {
// only paste if there is actually data here
if (!hasData()) { if (!hasData()) {
throw new IllegalStateException(); 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<Map.Entry<String, String>> ret = asTreeList(); List<Map.Entry<String, String>> ret = asTreeList();
ImmutableList.Builder<String> builder = getPasteHeader(version, "none", ret.size());
builder.add("```");
// build the header of the paste
ImmutableList.Builder<String> builder = getPasteHeader(version, "none", ret.size());
// add the tree data
builder.add("```");
for (Map.Entry<String, String> e : ret) { for (Map.Entry<String, String> e : ret) {
builder.add(e.getKey() + e.getValue()); builder.add(e.getKey() + e.getValue());
} }
builder.add("```"); builder.add("```");
// clear the initial data map
ret.clear(); 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"))))); 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.
*
* <p>Unlike {@link #uploadPasteData(String)}, this method will check each permission
* against a corresponding user, and colorize the output depending on the check results.</p>
*
* @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) { public String uploadPasteData(String version, String username, PermissionData checker) {
// only paste if there is actually data here
if (!hasData()) { if (!hasData()) {
throw new IllegalStateException(); 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<Map.Entry<String, String>> ret = asTreeList(); List<Map.Entry<String, String>> ret = asTreeList();
ImmutableList.Builder<String> builder = getPasteHeader(version, username, ret.size());
builder.add("```diff");
// build the header of the paste
ImmutableList.Builder<String> builder = getPasteHeader(version, username, ret.size());
// add the tree data
builder.add("```diff");
for (Map.Entry<String, String> e : ret) { for (Map.Entry<String, String> e : ret) {
// lookup a permission value for the node
Tristate tristate = checker.getPermissionValue(e.getValue()); Tristate tristate = checker.getPermissionValue(e.getValue());
// append the data to the paste
builder.add(getTristateDiffPrefix(tristate) + e.getKey() + e.getValue()); builder.add(getTristateDiffPrefix(tristate) + e.getKey() + e.getValue());
} }
builder.add("```"); builder.add("```");
// clear the initial data map
ret.clear(); 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"))))); 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("### Metadata")
.add("| Selection | Max Recursion | Reference User | Size | Produced at |") .add("| Selection | Max Recursion | Reference User | Size | Produced at |")
.add("|-----------|---------------|----------------|------|-------------|") .add("|-----------|---------------|----------------|------|-------------|")
.add("| " + selection + " | " + maxLevels + " | " + referenceUser + " | **" + size + "** | " + date + " |") .add("| " + selection + " | " + maxLevel + " | " + referenceUser + " | **" + size + "** | " + date + " |")
.add("") .add("")
.add("### Output"); .add("### Output");
} }
private Optional<TreeNode> findRoot(PermissionVault source) {
TreeNode root = source.getRootNode();
if (rootPosition.equals(".")) {
return Optional.of(root);
}
List<String> parts = Splitter.on('.').omitEmptyStrings().splitToList(rootPosition);
for (String part : parts) {
if (!root.getChildren().isPresent()) {
return Optional.empty();
}
Map<String, TreeNode> branch = root.getChildren().get();
root = branch.get(part);
if (root == null) {
return Optional.empty();
}
}
return Optional.of(root);
}
private List<Map.Entry<String, String>> asTreeList() {
String prefix = rootPosition.equals(".") ? "" : (rootPosition + ".");
List<Map.Entry<String, String>> ret = new ArrayList<>();
for (Map.Entry<Integer, String> 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;
}
} }

View File

@ -28,6 +28,9 @@ package me.lucko.luckperms.common.treeview;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
/**
* Builds a {@link TreeView}.
*/
@Accessors(fluent = true) @Accessors(fluent = true)
public class TreeViewBuilder { public class TreeViewBuilder {
public static TreeViewBuilder newBuilder() { public static TreeViewBuilder newBuilder() {

View File

@ -30,12 +30,26 @@ import lombok.Getter;
import me.lucko.luckperms.api.Tristate; import me.lucko.luckperms.api.Tristate;
/**
* Holds the data from a permission check
*/
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public class CheckData { public class CheckData {
private final String checked; /**
private final String node; * The name of the entity which was checked
private final Tristate value; */
private final String checkTarget;
/**
* The permission which was checked for
*/
private final String permission;
/**
* The result of the permission check
*/
private final Tristate result;
} }

View File

@ -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("!");
}
}

View File

@ -37,13 +37,22 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
/**
* Accepts {@link CheckData} and passes it onto registered {@link VerboseListener}s.
*/
public class VerboseHandler implements Runnable { public class VerboseHandler implements Runnable {
private final String pluginVersion; private final String pluginVersion;
// the listeners currently registered
private final Map<UUID, VerboseListener> listeners; private final Map<UUID, VerboseListener> listeners;
// a queue of check data
private final Queue<CheckData> queue; private final Queue<CheckData> queue;
// if there are any listeners currently registered
private boolean listening = false; private boolean listening = false;
// if the handler should shutdown
@Setter @Setter
private boolean shutdown = false; private boolean shutdown = false;
@ -55,31 +64,67 @@ public class VerboseHandler implements Runnable {
executor.execute(this); executor.execute(this);
} }
public void offer(String checked, String node, Tristate value) { /**
* Offers check data to the handler, to be eventually passed onto listeners.
*
* <p>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.</p>
*
* @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) { if (!listening) {
return; 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; listening = true;
listeners.put(sender.getUuid(), new VerboseListener(pluginVersion, sender, filter, notify)); 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(); flush();
VerboseListener ret = listeners.remove(uuid); VerboseListener ret = listeners.remove(uuid);
// stop listening if there are no listeners left
if (listeners.isEmpty()) { if (listeners.isEmpty()) {
listening = false; listening = false;
} }
return ret; return ret;
} }
@Override @Override
public void run() { public void run() {
while (true) { while (true) {
// remove listeners where the sender is no longer valid
listeners.values().removeIf(l -> !l.getNotifiedSender().isValid());
if (listeners.isEmpty()) {
listening = false;
}
flush(); flush();
if (shutdown) { if (shutdown) {
@ -92,6 +137,9 @@ public class VerboseHandler implements Runnable {
} }
} }
/**
* Flushes the current check data to the listeners.
*/
public synchronized void flush() { public synchronized void flush() {
for (CheckData e; (e = queue.poll()) != null; ) { for (CheckData e; (e = queue.poll()) != null; ) {
for (VerboseListener listener : listeners.values()) { for (VerboseListener listener : listeners.values()) {

View File

@ -25,6 +25,7 @@
package me.lucko.luckperms.common.verbose; package me.lucko.luckperms.common.verbose;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import com.google.common.collect.ImmutableList; 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.locale.Message;
import me.lucko.luckperms.common.utils.DateUtil; import me.lucko.luckperms.common.utils.DateUtil;
import me.lucko.luckperms.common.utils.PasteUtils; import me.lucko.luckperms.common.utils.PasteUtils;
import me.lucko.luckperms.common.utils.Scripting;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.script.ScriptEngine; /**
* Accepts and processes {@link CheckData}, passed from the {@link VerboseHandler}.
*/
@RequiredArgsConstructor @RequiredArgsConstructor
public class VerboseListener { 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 SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
private static final Function<Tristate, String> 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(); private final long startTime = System.currentTimeMillis();
// the version of the plugin. (used when we paste data to gist)
private final String pluginVersion; 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; private final String filter;
// if we should notify the sender
private final boolean notify; private final boolean notify;
// the number of checks we have processed
private final AtomicInteger counter = new AtomicInteger(0); private final AtomicInteger counter = new AtomicInteger(0);
private final AtomicInteger matchedCounter = new AtomicInteger(0);
private final List<CheckData> 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<CheckData> results = new ArrayList<>(DATA_TRUNCATION / 10);
/**
* Accepts and processes check data.
*
* @param data the data to process
*/
public void acceptData(CheckData data) { public void acceptData(CheckData data) {
counter.incrementAndGet(); counter.incrementAndGet();
if (!matches(data, filter)) { if (!VerboseFilter.passesFilter(data, filter)) {
return; return;
} }
matchedCounter.incrementAndGet(); matchedCounter.incrementAndGet();
@ -87,98 +98,23 @@ public class VerboseListener {
} }
if (notify) { 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("")) { * Uploads the captured data in this listener to a paste and returns the url
return true; *
} * @return the url
* @see PasteUtils#paste(String, List)
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("!");
}
public String uploadPasteData() { public String uploadPasteData() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
String startDate = DATE_FORMAT.format(new Date(startTime)); String startDate = DATE_FORMAT.format(new Date(startTime));
String endDate = DATE_FORMAT.format(new Date(now)); String endDate = DATE_FORMAT.format(new Date(now));
long secondsTaken = (now - startTime) / 1000L; long secondsTaken = (now - startTime) / 1000L;
String duration = DateUtil.formatTime(secondsTaken); String duration = DateUtil.formatTime(secondsTaken);
String filter = this.filter; String filter = this.filter;
if (filter == null || filter.equals("")){ if (filter == null || filter.equals("")){
filter = "any"; filter = "any";
@ -186,7 +122,7 @@ public class VerboseListener {
filter = "`" + filter + "`"; filter = "`" + filter + "`";
} }
ImmutableList.Builder<String> output = ImmutableList.<String>builder() ImmutableList.Builder<String> prettyOutput = ImmutableList.<String>builder()
.add("## Verbose Checking Output") .add("## Verbose Checking Output")
.add("#### This file was automatically generated by [LuckPerms](https://github.com/lucko/LuckPerms) " + pluginVersion) .add("#### This file was automatically generated by [LuckPerms](https://github.com/lucko/LuckPerms) " + pluginVersion)
.add("") .add("")
@ -197,33 +133,33 @@ public class VerboseListener {
.add("| End Time | " + endDate + " |") .add("| End Time | " + endDate + " |")
.add("| Duration | " + duration +" |") .add("| Duration | " + duration +" |")
.add("| Count | **" + matchedCounter.get() + "** / " + counter + " |") .add("| Count | **" + matchedCounter.get() + "** / " + counter + " |")
.add("| User | " + holder.getName() + " |") .add("| User | " + notifiedSender.getName() + " |")
.add("| Filter | " + filter + " |") .add("| Filter | " + filter + " |")
.add(""); .add("");
if (matchedCounter.get() > results.size()) { 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."); prettyOutput.add("**WARN:** Result set exceeded max size of " + DATA_TRUNCATION + ". The output below was truncated to " + DATA_TRUNCATION + " entries.");
output.add(""); prettyOutput.add("");
} }
output.add("### Output") prettyOutput.add("### Output")
.add("Format: `<checked>` `<permission>` `<value>`") .add("Format: `<checked>` `<permission>` `<value>`")
.add("") .add("")
.add("___") .add("___")
.add(""); .add("");
ImmutableList.Builder<String> data = ImmutableList.<String>builder() ImmutableList.Builder<String> csvOutput = ImmutableList.<String>builder()
.add("User,Permission,Result"); .add("User,Permission,Result");
results.stream() results.forEach(c -> {
.peek(c -> output.add("`" + c.getChecked() + "` - " + c.getNode() + " - **" + c.getValue().toString() + "** ")) prettyOutput.add("`" + c.getCheckTarget() + "` - " + c.getPermission() + " - **" + c.getResult().toString() + "** ");
.forEach(c -> data.add(escapeCommas(c.getChecked()) + "," + escapeCommas(c.getNode()) + "," + c.getValue().name().toLowerCase())); csvOutput.add(escapeCommas(c.getCheckTarget()) + "," + escapeCommas(c.getPermission()) + "," + c.getResult().name().toLowerCase());
});
results.clear(); results.clear();
List<Map.Entry<String, String>> content = ImmutableList.of( List<Map.Entry<String, String>> content = ImmutableList.of(
Maps.immutableEntry("luckperms-verbose.md", output.build().stream().collect(Collectors.joining("\n"))), Maps.immutableEntry("luckperms-verbose.md", prettyOutput.build().stream().collect(Collectors.joining("\n"))),
Maps.immutableEntry("raw-data.csv", data.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); return PasteUtils.paste("LuckPerms Verbose Checking Output", content);
@ -233,4 +169,15 @@ public class VerboseListener {
return s.contains(",") ? "\"" + s + "\"" : s; return s.contains(",") ? "\"" + s + "\"" : s;
} }
private static String getTristateColor(Tristate tristate) {
switch (tristate) {
case TRUE:
return "&2";
case FALSE:
return "&c";
default:
return "&7";
}
}
} }

View File

@ -45,6 +45,7 @@ import me.lucko.luckperms.common.config.LuckPermsConfiguration;
import me.lucko.luckperms.common.constants.CommandPermission; import me.lucko.luckperms.common.constants.CommandPermission;
import me.lucko.luckperms.common.contexts.ContextManager; import me.lucko.luckperms.common.contexts.ContextManager;
import me.lucko.luckperms.common.contexts.LuckPermsCalculator; 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.dependencies.DependencyManager;
import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.LocaleManager;
import me.lucko.luckperms.common.locale.NoopLocaleManager; 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.ExpireTemporaryTask;
import me.lucko.luckperms.common.tasks.UpdateTask; import me.lucko.luckperms.common.tasks.UpdateTask;
import me.lucko.luckperms.common.treeview.PermissionVault; 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.utils.UuidCache;
import me.lucko.luckperms.common.verbose.VerboseHandler; import me.lucko.luckperms.common.verbose.VerboseHandler;
import me.lucko.luckperms.sponge.calculators.SpongeCalculatorFactory; import me.lucko.luckperms.sponge.calculators.SpongeCalculatorFactory;
@ -497,7 +497,7 @@ public class LPSpongePlugin implements LuckPermsPlugin {
@Override @Override
public Sender getConsoleSender() { public Sender getConsoleSender() {
if (!game.isServerAvailable()) { 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()); return getSenderFactory().wrap(game.getServer().getConsole());
} }

View File

@ -45,7 +45,7 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode(of = {"id", "description", "owner"}) @EqualsAndHashCode(of = "id")
@ToString(of = {"id", "description", "owner"}) @ToString(of = {"id", "description", "owner"})
public final class LuckPermsPermissionDescription implements LPPermissionDescription { public final class LuckPermsPermissionDescription implements LPPermissionDescription {

View File

@ -73,6 +73,7 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -202,7 +203,19 @@ public class LuckPermsService implements LPPermissionService {
@Override @Override
public ImmutableSet<LPPermissionDescription> getDescriptions() { public ImmutableSet<LPPermissionDescription> getDescriptions() {
return ImmutableSet.copyOf(descriptionSet); Set<LPPermissionDescription> 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 @Override

View File

@ -247,7 +247,7 @@ public class PersistedSubject implements LPSubject {
public Tristate getPermissionValue(@NonNull ImmutableContextSet contexts, @NonNull String node) { public Tristate getPermissionValue(@NonNull ImmutableContextSet contexts, @NonNull String node) {
try (Timing ignored = service.getPlugin().getTimings().time(LPTiming.INTERNAL_SUBJECT_GET_PERMISSION_VALUE)) { try (Timing ignored = service.getPlugin().getTimings().time(LPTiming.INTERNAL_SUBJECT_GET_PERMISSION_VALUE)) {
Tristate t = permissionLookupCache.get(PermissionLookup.of(node, contexts)); 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; return t;
} }
} }