me.lucko.luckperms.core.Node.java Source code

Java tutorial

Introduction

Here is the source code for me.lucko.luckperms.core.Node.java

Source

/*
 * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 */

package me.lucko.luckperms.core;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import lombok.*;
import me.lucko.luckperms.api.Tristate;
import me.lucko.luckperms.constants.Patterns;
import me.lucko.luckperms.utils.ArgumentChecker;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * An immutable permission node
 */
@SuppressWarnings({ "WeakerAccess", "unused" })
@ToString(exclude = { "isPrefix", "isSuffix", "isMeta" })
@EqualsAndHashCode(exclude = { "isPrefix", "isSuffix", "isMeta" })
public class Node implements me.lucko.luckperms.api.Node {
    private static final Pattern PREFIX_PATTERN = Pattern.compile("(?i)prefix\\.-?\\d+\\..*");
    private static final Pattern SUFFIX_PATTERN = Pattern.compile("(?i)suffix\\.-?\\d+\\..*");
    private static final Pattern META_PATTERN = Pattern.compile("meta\\..*\\..*");

    @Getter
    private final String permission;

    @Getter
    private Boolean value;

    @Getter
    private boolean override;

    private String server = null;
    private String world = null;

    private long expireAt = 0L;

    private final Map<String, String> extraContexts;

    // Cache the state
    private Tristate isPrefix = Tristate.UNDEFINED;
    private Tristate isSuffix = Tristate.UNDEFINED;
    private Tristate isMeta = Tristate.UNDEFINED;

    /**
     * Make an immutable node instance
     * @param permission the actual permission node
     * @param value the value (if it's *not* negated)
     * @param expireAt the time when the node will expire
     * @param server the server this node applies on
     * @param world the world this node applies on
     * @param extraContexts any additional contexts applying to this node
     */
    public Node(String permission, boolean value, boolean override, long expireAt, String server, String world,
            Map<String, String> extraContexts) {
        if (permission == null || permission.equals("")) {
            throw new IllegalArgumentException("Empty permission");
        }

        if (server != null && (server.equalsIgnoreCase("global") || server.equals(""))) {
            server = null;
        }

        if (world != null && world.equals("")) {
            world = null;
        }

        if (world != null && server == null) {
            server = "global";
        }

        this.permission = permission;
        this.value = value;
        this.override = override;
        this.expireAt = expireAt;
        this.server = server;
        this.world = world;

        ImmutableMap.Builder<String, String> contexts = ImmutableMap.builder();
        if (extraContexts != null) {
            contexts.putAll(extraContexts);
        }
        this.extraContexts = contexts.build();
    }

    @Override
    public Tristate getTristate() {
        return Tristate.fromBoolean(value);
    }

    @Override
    public boolean isNegated() {
        return !value;
    }

    @Override
    public Optional<String> getServer() {
        return Optional.ofNullable(server);
    }

    @Override
    public Optional<String> getWorld() {
        return Optional.ofNullable(world);
    }

    @Override
    public boolean isServerSpecific() {
        return server != null && !server.equalsIgnoreCase("global");
    }

    @Override
    public boolean isWorldSpecific() {
        return world != null;
    }

    @Override
    public boolean isTemporary() {
        return expireAt != 0L;
    }

    @Override
    public boolean isPermanent() {
        return !isTemporary();
    }

    @Override
    public long getExpiryUnixTime() {
        if (!isTemporary()) {
            throw new IllegalStateException("Node does not have an expiry time.");
        }
        return expireAt;
    }

    @Override
    public Date getExpiry() {
        if (!isTemporary()) {
            throw new IllegalStateException("Node does not have an expiry time.");
        }
        return new Date(expireAt * 1000L);
    }

    @Override
    public long getSecondsTilExpiry() {
        if (!isTemporary()) {
            throw new IllegalStateException("Node does not have an expiry time.");
        }
        return expireAt - (System.currentTimeMillis() / 1000L);
    }

    @Override
    public boolean hasExpired() {
        return isTemporary() && expireAt < (System.currentTimeMillis() / 1000L);
    }

    @Override
    public Map<String, String> getExtraContexts() {
        return ImmutableMap.copyOf(extraContexts);
    }

    @Override
    public boolean isGroupNode() {
        return getPermission().toLowerCase().startsWith("group.");
    }

