com.sk89q.worldguard.protection.FlagValueCalculator.java Source code

Java tutorial

Introduction

Here is the source code for com.sk89q.worldguard.protection.FlagValueCalculator.java

Source

/*
 * WorldGuard, a suite of tools for Minecraft
 * Copyright (C) sk89q <http://www.sk89q.com>
 * Copyright (C) WorldGuard team and contributors
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package com.sk89q.worldguard.protection;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.sk89q.worldguard.domains.Association;
import com.sk89q.worldguard.protection.association.RegionAssociable;
import com.sk89q.worldguard.protection.flags.Flags;
import com.sk89q.worldguard.protection.flags.Flag;
import com.sk89q.worldguard.protection.flags.RegionGroup;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.StateFlag.State;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.util.NormativeOrders;

import javax.annotation.Nullable;
import java.util.*;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Calculates the value of a flag given a list of regions and an optional
 * global region.
 *
 * <p>Since there may be multiple overlapping regions, regions with
 * differing priorities, regions with inheritance, flags with region groups
 * assigned to them, and much more, the task of calculating the "effective"
 * value of a flag is far from trivial. This class abstracts away the
 * difficult with a number of methods for performing these calculations.</p>
 */
public class FlagValueCalculator {

    private final List<ProtectedRegion> regions;
    @Nullable
    private final ProtectedRegion globalRegion;
    private final Iterable<ProtectedRegion> applicable;

    /**
     * Create a new instance.
     *
     * @param regions a list of applicable regions that must be sorted according to {@link NormativeOrders}
     * @param globalRegion an optional global region (null to not use one)
     */
    public FlagValueCalculator(List<ProtectedRegion> regions, @Nullable ProtectedRegion globalRegion) {
        checkNotNull(regions);

        this.regions = regions;
        this.globalRegion = globalRegion;

        if (globalRegion != null) {
            applicable = Iterables.concat(regions, Arrays.asList(globalRegion));
        } else {
            applicable = regions;
        }
    }

    /**
     * Returns an iterable of regions sorted by priority (descending), with
     * the global region tacked on at the end if one exists.
     *
     * @return an iterable
     */
    private Iterable<ProtectedRegion> getApplicable() {
        return applicable;
    }

    /**
     * Return the membership status of the given subject, indicating
     * whether there are no (counted) regions in the list of regions,
     * whether the subject is a member of all regions, or whether
     * the region is not a member of all regions.
     *
     * <p>A region is "counted" if it doesn't have the
     * {@link Flags#PASSTHROUGH} flag set to {@code ALLOW}. (The
     * explicit purpose of the PASSTHROUGH flag is to have the region
     * be skipped over in this check.)</p>
     *
     * <p>This method is mostly for internal use. It's not particularly
     * useful.</p>
     *
     * @param subject the subject
     * @return the membership result
     */
    public Result getMembership(RegionAssociable subject) {
        checkNotNull(subject);

        int minimumPriority = Integer.MIN_VALUE;
        Result result = Result.NO_REGIONS;

        Set<ProtectedRegion> ignoredRegions = Sets.newHashSet();

        for (ProtectedRegion region : getApplicable()) {
            // Don't consider lower priorities below minimumPriority
            // (which starts at Integer.MIN_VALUE). A region that "counts"
            // (has the flag set OR has members) will raise minimumPriority
            // to its own priority.
            if (getPriority(region) < minimumPriority) {
                break;
            }

            // If PASSTHROUGH is set, ignore this region
            if (getEffectiveFlag(region, Flags.PASSTHROUGH, subject) == State.ALLOW) {
                continue;
            }

            if (ignoredRegions.contains(region)) {
                continue;
            }

            minimumPriority = getPriority(region);

            boolean member = RegionGroup.MEMBERS.contains(subject.getAssociation(Arrays.asList(region)));

            if (member) {
                result = Result.SUCCESS;
                addParents(ignoredRegions, region);
            } else {
                return Result.FAIL;
            }
        }

        return result;
    }

    /**
     * Get the effective value for a list of state flags. The rules of
     * states is observed here; that is, {@code DENY} overrides {@code ALLOW},
     * and {@code ALLOW} overrides {@code NONE}.
     *
     * <p>A subject can be provided that is used to determine whether the value
     * of a flag on a particular region should be used. For example, if a
     * flag's region group is set to {@link RegionGroup#MEMBERS} and the given
     * subject is not a member, then the region would be skipped when
     * querying that flag. If {@code null} is provided for the subject, then
     * only flags that use {@link RegionGroup#ALL},
     * {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
     *
     * @param subject an optional subject, which would be used to determine the region group to apply
     * @param flags a list of flags to check
     * @return a state
     */
    @Nullable
    public State queryState(@Nullable RegionAssociable subject, StateFlag... flags) {
        State value = null;

        for (StateFlag flag : flags) {
            value = StateFlag.combine(value, queryValue(subject, flag));
            if (value == State.DENY) {
                break;
            }
        }

        return value;
    }

    /**
     * Get the effective value for a list of state flags. The rules of
     * states is observed here; that is, {@code DENY} overrides {@code ALLOW},
     * and {@code ALLOW} overrides {@code NONE}.
     *
     * <p>This method is the same as
     * {@link #queryState(RegionAssociable, StateFlag...)}.</p>
     *
     * @param subject an optional subject, which would be used to determine the region group to apply
     * @param flag a flag to check
     * @return a state
     */
    @Nullable
    public State queryState(@Nullable RegionAssociable subject, StateFlag flag) {
        return queryValue(subject, flag);
    }

    /**
     * Get the effective value for a flag. If there are multiple values
     * (for example, if there are multiple regions with the same priority
     * but with different farewell messages set, there would be multiple
     * completing values), then the selected (or "winning") value will depend
     * on the flag type.
     *
     * <p>Only some flag types actually have a strategy for picking the
     * "best value." For most types, the actual value that is chosen to be
     * returned is undefined (it could be any value). As of writing, the only
     * type of flag that can consistently return the same 'best' value is
     * {@link StateFlag}.</p>
     *
     * <p>A subject can be provided that is used to determine whether the value
     * of a flag on a particular region should be used. For example, if a
     * flag's region group is set to {@link RegionGroup#MEMBERS} and the given
     * subject is not a member, then the region would be skipped when
     * querying that flag. If {@code null} is provided for the subject, then
     * only flags that use {@link RegionGroup#ALL},
     * {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
     *
     * @param subject an optional subject, which would be used to determine the region group to apply
     * @param flag the flag
     * @return a value, which could be {@code null}
     */
    @Nullable
    public <V> V queryValue(@Nullable RegionAssociable subject, Flag<V> flag) {
        Collection<V> values = queryAllValues(subject, flag, true);
        return flag.chooseValue(values);
    }

    /**
     * Get the effective values for a flag, returning a collection of all
     * values. It is up to the caller to determine which value, if any,
     * from the collection will be used.
     *
     * <p>A subject can be provided that is used to determine whether the value
     * of a flag on a particular region should be used. For example, if a
     * flag's region group is set to {@link RegionGroup#MEMBERS} and the given
     * subject is not a member, then the region would be skipped when
     * querying that flag. If {@code null} is provided for the subject, then
     * only flags that use {@link RegionGroup#ALL},
     * {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
     *
     * @param subject an optional subject, which would be used to determine the region group to apply
     * @param flag the flag
     * @return a collection of values
     */
    public <V> Collection<V> queryAllValues(@Nullable RegionAssociable subject, Flag<V> flag) {
        return queryAllValues(subject, flag, false);
    }