    @Override
    public String getGroupName() {
        if (!isGroupNode()) {
            throw new IllegalStateException("Node is not a group node");
        }

        return getPermission().substring("group.".length());
    }

    @Override
    public boolean isWildcard() {
        return getPermission().endsWith(".*");
    }

    @Override
    public int getWildcardLevel() {
        return (int) getPermission().chars().filter(num -> num == Character.getNumericValue('.')).count();
    }

    @Override
    public boolean isMeta() {
        if (isMeta == Tristate.UNDEFINED) {
            isMeta = Tristate.fromBoolean(META_PATTERN.matcher(getPermission()).matches());
        }

        return isMeta.asBoolean();
    }

    @Override
    public Map.Entry<String, String> getMeta() {
        if (!isMeta()) {
            throw new IllegalStateException();
        }

        List<String> metaPart = Splitter.on('.').limit(2).splitToList(getPermission().substring("meta.".length()));
        return new AbstractMap.SimpleEntry<>(metaPart.get(0), metaPart.get(1));
    }

    @Override
    public boolean isPrefix() {
        if (isPrefix == Tristate.UNDEFINED) {
            isPrefix = Tristate.fromBoolean(PREFIX_PATTERN.matcher(getPermission()).matches());
        }

        return isPrefix.asBoolean();
    }

    @Override
    public Map.Entry<Integer, String> getPrefix() {
        if (!isPrefix()) {
            throw new IllegalStateException();
        }

        List<String> prefixPart = Splitter.on('.').limit(2)
                .splitToList(getPermission().substring("prefix.".length()));
        Integer i = Integer.parseInt(prefixPart.get(0));
        return new AbstractMap.SimpleEntry<>(i, prefixPart.get(1));
    }

    @Override
    public boolean isSuffix() {
        if (isSuffix == Tristate.UNDEFINED) {
            isSuffix = Tristate.fromBoolean(SUFFIX_PATTERN.matcher(getPermission()).matches());
        }

        return isSuffix.asBoolean();
    }

    @Override
    public Map.Entry<Integer, String> getSuffix() {
        if (!isSuffix()) {
            throw new IllegalStateException();
        }

        List<String> suffixPart = Splitter.on('.').limit(2)
                .splitToList(getPermission().substring("suffix.".length()));
        Integer i = Integer.parseInt(suffixPart.get(0));
        return new AbstractMap.SimpleEntry<>(i, suffixPart.get(1));
    }

    @Override
    public boolean shouldApplyOnServer(String server, boolean includeGlobal, boolean applyRegex) {
        if (server == null || server.equals("")) {
            return !isServerSpecific() && includeGlobal;
        }

        if (isServerSpecific()) {
            return shouldApply(server, applyRegex, this.server);
        } else {
            return includeGlobal;
        }
    }

    @Override
    public boolean shouldApplyOnWorld(String world, boolean includeGlobal, boolean applyRegex) {
        if (world == null || world.equals("")) {
            return !isWorldSpecific() && includeGlobal;
        }

        if (isWorldSpecific()) {
            return shouldApply(world, applyRegex, this.world);
        } else {
            return includeGlobal;
        }
    }

    private static boolean shouldApply(String world, boolean applyRegex, String thisWorld) {
        if (world.toLowerCase().startsWith("r=") && applyRegex) {
            Pattern p = Patterns.compile(world.substring(2));
            if (p == null) {
                return false;
            }
            return p.matcher(thisWorld).matches();
        }

        if (world.startsWith("(") && world.endsWith(")") && world.contains("|")) {
            final String bits = world.substring(1, world.length() - 1);
            Iterable<String> parts = Splitter.on('|').split(bits);

            for (String s : parts) {
                if (s.equalsIgnoreCase(thisWorld)) {
                    return true;
                }
            }

            return false;
        }

        return thisWorld.equalsIgnoreCase(world);
    }