    /**
     * Get the effective values for a flag, returning a collection of all
     * values. It is up to the caller to determine which value, if any,
     * from the collection will be used.
     *
     * <p>A subject can be provided that is used to determine whether the value
     * of a flag on a particular region should be used. For example, if a
     * flag's region group is set to {@link RegionGroup#MEMBERS} and the given
     * subject is not a member, then the region would be skipped when
     * querying that flag. If {@code null} is provided for the subject, then
     * only flags that use {@link RegionGroup#ALL},
     * {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
     *
     * @param subject an optional subject, which would be used to determine the region group to apply
     * @param flag the flag
     * @param acceptOne if possible, return only one value if it doesn't matter
     * @return a collection of values
     */
    @SuppressWarnings("unchecked")
    private <V> Collection<V> queryAllValues(@Nullable RegionAssociable subject, Flag<V> flag, boolean acceptOne) {
        checkNotNull(flag);

        // Can't use this optimization with flags that have a conflict resolution strategy
        if (acceptOne && flag.hasConflictStrategy()) {
            acceptOne = false;
        }

        // Check to see whether we have a subject if this is BUILD
        if (flag.requiresSubject() && subject == null) {
            throw new NullPointerException("The " + flag.getName()
                    + " flag is handled in a special fashion and requires a non-null subject parameter");
        }

        int minimumPriority = Integer.MIN_VALUE;

        Map<ProtectedRegion, V> consideredValues = new HashMap<>();
        Set<ProtectedRegion> ignoredParents = new HashSet<>();

        for (ProtectedRegion region : getApplicable()) {
            if (getPriority(region) < minimumPriority) {
                break;
            }

            if (ignoredParents.contains(region)) {
                continue;
            }

            V value = getEffectiveFlag(region, flag, subject);
            int priority = getPriority(region);

            if (value != null) {
                minimumPriority = priority;

                if (acceptOne) {
                    return Arrays.asList(value);
                } else {
                    consideredValues.put(region, value);
                }
            }

            addParents(ignoredParents, region);

            // The BUILD flag is implicitly set on every region where
            // PASSTHROUGH is not set to ALLOW
            if (priority != minimumPriority && flag.implicitlySetWithMembership()
                    && getEffectiveFlag(region, Flags.PASSTHROUGH, subject) != State.ALLOW) {
                minimumPriority = getPriority(region);
            }
        }

        if (flag.usesMembershipAsDefault() && consideredValues.isEmpty()) {
            switch (getMembership(subject)) {
            case FAIL:
                return ImmutableList.of();
            case SUCCESS:
                return (Collection<V>) ImmutableList.of(State.ALLOW);
            }
        }

        if (consideredValues.isEmpty()) {
            V fallback = flag.getDefault();
            return fallback != null ? ImmutableList.of(fallback) : (Collection<V>) ImmutableList.of();
        }

        return consideredValues.values();
    }

    /**
     * Get the effective priority of a region, overriding a region's priority
     * when appropriate (i.e. with the global region).
     *
     * @param region the region
     * @return the priority
     */
    public int getPriority(final ProtectedRegion region) {
        if (region == globalRegion) {
            return Integer.MIN_VALUE;
        } else {
            return region.getPriority();
        }
    }

    /**
     * Get a region's state flag, checking parent regions until a value for the
     * flag can be found (if one even exists).
     *
     * @param region the region
     * @param flag the flag
     * @param subject an subject object
     * @return the value
     */
    @SuppressWarnings("unchecked")
    public <V> V getEffectiveFlag(final ProtectedRegion region, Flag<V> flag, @Nullable RegionAssociable subject) {
        if (region == globalRegion) {
            if (flag == Flags.PASSTHROUGH) {
                // Has members/owners -> the global region acts like
                // a regular region without PASSTHROUGH
                if (region.hasMembersOrOwners() || region.getFlag(Flags.PASSTHROUGH) == State.DENY) {
                    return null;
                } else {
                    return (V) State.ALLOW;
                }

            } else if (flag instanceof StateFlag && ((StateFlag) flag).preventsAllowOnGlobal()) {
                // Legacy behavior -> we can't let people change BUILD on
                // the global region
                State value = region.getFlag((StateFlag) flag);
                return value != State.ALLOW ? (V) value : null;
            }
        }

        ProtectedRegion current = region;

        List<ProtectedRegion> seen = new ArrayList<>();

        while (current != null) {
            seen.add(current);

            V value = current.getFlag(flag);

            if (value != null) {
                boolean use = true;

                if (flag.getRegionGroupFlag() != null) {
                    RegionGroup group = current.getFlag(flag.getRegionGroupFlag());
                    if (group == null) {
                        group = flag.getRegionGroupFlag().getDefault();
                    }

                    if (group == null) {
                        use = false;
                    } else if (subject == null) {
                        use = group.contains(Association.NON_MEMBER);
                    } else if (!group.contains(subject.getAssociation(seen))) {
                        use = false;
                    }
                }

                if (use) {
                    return value;
                }
            }

            current = current.getParent();
        }

        return null;
    }

    /**
     * Clear a region's parents for getFlag().
     *
     * @param ignored The regions to ignore
     * @param region The region to start from
     */
    private void addParents(Set<ProtectedRegion> ignored, ProtectedRegion region) {
        ProtectedRegion parent = region.getParent();

        while (parent != null) {
            ignored.add(parent);
            parent = parent.getParent();
        }
    }

    /**
     * Describes the membership result from
     * {@link #getMembership(RegionAssociable)}.
     */
    public static enum Result {
        /**
         * Indicates that there are no regions or the only regions are
         * ones with {@link Flags#PASSTHROUGH} enabled.
         */
        NO_REGIONS,

        /**
         * Indicates that the player is not a member of all overlapping
         * regions.
         */
        FAIL,

        /**
         * Indicates that the player is a member of all overlapping
         * regions.
         */
        SUCCESS
    }

}