    @Override
    public boolean shouldApplyWithContext(Map<String, String> context, boolean worldAndServer) {
        if (extraContexts.isEmpty() && !isServerSpecific() && !isWorldSpecific()) {
            return true;
        }

        if (worldAndServer) {
            if (isWorldSpecific()) {
                if (context == null)
                    return false;
                if (!context.containsKey("world"))
                    return false;
                if (!context.get("world").equalsIgnoreCase(world))
                    return false;
            }

            if (isServerSpecific()) {
                if (context == null)
                    return false;
                if (!context.containsKey("server"))
                    return false;
                if (!context.get("server").equalsIgnoreCase(server))
                    return false;
            }
        }

        if (!extraContexts.isEmpty()) {
            if (context == null)
                return false;

            for (Map.Entry<String, String> c : extraContexts.entrySet()) {
                if (!context.containsKey(c.getKey()))
                    return false;
                if (!context.get(c.getKey()).equalsIgnoreCase(c.getValue()))
                    return false;
            }
        }

        return true;
    }

    @Override
    public boolean shouldApplyWithContext(Map<String, String> context) {
        return shouldApplyWithContext(context, true);
    }

    @Override
    public boolean shouldApplyOnAnyServers(List<String> servers, boolean includeGlobal) {
        for (String s : servers) {
            if (shouldApplyOnServer(s, includeGlobal, false)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public boolean shouldApplyOnAnyWorlds(List<String> worlds, boolean includeGlobal) {
        for (String s : worlds) {
            if (shouldApplyOnWorld(s, includeGlobal, false)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public List<String> resolveWildcard(List<String> possibleNodes) {
        if (!isWildcard() || possibleNodes == null) {
            return Collections.emptyList();
        }

        String match = getPermission().substring(0, getPermission().length() - 2);
        return possibleNodes.stream().filter(pn -> pn.startsWith(match)).collect(Collectors.toList());
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private static boolean isInt(String a, String b) {
        try {
            Integer.parseInt(a);
            Integer.parseInt(b);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    private static boolean isChar(String a, String b) {
        return a.length() == 1 && b.length() == 1;
    }

    private static Set<String> getCharRange(char a, char b) {
        Set<String> s = new HashSet<>();
        for (char c = a; c <= b; c++) {
            s.add(Character.toString(c));
        }
        return s;
    }

    @Override
    public List<String> resolveShorthand() {
        if (!Patterns.SHORTHAND_NODE.matcher(getPermission()).find()) {
            return Collections.emptyList();
        }

        if (!getPermission().contains(".")) {
            return Collections.emptyList();
        }

        Iterable<String> parts = Splitter.on('.').split(getPermission());
        List<Set<String>> nodeParts = new ArrayList<>();

        for (String s : parts) {
            if ((!s.startsWith("(") || !s.endsWith(")")) || (!s.contains("|") && !s.contains("-"))) {
                nodeParts.add(Collections.singleton(s));
                continue;
            }

            final String bits = s.substring(1, s.length() - 1);
            if (s.contains("|")) {
                nodeParts.add(new HashSet<>(Splitter.on('|').splitToList(bits)));
            } else {
                List<String> range = Splitter.on('-').limit(2).splitToList(bits);
                if (isChar(range.get(0), range.get(1))) {
                    nodeParts.add(getCharRange(range.get(0).charAt(0), range.get(1).charAt(0)));
                } else if (isInt(range.get(0), range.get(1))) {
                    nodeParts.add(
                            IntStream.rangeClosed(Integer.parseInt(range.get(0)), Integer.parseInt(range.get(1)))
                                    .boxed().map(i -> "" + i).collect(Collectors.toSet()));
                } else {
                    // Fallback
                    nodeParts.add(Collections.singleton(s));
                }
            }
        }

        Set<String> nodes = new HashSet<>();
        for (Set<String> set : nodeParts) {
            final Set<String> newNodes = new HashSet<>();
            if (nodes.isEmpty()) {
                newNodes.addAll(set);
            } else {
                nodes.forEach(str -> newNodes
                        .addAll(set.stream().map(add -> str + "." + add).collect(Collectors.toList())));
            }
            nodes = newNodes;
        }

        return new ArrayList<>(nodes);
    }

    public String toSerializedNode() {
        StringBuilder builder = new StringBuilder();

        if (server != null) {
            builder.append(server);

            if (world != null) {
                builder.append("-").append(world);
            }
            builder.append("/");
        } else {
            if (world != null) {
                builder.append("global-").append(world).append("/");
            }
        }

        if (!extraContexts.isEmpty()) {
            builder.append("(");
            for (Map.Entry<String, String> entry : extraContexts.entrySet()) {
                builder.append(entry.getKey()).append("=").append(entry.getValue()).append(",");
            }

            builder.deleteCharAt(builder.length() - 1);
            builder.append(")");
        }

        builder.append(permission);

        if (expireAt != 0L) {
            builder.append("$").append(expireAt);
        }

        return builder.toString();
    }

    @Override
    public boolean equalsIgnoringValue(me.lucko.luckperms.api.Node other) {
        if (!other.getPermission().equalsIgnoreCase(this.getPermission())) {
            return false;
        }

        if (other.isTemporary() != this.isTemporary()) {
            return false;
        }

        if (this.isTemporary()) {
            if (other.getExpiryUnixTime() != this.getExpiryUnixTime()) {
                return false;
            }
        }

        if (other.getServer().isPresent() == this.getServer().isPresent()) {
            if (other.getServer().isPresent()) {
                if (!other.getServer().get().equalsIgnoreCase(this.getServer().get())) {
                    return false;
                }
            }
        } else {
            return false;
        }

        if (other.getWorld().isPresent() == this.getWorld().isPresent()) {
            if (other.getWorld().isPresent()) {
                if (!other.getWorld().get().equalsIgnoreCase(this.getWorld().get())) {
                    return false;
                }
            }
        } else {
            return false;
        }

        if (!other.getExtraContexts().equals(this.getExtraContexts())) {
            return false;
        }

        return true;
    }

    @Override
    public boolean almostEquals(me.lucko.luckperms.api.Node other) {
        if (!other.getPermission().equalsIgnoreCase(this.getPermission())) {
            return false;
        }

        if (other.isTemporary() != this.isTemporary()) {
            return false;
        }

        if (other.getServer().isPresent() == this.getServer().isPresent()) {
            if (other.getServer().isPresent()) {
                if (!other.getServer().get().equalsIgnoreCase(this.getServer().get())) {
                    return false;
                }
            }
        } else {
            return false;
        }

        if (other.getWorld().isPresent() == this.getWorld().isPresent()) {
            if (other.getWorld().isPresent()) {
                if (!other.getWorld().get().equalsIgnoreCase(this.getWorld().get())) {
                    return false;
                }
            }
        } else {
            return false;
        }

        if (!other.getExtraContexts().equals(this.getExtraContexts())) {
            return false;
        }

        return true;
    }

    @Override
    public boolean equalsIgnoringValueOrTemp(me.lucko.luckperms.api.Node other) {
        if (!other.getPermission().equalsIgnoreCase(this.getPermission())) {
            return false;
        }

        if (other.getServer().isPresent() == this.getServer().isPresent()) {
            if (other.getServer().isPresent()) {
                if (!other.getServer().get().equalsIgnoreCase(this.getServer().get())) {
                    return false;
                }
            }
        } else {
            return false;
        }

        if (other.getWorld().isPresent() == this.getWorld().isPresent()) {
            if (other.getWorld().isPresent()) {
                if (!other.getWorld().get().equalsIgnoreCase(this.getWorld().get())) {
                    return false;
                }
            }
        } else {
            return false;
        }

        if (!other.getExtraContexts().equals(this.getExtraContexts())) {
            return false;
        }

        return true;
    }

    @Override
    public Boolean setValue(Boolean value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getKey() {
        return getPermission();
    }

    private static final Map<String, me.lucko.luckperms.api.Node> CACHE = new ConcurrentHashMap<>();
    private static final Map<String, me.lucko.luckperms.api.Node> CACHE_NEGATED = new ConcurrentHashMap<>();

    public static me.lucko.luckperms.api.Node fromSerialisedNode(String s, Boolean b) {
        if (b) {
            return CACHE.computeIfAbsent(s, s1 -> builderFromSerialisedNode(s1, true).build());
        } else {
            return CACHE_NEGATED.computeIfAbsent(s, s1 -> builderFromSerialisedNode(s1, false).build());
        }
    }

    public static me.lucko.luckperms.api.Node.Builder builderFromSerialisedNode(String s, Boolean b) {
        if (s.contains("/")) {
            List<String> parts = Splitter.on('/').limit(2).splitToList(s);
            // 0=server(+world)   1=node

            // WORLD SPECIFIC
            if (parts.get(0).contains("-")) {
                List<String> serverParts = Splitter.on('-').limit(2).splitToList(parts.get(0));
                // 0=server   1=world

                if (parts.get(1).contains("$")) {
                    List<String> tempParts = Splitter.on('$').limit(2).splitToList(parts.get(1));
                    return new Node.Builder(tempParts.get(0), true).setServerRaw(serverParts.get(0))
                            .setWorld(serverParts.get(1)).setExpiry(Long.parseLong(tempParts.get(1))).setValue(b);
                } else {
                    return new Node.Builder(parts.get(1), true).setServerRaw(serverParts.get(0))
                            .setWorld(serverParts.get(1)).setValue(b);
                }

            } else {
                // SERVER BUT NOT WORLD SPECIFIC
                if (parts.get(1).contains("$")) {
                    List<String> tempParts = Splitter.on('$').limit(2).splitToList(parts.get(1));
                    return new Node.Builder(tempParts.get(0), true).setServerRaw(parts.get(0))
                            .setExpiry(Long.parseLong(tempParts.get(1))).setValue(b);
                } else {
                    return new Node.Builder(parts.get(1), true).setServerRaw(parts.get(0)).setValue(b);
                }
            }
        } else {
            // NOT SERVER SPECIFIC
            if (s.contains("$")) {
                List<String> tempParts = Splitter.on('$').limit(2).splitToList(s);
                return new Node.Builder(tempParts.get(0), true).setExpiry(Long.parseLong(tempParts.get(1)))
                        .setValue(b);
            } else {
                return new Node.Builder(s, true).setValue(b);
            }
        }
    }

    public static me.lucko.luckperms.api.Node.Builder builderFromExisting(me.lucko.luckperms.api.Node other) {
        return new Builder(other);
    }

    @RequiredArgsConstructor
    public static class Builder implements me.lucko.luckperms.api.Node.Builder {
        private final String permission;
        private Boolean value = true;
        private boolean override = false;
        private String server = null;
        private String world = null;
        private long expireAt = 0L;

        private final Map<String, String> extraContexts = new HashMap<>();

        Builder(String permission, boolean shouldConvertContexts) {
            if (!shouldConvertContexts) {
                this.permission = permission;
            } else {
                if (!Patterns.NODE_CONTEXTS.matcher(permission).matches()) {
                    this.permission = permission;
                } else {
                    List<String> contextParts = Splitter.on(')').limit(2).splitToList(permission.substring(1));
                    // 0 = context, 1 = node

                    this.permission = contextParts.get(1);
                    try {
                        extraContexts
                                .putAll(Splitter.on(',').withKeyValueSeparator('=').split(contextParts.get(0)));
                    } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        Builder(me.lucko.luckperms.api.Node other) {
            this.permission = other.getPermission();
            this.value = other.getValue();
            this.override = other.isOverride();
            this.server = other.getServer().orElse(null);
            this.world = other.getWorld().orElse(null);
            this.expireAt = other.isPermanent() ? 0L : other.getExpiryUnixTime();
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder setNegated(boolean negated) {
            value = !negated;
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder setValue(boolean value) {
            this.value = value;
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder setOverride(boolean override) {
            this.override = override;
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder setExpiry(long expireAt) {
            this.expireAt = expireAt;
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder setWorld(String world) {
            this.world = world;
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder setServer(String server) {
            if (server != null && ArgumentChecker.checkServer(server)) {
                throw new IllegalArgumentException("Server name invalid.");
            }

            this.server = server;
            return this;
        }

        public me.lucko.luckperms.api.Node.Builder setServerRaw(String server) {
            this.server = server;
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder withExtraContext(@NonNull String key, @NonNull String value) {
            switch (key) {
            case "server":
                setServer(value);
                break;
            case "world":
                setWorld(value);
                break;
            default:
                this.extraContexts.put(key, value);
                break;
            }

            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder withExtraContext(Map<String, String> map) {
            map.entrySet().forEach(this::withExtraContext);
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node.Builder withExtraContext(Map.Entry<String, String> entry) {
            withExtraContext(entry.getKey(), entry.getValue());
            return this;
        }

        @Override
        public me.lucko.luckperms.api.Node build() {
            return new Node(permission, value, override, expireAt, server, world, extraContexts);
        }
    }

}