src/main/java/nl/rutgerkok/blocklocker/impl/profile/ProfileFactoryImpl.java
public final class ProfileFactoryImpl implements ProfileFactory {

    private static final Profile EMPTY_PROFILE = new PlayerProfileImpl("", Optional.empty());

    private final Profile everyoneProfile;
    private final List<String> everyoneTagList;
    private final GroupSystem groupSystem;
    private final Profile redstoneProfile;
    private final List<String> redstoneTagList;
    private final List<String> timerTagStart;
    private final Translator translator;

    public ProfileFactoryImpl(GroupSystem groupSystem, Translator translator) {
        this.groupSystem = Preconditions.checkNotNull(groupSystem);
        this.translator = Preconditions.checkNotNull(translator);

        this.everyoneTagList = new ArrayList<>();
        this.redstoneTagList = new ArrayList<>();
        this.timerTagStart = new ArrayList<>();

        translator.getAllWithoutColor(Translation.TAG_EVERYONE).forEach(value -> this.everyoneTagList.add("[" + value + "]"));
        translator.getAllWithoutColor(Translation.TAG_REDSTONE).forEach(value -> this.redstoneTagList.add("[" + value + "]"));
        translator.getAllWithoutColor(Translation.TAG_TIMER).forEach(value -> this.timerTagStart.add("[" + value + ":"));

        this.everyoneProfile = new EveryoneProfileImpl(translator.get(Translation.TAG_EVERYONE));
        this.redstoneProfile = new RedstoneProfileImpl(translator.get(Translation.TAG_REDSTONE));
    }

    /**
     * Gets an empty profile, which is equal to what
     * {@link #fromDisplayText(String)} returns for an empty string.
     *
     * @return The empty profile.
     */
    public Profile emptyProfile() {
        return EMPTY_PROFILE;
    }

    /**
     * Parses a profile from the text displayed on a sign. Used for newly created
     * signs and for signs created by Lockette/Deadbolt.
     *
     * @param text
     *            The text on a single line.
     * @return The profile.
     */
    public Profile fromDisplayText(String text) {
        final String stripped = ChatColor.stripColor(text.trim());

        if (stripped.length() > 2) {
            // [Everyone]
            if (everyoneTagList.stream().anyMatch(s -> s.equalsIgnoreCase(stripped))) {
                return new EveryoneProfileImpl(stripped.substring(1, stripped.length() - 1));
            }

            // [Redstone]
            if (redstoneTagList.stream().anyMatch(s -> s.equalsIgnoreCase(stripped))) {
                return new RedstoneProfileImpl(stripped.substring(1, stripped.length() - 1));
            }

            // [Timer:X]
            if (timerTagStart.stream().anyMatch(s -> StringUtil.startsWithIgnoreCase(stripped, s) && stripped.endsWith("]"))) {
                return readTimerProfile(stripped);
            }

            // [GroupName]
            if (stripped.startsWith("[") && stripped.endsWith("]")) {
                return new GroupProfileImpl(groupSystem, stripped.substring(1, stripped.length() - 1));
            }

            // +GroupName+
            if (stripped.startsWith("+") && stripped.endsWith("+")) {
                return new GroupLeaderProfileImpl(groupSystem, stripped.substring(1, stripped.length() - 1));
            }

            // DisplayName#UUID (format of LockettePro)
            int hashCharIndex = stripped.indexOf('#');
            if (hashCharIndex > 0) {
                String name = stripped.substring(0, hashCharIndex);
                try {
                    UUID uuid = UUID.fromString(stripped.substring(hashCharIndex + 1));
                    return new PlayerProfileImpl(name, Optional.of(uuid));
                } catch (IllegalArgumentException e) {
                    // Ignore, not in name#uuid format. Someone probably added
                    // a # for some other reason
                }
            }
        }

        return new PlayerProfileImpl(stripped, Optional.empty());
    }

    @Override
    public Profile fromEveryone() {
        return this.everyoneProfile;
    }

    @Override
    public PlayerProfile fromNameAndUniqueId(String name, Optional<UUID> uuid) {
        Preconditions.checkNotNull(name, "name");
        Preconditions.checkNotNull(uuid, "uuid");
        return new PlayerProfileImpl(name, uuid);
    }

    @Override
    public PlayerProfile fromPlayer(Player player) {
        Preconditions.checkNotNull(player);
        Optional<UUID> uuid = Optional.of(player.getUniqueId());
        return new PlayerProfileImpl(player.getName(), uuid);
    }

    @Override
    public Profile fromRedstone() {
        return this.redstoneProfile;
    }



    /**
     * Converts the given profile from a saved JSON object.
     *
     * @param object
     *            The object to convert from.
     * @return The profile, if any.
     */
    public Optional<Profile> fromSavedObject(SecretSignEntry object) {
        // Player
        Optional<String> name = object.getString(PlayerProfileImpl.NAME_KEY);
        if (name.isPresent()) {
            Optional<UUID> uuid = object.getUniqueId(PlayerProfileImpl.UUID_KEY);
            Profile profile = new PlayerProfileImpl(name.get(), uuid);
            return Optional.of(profile);
        }

        // [Everyone]
        Optional<Boolean> isEveryone = object.getBoolean(EveryoneProfileImpl.EVERYONE_KEY);
        if (isEveryone.isPresent()) {
            return Optional.of(this.everyoneProfile);
        }

        // [Redstone]
        Optional<Boolean> isRedstone = object.getBoolean(RedstoneProfileImpl.REDSTONE_KEY);
        if (isRedstone.isPresent()) {
            return Optional.of(this.redstoneProfile);
        }

        // Timer
        OptionalInt secondsOpen = object.getInteger(TimerProfileImpl.TIME_KEY);
        if (secondsOpen.isPresent()) {
            Profile profile = new TimerProfileImpl(translator.getWithoutColor(Translation.TAG_TIMER),
                    secondsOpen.getAsInt());
            return Optional.of(profile);
        }

        // Groups
        Optional<String> groupName = object.getString(GroupProfileImpl.GROUP_KEY);
        if (groupName.isPresent()) {
            Profile profile = new GroupProfileImpl(groupSystem, groupName.get());
            return Optional.of(profile);
        }

        // Group leaders
        groupName = object.getString(GroupLeaderProfileImpl.GROUP_LEADER_KEY);
        if (groupName.isPresent()) {
            Profile profile = new GroupLeaderProfileImpl(groupSystem, groupName.get());
            return Optional.of(profile);
        }

        return Optional.empty();
    }

    private int readDigit(char digit) {
        try {
            return Integer.parseInt(String.valueOf(digit));
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    private Profile readTimerProfile(String text) {
        // First decide which one to use
        String tagStart = timerTagStart.stream()
                .filter(tag -> StringUtil.startsWithIgnoreCase(text, tag) && text.endsWith("]")).findFirst()
                .orElse(null);

        char digit = text.charAt(tagStart.length());
        if (digit == ' ') {
            // In format [Timer: X]
            digit = text.charAt(tagStart.length() + 1);
        }
        int seconds = readDigit(digit);
        return new TimerProfileImpl(tagStart.substring(1, tagStart.length() - 1), seconds);
    }

}
src/main/java/nl/rutgerkok/blocklocker/ProfileFactory.java
public interface ProfileFactory {

    /**
     * Gets the profile represented by [Everyone] on a sign. Allows everyone
     * else to access the sign.
     * 
     * @return The profile.
     */
    Profile fromEveryone();

    /**
     * Creates a new profile from the given name and id.
     *
     * @param name
     *            The name.
     * @param uuid
     *            The uuid. When absent, comparisons between other
     *            {@link PlayerProfile} the plugin may try to fetch this UUID at
     *            any time based on the name. {@code new UUID(0,0)}
     * @return The profile.
     */
    PlayerProfile fromNameAndUniqueId(String name, Optional<UUID> uuid);

    /**
     * Creates a new profile representing the online player.
     *
     * @param player
     *            The online player.
     * @return The profile.
     */
    PlayerProfile fromPlayer(Player player);

    /**
     * Gets the profile for redstone.
     * 
     * @return The profile.
     */
    Profile fromRedstone();
}
src/main/java/nl/rutgerkok/blocklocker/impl/event/InteractListener.java
public final class InteractListener extends EventListener {

    private static Set<BlockFace> AUTOPLACE_BLOCK_FACES = ImmutableSet.of(BlockFace.NORTH, BlockFace.EAST,
            BlockFace.SOUTH, BlockFace.WEST, BlockFace.UP);

    public InteractListener(BlockLockerPluginImpl plugin) {
        super(plugin);
    }

    private boolean allowedByBlockPlaceEvent(Block placedBlock, BlockState replacedBlockState, Block placedAgainst,
            EquipmentSlot placedUsingHand, Player player) {
        Material originalMaterial = placedBlock.getType();

        ItemStack itemInHand = placedUsingHand == EquipmentSlot.OFF_HAND ? player.getInventory().getItemInOffHand()
                : player.getInventory().getItemInMainHand();
        BlockPlaceEvent placeEvent = new BlockPlaceEvent(placedBlock, replacedBlockState, placedAgainst,
                itemInHand, player, true, placedUsingHand);
        Bukkit.getPluginManager().callEvent(placeEvent);

        Material placedMaterial = placeEvent.getBlockPlaced().getType();
        if (placeEvent.isCancelled() || !placedMaterial.equals(originalMaterial)) {
            // We consider the event cancelled too when the placed block was
            // changed
            return false;
        }
        return true;
    }

    /**
     * Gets whether players are allowed to build in the given game mode.
     *
     * @param gameMode
     *            The game mode, may be null.
     * @return True for survival and creative, false for the other modes.
     */
    private boolean canBuildInMode(GameMode gameMode) {
        if (gameMode == null) {
            return false;
        }
        switch (gameMode) {
            case ADVENTURE:
                return false;
            case CREATIVE:
                return true;
            case SPECTATOR:
                return false;
            case SURVIVAL:
                return true;
            default:
                return false; // Speculative
        }
    }

    private boolean checkAllowed(Player player, Protection protection, boolean clickedSign) {
        PlayerProfile playerProfile = plugin.getProfileFactory().fromPlayer(player);
        boolean allowed = protection.isAllowed(playerProfile);

        // Check for expired protection
        if (!allowed && isExpired(protection)) {
            plugin.getTranslator().sendMessage(player, Translation.PROTECTION_EXPIRED);
            allowed = true;
        }

        // Allow admins to bypass the protection
        if (!allowed && player.hasPermission(Permissions.CAN_BYPASS)) {
            allowed = true;
            if (!clickedSign) {
                // Only show message about bypass when not clicking a sign
                String ownerName = protection.getOwnerDisplayName();
                plugin.getTranslator().sendMessage(player, Translation.PROTECTION_BYPASSED, ownerName);
            }
        }

        return allowed;
    }

    /**
     * Gets the block the inventory is stored in, or null if the inventory is not
     * stored in a block.
     *
     * @param inventory The inventory.
     * @return The block, or null.
     */
    private Block getInventoryBlockOrNull(Inventory inventory) {
        InventoryHolder holder = inventory.getHolder();
        if (holder instanceof BlockState) {
            return ((BlockState) holder).getBlock();
        }
        if (holder instanceof DoubleChest) {
            InventoryHolder leftHolder = ((DoubleChest) holder).getLeftSide();
            if (leftHolder instanceof BlockState) {
                return ((BlockState) leftHolder).getBlock();
            }
        }
        return null;
    }

    private org.bukkit.block.data.type.Sign getRotatedSignPost(Player player, Material signMaterial) {
        float rotation = player.getLocation().getYaw();
        if (rotation < 0) {
            rotation += 360.0f;
        }
        org.bukkit.block.data.type.Sign materialData = (org.bukkit.block.data.type.Sign) signMaterial
                .createBlockData();
        materialData.setRotation(rotationToBlockFace(rotation));
        return materialData;
    }

    private Waterlogged getSignBlockData(BlockFace blockFace, Player player, Material signMaterial) {
        if (blockFace == BlockFace.UP) {
            // Place standing sign in direction of player
            return getRotatedSignPost(player, signMaterial);
        } else {
            // Place attached sign
            WallSign wallSignData = (WallSign) toWallSign(signMaterial).createBlockData();
            wallSignData.setFacing(blockFace);
            return wallSignData;
        }
    }

    private Optional<Material> getSignInHand(Player player, EquipmentSlot hand) {
        PlayerInventory inventory = player.getInventory();
        ItemStack item = hand == EquipmentSlot.OFF_HAND ? inventory.getItemInOffHand() : inventory.getItemInMainHand();
        if (isOfType(item, Tag.SIGNS)) {
            return Optional.of(item.getType());
        }
        return Optional.empty();
    }

    private void handleAllowed(PlayerInteractEvent event, Protection protection, boolean clickedSign,
                               boolean usedOffHand) {
        Block clickedBlock = event.getClickedBlock();
        Player player = event.getPlayer();
        PlayerProfile playerProfile = plugin.getProfileFactory().fromPlayer(player);
        boolean isOwner = protection.isOwner(playerProfile);

        // Select signs
        if (clickedSign) {
            if ((isOwner || player.hasPermission(Permissions.CAN_BYPASS)) && !usedOffHand) {
                Sign sign = (Sign) clickedBlock.getState();
                plugin.getSignSelector().setSelectedSign(player, sign);
                sendSelectedSignMessage(player);
            }
            return;
        }

        // Add [More Users] sign
        if (isOwner && tryPlaceSign(player, clickedBlock, event.getBlockFace(), event.getHand(), SignType.MORE_USERS)) {
            event.setCancelled(true);
            return;
        }

        // Open (double/trap/fence) doors manually
        boolean clickedMainBlock = plugin.getChestSettings().canProtect(clickedBlock);
        if (protection.canBeOpened() && !isSneakPlacing(player) && clickedMainBlock) {
            event.setCancelled(true);

            if (!usedOffHand) {
                if (protection.isOpen()) {
                    protection.setOpen(false, SoundCondition.AUTOMATIC);
                } else {
                    protection.setOpen(true, SoundCondition.AUTOMATIC);
                }

                // Schedule automatic close
                scheduleClose(protection);
            }
        }
    }

    private void handleDisallowed(PlayerInteractEvent event, Protection protection, boolean clickedSign,
            boolean usedOffHand) {
        event.setCancelled(true);

        if (usedOffHand) {
            // Don't send messages
            return;
        }

        Player player = event.getPlayer();
        if (clickedSign) {
            plugin.getTranslator()
                    .sendMessage(player, Translation.PROTECTION_IS_CLAIMED_BY, protection.getOwnerDisplayName());
        } else {
            plugin.getTranslator()
                    .sendMessage(player, Translation.PROTECTION_NO_ACCESS, protection.getOwnerDisplayName());
        }
    }

    private boolean isNullOrAir(ItemStack stack) {
        return stack == null || stack.getType() == Material.AIR || stack.getAmount() == 0;
    }

    private boolean isOfType(ItemStack stackOrNull, Tag<Material> tag) {
        if (stackOrNull == null) {
            return false;
        }
        return tag.isTagged(stackOrNull.getType());
    }

    private boolean isSneakPlacing(Player player) {
        if (!player.isSneaking()) {
            return false;
        }
        if (isNullOrAir(player.getInventory().getItemInMainHand())) {
            return false;
        }
        if (isNullOrAir(player.getInventory().getItemInOffHand())) {
            return false;
        }
        return true;
    }

    @EventHandler(ignoreCancelled = true)
    public void onEntityInteract(EntityInteractEvent event) {
        // Prevents villagers from opening doors
        if (!(event.getEntity() instanceof Villager)) {
            return;
        }
        if (plugin.getChestSettings().allowDestroyBy(AttackType.VILLAGER)) {
           return;
        }
        if (isProtected(event.getBlock())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onInventoryMoveItemEvent(InventoryMoveItemEvent event) {
        Block from = getInventoryBlockOrNull(event.getSource());
        if(from != null) {
            if (isRedstoneDenied(from)) {
                event.setCancelled(true);
                return;
            }
        }
        Block to = getInventoryBlockOrNull(event.getDestination());
        if (to != null) {
            if (isRedstoneDenied(to)) {
                event.setCancelled(true);
                return;
            }
        }
    }

    /**
     * Prevents access to containers.
     *
     * @param event The event object.
     */
    @EventHandler(ignoreCancelled = true)
    public void onPlayerInteract(PlayerInteractEvent event) {
        if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
            return;
        }

        Player player = event.getPlayer();
        Block block = event.getClickedBlock();
        Material material = block.getType();
        boolean clickedSign = Tag.STANDING_SIGNS.isTagged(material) || Tag.WALL_SIGNS.isTagged(material);
        // When using the offhand check, access checks must still be performed,
        // but no messages must be sent
        boolean usedOffHand = event.getHand() == EquipmentSlot.OFF_HAND;
        Optional<Protection> protection = plugin.getProtectionFinder().findProtection(block);

        if (!protection.isPresent()) {
            if (tryPlaceSign(event.getPlayer(), block, event.getBlockFace(), event.getHand(), SignType.PRIVATE)) {
                event.setCancelled(true);
            }
            return;
        }

        // Check if protection needs update
        plugin.getProtectionUpdater().update(protection.get(), false);

        // Check if player is allowed, open door
        if (checkAllowed(player, protection.get(), clickedSign)) {
            handleAllowed(event, protection.get(), clickedSign, usedOffHand);
        } else {
            handleDisallowed(event, protection.get(), clickedSign, usedOffHand);
        }
    }

    private ItemStack removeOneItem(ItemStack item) {
        if (item.getAmount() > 1) {
            item.setAmount(item.getAmount() - 1);
            return item;
        } else {
            return null;
        }
    }

    private void removeSingleSignFromHand(Player player) {
        if (player.getGameMode() == GameMode.CREATIVE) {
            return;
        }

        // Keep order (main, then off hand) the same as in getSignInHand - otherwise you
        // might remove the wrong sign
        PlayerInventory inventory = player.getInventory();
        if (isOfType(inventory.getItemInMainHand(), Tag.SIGNS)) {
            inventory.setItemInMainHand(removeOneItem(inventory.getItemInMainHand()));
        } else if (isOfType(inventory.getItemInOffHand(), Tag.SIGNS)) {
            inventory.setItemInOffHand(removeOneItem(inventory.getItemInOffHand()));
        }
    }

    private BlockFace rotationToBlockFace(float rotation) {
        int intRotation = Math.round((rotation / 360.0f) * 16.0f);
        byte dataValue = (byte) ((intRotation + 8) % 16);
        switch (dataValue) {
            case 0x0:
                return BlockFace.SOUTH;
            case 0x1:
                return BlockFace.SOUTH_SOUTH_WEST;
            case 0x2:
                return BlockFace.SOUTH_WEST;
            case 0x3:
                return BlockFace.WEST_SOUTH_WEST;
            case 0x4:
                return BlockFace.WEST;
            case 0x5:
                return BlockFace.WEST_NORTH_WEST;
            case 0x6:
                return BlockFace.NORTH_WEST;
            case 0x7:
                return BlockFace.NORTH_NORTH_WEST;
            case 0x8:
                return BlockFace.NORTH;
            case 0x9:
                return BlockFace.NORTH_NORTH_EAST;
            case 0xA:
                return BlockFace.NORTH_EAST;
            case 0xB:
                return BlockFace.EAST_NORTH_EAST;
            case 0xC:
                return BlockFace.EAST;
            case 0xD:
                return BlockFace.EAST_SOUTH_EAST;
            case 0xE:
                return BlockFace.SOUTH_EAST;
            case 0xF:
                return BlockFace.SOUTH_SOUTH_EAST;
        }
        throw new RuntimeException("Couldn't handle rotation " + rotation);
    }

    private void scheduleClose(final Protection protection) {
        if (!protection.isOpen()) {
            return;
        }
        int openSeconds = protection.getOpenSeconds();
        if (openSeconds == -1) {
            // Not specified, use default
            openSeconds = plugin.getChestSettings().getDefaultDoorOpenSeconds();
        }
        if (openSeconds <= 0) {
            return;
        }
        plugin.runLater(() -> protection.setOpen(false, SoundCondition.ALWAYS), openSeconds * 20);
    }

    private void sendSelectedSignMessage(Player player) {
        String message = plugin.getTranslator().get(Translation.PROTECTION_SELECTED_SIGN);

        List<BaseComponent> textComponent = new ArrayList<>();
        while (true) {
            int firstOpenBracket = message.indexOf("[");
            if (firstOpenBracket == -1) {
                // No new opening bracket
                TextComponents.addLegacyText(textComponent, message);
                break;
            }

            // Add everything up to the closing bracket
            TextComponents.addLegacyText(textComponent, message.substring(0, firstOpenBracket));

            // Check what's after the opening bracket
            String remainingMessage = message.substring(firstOpenBracket);
            int closeBracketIndex = remainingMessage.indexOf("]");
            if (closeBracketIndex == -1) {
                TextComponents.addLegacyText(textComponent, remainingMessage);
                break;
            }

            // Add this part as link
            String linkText = remainingMessage.substring(0, closeBracketIndex + 1);
            String textOnLine = ChatColor.stripColor(linkText);
            TextComponents.addLegacyText(textComponent, linkText, part -> {
                        part.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
                                "/blocklocker:blocklocker ~ " + textOnLine));
                            });

            // Next!
            message = remainingMessage.substring(closeBracketIndex + 1);
        }

        player.spigot().sendMessage(textComponent.toArray(new BaseComponent[textComponent.size()]));
    }

    private Material toWallSign(Material signMaterial) {
        return Material.valueOf(signMaterial.name().replace("_SIGN", "_WALL_SIGN"));
    }

    private boolean tryPlaceSign(Player player, Block block, BlockFace clickedSide, EquipmentSlot hand,
            SignType signType) {
        if (player.isSneaking() || !canBuildInMode(player.getGameMode())) {
            return false;
        }
        Optional<Material> optionalSignMaterial = getSignInHand(player, hand);
        if (!optionalSignMaterial.isPresent()) {
            return false;
        }
        Material signMaterial = optionalSignMaterial.get();

        if (!plugin.getProtectionFinder().isProtectable(block)) {
            return false;
        }
        if (!player.hasPermission(Permissions.CAN_PROTECT)) {
            return false;
        }
        if (!AUTOPLACE_BLOCK_FACES.contains(clickedSide)) {
            return false;
        }
        try {
            plugin.getLocationCheckers().checkLocationAndPermission(player, block);
        } catch (IllegalLocationException e) {
            return false;
        }

        Block signBlock = block.getRelative(clickedSide);
        boolean waterlogged = false;
        if (!signBlock.getType().isAir()) {
            if (signBlock.getType() != Material.WATER) {
                return false;
            }
            // So block is a water block - check its water level
            waterlogged = ((Levelled) signBlock.getBlockData()).getLevel() == 0;
        }

        // Fire our PlayerProtectionCreateEvent
        if (this.plugin.callEvent(new PlayerProtectionCreateEvent(player, signBlock)).isCancelled()) {
            return false;
        }

        // Create sign and fire Bukkit's BlockPlaceEvent for the sign to be placed
        BlockState oldState = signBlock.getState();
        Waterlogged newBlockData = getSignBlockData(clickedSide, player, signMaterial);
        newBlockData.setWaterlogged(waterlogged);
        signBlock.setBlockData(newBlockData);
        if (!allowedByBlockPlaceEvent(signBlock, oldState, block, hand, player)) {
            // Revert to old state
            oldState.update(true);
            return false;
        }

        // Get state again now that block has been changed, so that it can
        // be casted to Sign
        Sign sign = (Sign) signBlock.getState();

        // Decide what the text on the sign is going to be
        Profile profile = signType.isMainSign() ? plugin.getProfileFactory().fromPlayer(player)
                : plugin.getProfileFactory().fromRedstone();
        ProtectionSign protectionSign = plugin.getProtectionFinder().newProtectionSign(sign, signType, profile);
        String[] newLines = plugin.getSignParser().getDisplayLines(protectionSign);

        // Test if we can place it
        SignChangeEvent signChangeEvent = new SignChangeEvent(sign.getBlock(), player, newLines);
        Bukkit.getPluginManager().callEvent(signChangeEvent);

        if (sign.getBlock().getType() != sign.getType()) {
            // The plugin listening to the event removed/replaced the sign
            removeSingleSignFromHand(player); // We're forced to consume the sign, to avoid item duplication
            return false; // Report as failed
        }

        if (signChangeEvent.isCancelled()) {
            // Event failed, revert to old state
            oldState.update(true); // Remove the entire sign, to avoid leaving an empty sign
            return false; // And report as failed (player will keep the sign)
        }

        // Actually write the text
        for (int i = 0; i < newLines.length; i++) {
            sign.setLine(i, newLines[i]);
        }
        sign.update();

        // Remove the sign from the player's hand
        removeSingleSignFromHand(player);
        return true;
    }

}
src/main/java/nl/rutgerkok/blocklocker/impl/group/TownyGroupSystem.java
public final class TownyGroupSystem extends GroupSystem {

    /**
     * Tests if the Towny plugin is installed.
     *
     * @return True if the factions plugin is installed, false otherwise.
     */
    public static boolean isAvailable() {
        try {
            JavaPlugin.getProvidingPlugin(Towny.class);
            return true;
        } catch (NoClassDefFoundError e) {
            return false;
        }
    }

    @Override
    public boolean isGroupLeader(Player player, String groupName) {
        try {
            Resident resident = TownyUniverse.getInstance().getResident(player.getUniqueId());
            Town town = resident.getTown();
            if (town.getName().equalsIgnoreCase(groupName)) {
                if (town.isMayor(resident) || resident.hasTownRank("assistant")) {
                    return true;
                }
            }

            Nation nation = town.getNation();
            if (nation.getName().equalsIgnoreCase(groupName)) {
                if (nation.isKing(resident) || nation.hasAssistant(resident)) {
                    return true;
                }
            }

            return false;
        } catch (Exception e) {
            // Cannot use catch (NotRegisteredException e) because the class
            // cannot be loaded then when Towny isn't present
            if (e instanceof NotRegisteredException) {
                return false;
            }
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean isInGroup(Player player, String groupName) {
        try {
            Resident resident = TownyUniverse.getInstance().getResident(player.getName());
            Town town = resident.getTown();
            if (town.getName().equalsIgnoreCase(groupName)) {
                return true;
            }

            Nation nation = town.getNation();
            if (nation.getName().equalsIgnoreCase(groupName)) {
                return true;
            }

            return false;
        } catch (Exception e) {
            // Cannot use catch (NotRegisteredException e) because the class
            // cannot be loaded then when Towny isn't present
            if (e instanceof NotRegisteredException) {
                return false;
            }
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/HopperCache.java
public interface HopperCache {

    public enum CacheFlag {
        /**
         * Block is certainly protected.
         */
        PROTECTED,
        /**
         * Block is not protected.
         */
        NOT_PROTECTED,
        /**
         * Not cached; we don't know whether the block is protected or not.
         */
        MISS_CACHE
    }

    /**
     * Gets whether the given block is locked.
     * 
     * @param block
     *            The block.
     * @return Whether the given block is locked.
     */
    CacheFlag getIsRedstoneAllowed(Block block);

    /**
     * Sets whether the given block is locked for redstone.
     * 
     * @param block
     *            The block.
     * @param locked
     *            Whether the block is locked.
     */
    void setIsRedstoneAllowed(Block block, boolean locked);

}
src/main/java/nl/rutgerkok/blocklocker/impl/group/GuildsGroupSystem.java
public final class GuildsGroupSystem extends GroupSystem {

    /**
     * Tests if the Guilds plugin is installed.
     * 
     * @return True if the Guilds plugin is installed, false otherwise.
     */
    public static boolean isAvailable() {
        try {
            JavaPlugin.getProvidingPlugin(GuildsAPI.class);
            return true;
        } catch (NoClassDefFoundError e) {
            return false;
        }
    }

    @Override
    public boolean isGroupLeader(Player player, String groupName) {
        Guild guild = Guilds.getApi().getGuild(player);
        if (guild == null) {
            return false;
        }
        GuildMember master = guild.getGuildMaster();
        if (master == null) {
            // Group has no master, so player is not the group master
            return false;
        }
        if (!master.getUuid().equals(player.getUniqueId())) {
            // Player is not het group master
            return false;
        }
        // Player is the group master, but also check name
        return guild.getName().equalsIgnoreCase(groupName);
    }

    @Override
    public boolean isInGroup(Player player, String groupName) {
        Guild guild = Guilds.getApi().getGuild(player);
        if (guild == null) {
            return false;
        }
        return guild.getName().equalsIgnoreCase(groupName);
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/SignSelector.java
public interface SignSelector {

    /**
     * Gets the sign the player has selected. The player will have permission to
     * edit this sign.
     *
     * @param player
     *            The player.
     * @return The sign the player has selected, if any.
     */
    Optional<Sign> getSelectedSign(Player player);

    /**
     * Sets the sign that the player has selected. This will grant the right to
     * use a command as many times as the player wants to edit this specific
     * sign. In other words: check if the player is actually allowed to edit
     * this sign before calling this method.
     *
     * @param player
     *            The player.
     * @param sign
     *            The sign that the player has selected.
     */
    void setSelectedSign(Player player, Sign sign);
}
src/main/java/nl/rutgerkok/blocklocker/impl/event/BlockLockerCommand.java
public final class BlockLockerCommand implements TabExecutor {

    private static <T> T[] copyArray(T[] original) {
        return Arrays.copyOf(original, original.length);
    }

    private final BlockLockerPlugin plugin;

    public BlockLockerCommand(BlockLockerPlugin plugin) {
        this.plugin = Preconditions.checkNotNull(plugin);
    }

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (args.length == 0) {
            return false;
        }

        if (args[0].equalsIgnoreCase("reload")) {
            return reloadCommand(sender);
        }
        return signChangeCommand(sender, args);
    }

    @Override
    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
        if (args.length == 2) {
            return null;
        }
        if (args.length > 2) {
            return Collections.emptyList();
        }
        return Arrays.asList("2", "3", "4");
    }

    private boolean reloadCommand(CommandSender sender) {
        if (!sender.hasPermission(Permissions.CAN_RELOAD)) {
            plugin.getTranslator().sendMessage(sender, Translation.COMMAND_NO_PERMISSION);
            return true;
        }

        plugin.reload();
        plugin.getLogger().info(plugin.getTranslator().getWithoutColor(Translation.COMMAND_PLUGIN_RELOADED));
        if (!(sender instanceof ConsoleCommandSender)) {
            // Avoid sending message twice to the console
            plugin.getTranslator().sendMessage(sender, Translation.COMMAND_PLUGIN_RELOADED);
        }
        return true;
    }

    private boolean signChangeCommand(CommandSender sender, String[] args) {
        if (!(sender instanceof Player)) {
            plugin.getTranslator().sendMessage(sender, Translation.COMMAND_CANNOT_BE_USED_BY_CONSOLE);
            return true;
        }

        Player player = (Player) sender;
        Optional<Sign> selectedSign = plugin.getSignSelector().getSelectedSign(player);
        if (!selectedSign.isPresent()) {
            plugin.getTranslator().sendMessage(player, Translation.COMMAND_NO_SIGN_SELECTED);
            return true;
        }

        if (args.length > 2) {
            return false;
        }

        // Parse line number
        int lineNumber;
        try {
            lineNumber = Integer.parseInt(args[0]);
        } catch (NumberFormatException e) {
            if (args[0].equals("~")) {
                // Place on first available line, or else the last line
                lineNumber = selectedSign.get().getLine(2).isEmpty() ? 3 : 4;
            } else {
                return false;
            }
        }
        if (lineNumber < 2 || lineNumber > 4) {
            plugin.getTranslator().sendMessage(player, Translation.COMMAND_LINE_NUMBER_OUT_OF_BOUNDS);
            return true;
        }

        // Parse name
        String name = args.length > 1 ? args[1] : "";
        if (name.length() > 25) {
            plugin.getTranslator().sendMessage(player, Translation.COMMAND_PLAYER_NAME_TOO_LONG);
            return true;
        }

        // Check protection
        Sign sign = selectedSign.get();
        Optional<SignType> signType = plugin.getSignParser().getSignType(sign);
        Optional<Protection> protection = plugin.getProtectionFinder().findProtection(sign.getBlock());

        if (!protection.isPresent() || !signType.isPresent()) {
            plugin.getTranslator().sendMessage(player, Translation.COMMAND_SIGN_NO_LONGER_PART_OF_PROTECTION);
            return true;
        }

        // Check line number in combination with sign type
        if (signType.get().isMainSign() && lineNumber == 2 && !player.hasPermission(Permissions.CAN_BYPASS)) {
            plugin.getTranslator().sendMessage(player, Translation.COMMAND_CANNOT_EDIT_OWNER);
            return true;
        }

        // Prepare change of the sign
        String[] newLines = copyArray(sign.getLines());
        newLines[lineNumber - 1] = name;

        // Commit the changes
        SignChangeEvent changeEvent = new SignChangeEvent(sign.getBlock(), player, newLines);
        plugin.callEvent(changeEvent);
        if (!changeEvent.isCancelled()) {
            newLines = changeEvent.getLines();
            for (int i = 0; i < newLines.length; i++) {
                sign.setLine(i, newLines[i]);
            }
            sign.update();
            plugin.getTranslator().sendMessage(player, Translation.COMMAND_UPDATED_SIGN);
        }

        return true;
    }

}
src/main/java/nl/rutgerkok/blocklocker/group/CombinedGroupSystem.java
public final class CombinedGroupSystem extends GroupSystem {

    private List<GroupSystem> systems;

    /**
     * Constructs an initially empty combined group system.
     *
     * @see #addSystem(GroupSystem)
     */
    public CombinedGroupSystem() {
        this.systems = new ArrayList<>();
    }

    /**
     * Adds a new group system. After calling this method, the
     * {@link #isInGroup(Player, String)} method on this object is guaranteed to
     * return true if the {@link #isInGroup(Player, String)} on the given group
     * system returns true too. too.
     *
     * @param system
     *            The group system, may not be null.
     */
    public void addSystem(GroupSystem system) {
        this.systems.add(Preconditions.checkNotNull(system));
    }

    /**
     * @deprecated Just use {@link #addSystem(GroupSystem)}
     * @param systems The systems to add.
     */
    @Deprecated
    public void addSystems(Iterable<GroupSystem> systems) {
        for (GroupSystem system : systems) {
            addSystem(system);
        }
    }

    /**
     * Gets all groups that must be kept on reload.
     * 
     * @return All groups that must be kept.
     */
    public Collection<GroupSystem> getReloadSurvivors() {
        Collection<GroupSystem> reloadSurvivors = new ArrayList<>();
        for (GroupSystem system : this.systems) {
            if (system.keepOnReload()) {
                reloadSurvivors.add(system);
            }
        }
        return reloadSurvivors;
    }

    @Override
    public boolean isGroupLeader(Player player, String groupName) {
        for (GroupSystem system : systems) {
            if (system.isGroupLeader(player, groupName)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isInGroup(Player player, String groupName) {
        for (GroupSystem system : systems) {
            if (system.isInGroup(player, groupName)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }
}
src/main/java/nl/rutgerkok/blocklocker/impl/nms/ServerSpecific.java
public interface ServerSpecific {

    /**
     * Holds the JSON data on a sign.
     */
    public static class JsonSign implements Iterable<SecretSignEntry> {
        public static final JsonSign EMPTY = new JsonSign("", new JsonArray());

        private final String firstLine;
        private final JsonArray jsonData;

        public JsonSign(String firstLine, JsonArray jsonData) {
            this.firstLine = Preconditions.checkNotNull(firstLine);
            this.jsonData = Preconditions.checkNotNull(jsonData);
        }

        /**
         * Gets the text on the first line of a sign, may be an empty string.
         *
         * @return The text.
         */
        public String getFirstLine() {
            return firstLine;
        }

        /**
         * Gets whether there is data on this sign.
         *
         * @return True if there is data, false otherwise.
         */
        public boolean hasData() {
            return this != EMPTY;
        }

        @Override
        public Iterator<SecretSignEntry> iterator() {
            return Iterators.transform(Iterators.filter(jsonData.iterator(), JsonObject.class), JsonSecretSignEntry::new);
        }
    }

    /**
     * Gets the stored {@link JsonObject}s. If the sign contains no extra data at
     * all, an empty {@link Optional} will be returned. Otherwise, all non-null
     * {@link JsonObject}s stored in the sign will be added to the list.
     *
     * @param world
     *            The world the sign is in.
     * @param x
     *            The x position of the sign.
     * @param y
     *            The y position of the sign.
     * @param z
     *            The z position of the sign.
     * @return The extra data, or empty if not found.
     */
    JsonSign getJsonData(World world, int x, int y, int z);

    /**
     * Sets the given JSON array on the sign. The JSON array can have as many
     * elements as you want, and can contain anything that can be serialized as
     * JSON.
     *
     * @param sign
     *            The sign to set the text on.
     * @param jsonArray
     *            The array to store.
     */
    void setJsonData(Sign sign, JsonArray jsonArray);

}
src/main/java/nl/rutgerkok/blocklocker/location/CombinedLocationChecker.java
public final class CombinedLocationChecker implements LocationChecker {

    private final List<LocationChecker> checkers = new ArrayList<>();

    /**
     * Adds a location checker.
     *
     * @param checker
     *            The location checker.
     */
    public void addChecker(LocationChecker checker) {
        this.checkers.add(Objects.requireNonNull(checker, "checker"));
    }

    @Override
    public void checkLocation(Player player, Block block) throws IllegalLocationException {
        for (LocationChecker checker : this.checkers) {
            checker.checkLocation(player, block);
        }
    }

    /**
     * Gets all groups that must be kept on reload.
     *
     * @return All groups that must be kept.
     */
    public Collection<LocationChecker> getReloadSurvivors() {
        Collection<LocationChecker> reloadSurvivors = new ArrayList<>();
        for (LocationChecker checker : this.checkers) {
            if (checker.keepOnReload()) {
                reloadSurvivors.add(checker);
            }
        }
        return reloadSurvivors;
    }

}
src/main/java/nl/rutgerkok/blocklocker/impl/group/SimpleClansGroupSystem.java
public final class SimpleClansGroupSystem extends GroupSystem {

    /**
     * Tests if the Factions plugin is installed.
     *
     * @return True if the factions plugin is installed, false otherwise.
     */
    public static boolean isAvailable() {
        try {
            JavaPlugin.getProvidingPlugin(ClanPlayer.class);
            return true;
        } catch (NoClassDefFoundError e) {
            return false;
        }
    }

    @Override
    public boolean isGroupLeader(Player player, String groupName) {
        SimpleClans simpleClans = JavaPlugin.getPlugin(SimpleClans.class);
        ClanPlayer clanPlayer = simpleClans.getClanManager().getClanPlayer(player.getUniqueId());
        if (!clanPlayer.isLeader()) {
            return false;
        }
        Clan clan = clanPlayer.getClan();
        if (clan == null) {
            return false;
        }
        return clan.getName().equalsIgnoreCase(groupName);
    }

    @Override
    public boolean isInGroup(Player player, String groupName) {
        SimpleClans simpleClans = JavaPlugin.getPlugin(SimpleClans.class);
        ClanPlayer clanPlayer = simpleClans.getClanManager().getClanPlayer(player.getUniqueId());
        Clan clan = clanPlayer.getClan();
        if (clan == null) {
            return false;
        }
        return clan.getName().equalsIgnoreCase(groupName);
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/group/GroupSystem.java
public abstract class GroupSystem {

    /**
     * Gets whether the given player is in a group with the given name. The
     * implementation should make an effort to make the group name case
     * insensitive, but this is not strictly required.
     *
     * @param player
     *            The player to check.
     * @param groupName
     *            The name of the group.
     *
     * @return True if the player is in the given group, false otherwise.
     */
    public abstract boolean isInGroup(Player player, String groupName);

    /**
     * Gets whether the given player is a leader of the given group. The exact
     * definition of a leader is up to the implementation. The implementation
     * should make an effort to make the group name case insensitive, but this
     * is not strictly required.
     *
     * <p>
     * The default implementation just returns false.
     * 
     * @param player
     *            The player to check.
     * @param groupName
     *            The name of the group.
     *
     * @return True if the player is a leader of the given group, false
     *         otherwise.
     */
    public boolean isGroupLeader(Player player, String groupName) {
        return false;
    }

    /**
     * Gets whether this group system must be kept when the plugin is reloaded
     * using the reload command of the plugin.
     *
     * <p>
     * When a group system is removed on reload, it must be re-added after the
     * reload. For group systems provided by other plugins this is problematic,
     * so they must return true. On the other hand, group systems included in
     * BlockLocker will be loaded again by BlockLocker, so they must return
     * false.
     *
     * @return True if the group system must be kept on reload, false otherwise.
     */
    public boolean keepOnReload() {
        // "return true" is the correct option for third-party plugins
        return true;
    }
}
src/main/java/nl/rutgerkok/blocklocker/impl/event/BlockPlaceListener.java
public final class BlockPlaceListener extends EventListener {

    private final Set<UUID> doNotSendChestHintPlayerIds = new HashSet<>();

    public BlockPlaceListener(BlockLockerPluginImpl plugin) {
        super(plugin);
    }

    /**
     * Returns true if there's a protection, but the player is not the owner.
     *
     * @param player
     *            The player.
     * @param block
     *            A block that's part of the protection.
     * @return True if there is a protection, but the player is not the owner. False
     *         otherwise. Still returns false if the player is allowed, but not the
     *         owner.
     */
    private Optional<Protection> getProtectionBySomeoneElse(Player player, Block block) {
        return plugin.getProtectionFinder().findProtection(block).filter(protection -> {
            PlayerProfile playerProfile = plugin.getProfileFactory().fromPlayer(player);
            return !protection.isOwner(playerProfile);
        });
    }

    /**
     * Sends a message that the player can protect a chest.
     *
     * @param event
     *            The block place event.
     */
    @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
    public void onBlockPlace(BlockPlaceEvent event) {
        Player player = event.getPlayer();

        Optional<Protection> interferingProtection = willInterfereWith(player, event.getBlockPlaced());
        if (interferingProtection.isPresent()) {
            // Not allowed to place a block here, would interfere with an existing
            // protection
            if (!event.getPlayer().hasPermission(Permissions.CAN_BYPASS)) {
                plugin.getTranslator().sendMessage(player, Translation.PROTECTION_NO_ACCESS, interferingProtection.get()
                        .getOwnerDisplayName());
                event.setCancelled(true);
                return;
            } else {
                plugin.getTranslator().sendMessage(player, Translation.PROTECTION_BYPASSED, interferingProtection.get()
                        .getOwnerDisplayName());
                return;
            }
        }

        // Rest of the method: check if sign placing hint must be shown
        if (event.getBlockPlaced().getType() != Material.CHEST) {
            return;
        }

        if (!player.hasPermission(Permissions.CAN_PROTECT)) {
            return;
        }

        try {
            plugin.getLocationCheckers().checkLocationAndPermission(player, event.getBlockPlaced());
        } catch (IllegalLocationException e) {
            return; // Cannot place protection here, so don't show hint
        }

        sendChestHint(player);
    }

    /**
     * Sends a chest hint. Includes a simple spam limiter.
     *
     * @param player
     *            The player.
     */
    private void sendChestHint(Player player) {
        UUID playerId = player.getUniqueId();
        if (this.doNotSendChestHintPlayerIds.contains(playerId)) {
            return;
        }
        plugin.getTranslator().sendMessage(player, Translation.PROTECTION_CHEST_HINT);

        // Temporarily suppress chest hint (120 seconds)
        this.doNotSendChestHintPlayerIds.add(playerId);
        plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
            // Note: we only keep a reference to the player id, not the entire player
            // object, which might be replaced
            this.doNotSendChestHintPlayerIds
                    .remove(playerId);
        }, 20 * 120);
    }

    private Optional<Protection> willInterfereWith(Player player, Block block) {
        if (this.plugin.getChestSettings().getConnectContainers()) {
            return willInterfereWithConnectedContainers(player, block);
        } else {
            return willInterfereWithStandard(player, block);
        }
    }

    private Optional<Protection> willInterfereWithConnectedContainers(Player player, Block block) {
        // Used for interference check when connectedContainers is on

        if (block.getType().equals(Material.CHEST) || block.getType().equals(Material.TRAPPED_CHEST)) {
            // Single left chest may interfere with right chest that locked by others, and
            // vice versa
            for (BlockFace searchFace : BlockFinder.NORTH_EAST_SOUTH_WEST_UP_DOWN) {
                Block nearBlock = block.getRelative(searchFace);

                if (block.getType() != nearBlock.getType()) {
                    continue;
                }

                Optional<Protection> protectionBySomeoneElse = getProtectionBySomeoneElse(player, nearBlock);
                if (protectionBySomeoneElse.isPresent()) {
                    return protectionBySomeoneElse;
                }
            }
        } else if (Tag.DOORS.isTagged(block.getType())) {
            // Left door may interfere with right door that locked by others, and vice
            // versa
            for (BlockFace searchFace : BlockFinder.CARDINAL_FACES) {
                Block nearBlock = block.getRelative(searchFace);
                if (block.getType() != nearBlock.getType()) {
                    continue;
                }
                Optional<Protection> protectionBySomeoneElse = getProtectionBySomeoneElse(player, nearBlock);
                if (protectionBySomeoneElse.isPresent()) {
                    return protectionBySomeoneElse;
                }
            }
        }
        return Optional.empty();
    }

    private Optional<Protection> willInterfereWithStandard(Player player, Block block) {
        // Used for interference check when connectedContainers is off
        if (block.getType().equals(Material.CHEST) || block.getType().equals(Material.TRAPPED_CHEST)) {
            // Single left chest may interfere with right chest that locked by others, and
            // vice versa
            if (player.isSneaking()) {
                return Optional.empty(); // If a player is sneaking, the chest will not connect, so it's safe
            }
            for (BlockFace searchFace : BlockFinder.CARDINAL_FACES) {
                Block nearBlock = block.getRelative(searchFace);

                if (block.getType() != nearBlock.getType()) {
                    continue;
                }
                BlockFace faceOfNearBlock = ((Directional) nearBlock.getBlockData()).getFacing();
                BlockFace faceOfBlock = ((Directional) block.getBlockData()).getFacing();
                boolean alreadyADoubleChest = (((InventoryHolder) nearBlock
                        .getState()).getInventory().getHolder() instanceof DoubleChest);
                if (alreadyADoubleChest) {
                    continue;
                }

                // You are allowed to place two NORTH facing chests in adjacent blocks, provided
                // they are behind each other
                boolean areSiblings = (faceOfNearBlock == faceOfBlock) && faceOfBlock != searchFace
                        && faceOfBlock != searchFace.getOppositeFace();

                if (!areSiblings) {
                    continue;
                }

                Optional<Protection> protectionBySomeoneElse = getProtectionBySomeoneElse(player, nearBlock);
                if (protectionBySomeoneElse.isPresent()) {
                    return protectionBySomeoneElse;
                }
            }
        } else if (Tag.DOORS.isTagged(block.getType())) {
            // Left door may interfere with right door that locked by others, and vice
            // versa
            for (BlockFace searchFace : BlockFinder.CARDINAL_FACES) {
                Block nearBlock = block.getRelative(searchFace);
                if (block.getType() != nearBlock.getType()) {
                    continue;
                }
                Optional<Protection> protectionBySomeoneElse = getProtectionBySomeoneElse(player, nearBlock);
                if (protectionBySomeoneElse.isPresent()) {
                    return protectionBySomeoneElse;
                }
            }
        }
        return Optional.empty();
    }


}
src/main/java/nl/rutgerkok/blocklocker/impl/group/FactionsGroupSystem.java
public final class FactionsGroupSystem extends GroupSystem {

    /**
     * Tests if the Factions plugin is installed.
     * 
     * @return True if the factions plugin is installed, false otherwise.
     */
    public static boolean isAvailable() {
        try {
            JavaPlugin.getProvidingPlugin(MPlayer.class);
            return true;
        } catch (NoClassDefFoundError e) {
            return false;
        }
    }

    @Override
    public boolean isInGroup(Player player, String groupName) {
        MPlayer mPlayer = MPlayer.get(player);
        Faction faction = mPlayer.getFaction();
        return (faction != null && faction.getName().equalsIgnoreCase(groupName));
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/impl/group/mcMMOGroupSystem.java
public final class mcMMOGroupSystem extends GroupSystem {

	/**
	 * Tests if the mcMMO plugin is installed.
	 * 
	 * @return True if the mcMMO plugin is installed, false otherwise.
	 */
	public static boolean isAvailable() {
		try {
			JavaPlugin.getProvidingPlugin(mcMMO.class);
			return true;
		} catch (NoClassDefFoundError e) {
			return false;
		}
	}
	
	@Override
	public boolean isGroupLeader(Player player, String groupName) {
		if (!isInGroup(player, groupName)) {
			return false;
		}
		
		String leader = PartyAPI.getPartyLeader(groupName);
		
		return Objects.equals(player.getName(), leader) ? true : false;
	}
	
	@Override
	public boolean isInGroup(Player player, String groupName) {
		if (!PartyAPI.inParty(player)) {
			// Player is not in a party
			return false;
		}
		
		String partyName = PartyAPI.getPartyName(player);

		// Ignore case, mcMMO is not case sensitive
	    return partyName.equalsIgnoreCase(groupName);
	}
	
	@Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/BlockLockerPlugin.java
public interface BlockLockerPlugin {

    /**
	 * Calls the given event, allowing other plugins to react to this event.
	 *
	 * @param <E> The type of the event.
	 * @param event The event, may not be null.
	 * @return The same event, for chaining.
	 */
	<E extends Event> E callEvent(E event);

    /**
     * Gets the {@link ChestSettings} object, containing all customizable
     * aspects of the plugin.
     *
     * @return The settings object.
     */
    ChestSettings getChestSettings();

    /**
     * Gets the combined group system of the plugin, which can be used to add
     * other group systems.
     *
     * @return The combined group system.
     * @see CombinedGroupSystem#addSystem(GroupSystem)
     */
    CombinedGroupSystem getGroupSystems();

    /**
     * Gets the cache that stores whether a block is protected or not.
     * @return The cache.
     */
    HopperCache getHopperCache();

    /**
     * Gets the location checkers, which are used to prevent players from placing chests in the wilderness.
     * @return A location checker.
     */
    CombinedLocationChecker getLocationCheckers();

    /**
     * Gets the logger of the plugin.
     *
     * @return The logger.
     */
    Logger getLogger();

    /**
     * Gets the profile factory, used to create profiles.
     *
     * @return The profile factory.
     */
    ProfileFactory getProfileFactory();

    /**
     * Gets the protection finder, used to find protections in the world.
     *
     * @return The protection finder.
     */
    ProtectionFinder getProtectionFinder();

    /**
     * Gets the protection updater, used to mark protections as needing an
     * update, for example for fixing missing UUIDs.
     *
     * @return The protection updater.
     */
    ProtectionUpdater getProtectionUpdater();

    /**
     * Gets the {@link SignParser} object.
     *
     * @return The sign parser.
     */
    SignParser getSignParser();

    /**
     * Gets the sign selector, that holds the currently selected signs of all
     * players.
     *
     * @return The sign selector.
     */
    SignSelector getSignSelector();

    /**
     * Gets the translator, used to translate messages.
     *
     * @return The translator.
     */
    Translator getTranslator();

    /**
     * Reloads the configuration files of the plugin.
     */
    void reload();

    /**
     * Runs a task the next tick on the server thread.
     *
     * @param runnable
     *            The task.
     */
    void runLater(Runnable runnable);

    /**
     * Runs a task in the given amount of ticks on the server thread.
     *
     * @param runnable
     *            The task.
     * @param ticks
     *            In how many ticks the method needs to run.
     */
    void runLater(Runnable runnable, int ticks);
}
src/main/java/nl/rutgerkok/blocklocker/impl/updater/Updater.java
public final class Updater {

    /**
     * Every twelve hours.
     */
    private static final long CHECK_INTERVAL = 20 * 60 * 60 * 12;

    private final Plugin plugin;
    private volatile UpdatePreference preference;
    private final BukkitRunnable task = new BukkitRunnable() {
        @Override
        public void run() {
            if (preference.checkForUpdates()) {
                updateSync();
            } else {
                this.cancel();
            }
        }
    };
    private final Translator translator;

    public Updater(UpdatePreference preference, Translator translator, Plugin plugin) {
        this.preference = Preconditions.checkNotNull(preference);
        this.translator = Preconditions.checkNotNull(translator);
        this.plugin = Preconditions.checkNotNull(plugin);

    }

    private Optional<String> getMinecraftVersion() {
        String serverVersion = plugin.getServer().getVersion();
        String regex = "MC\\: *([A-Za-z0-9\\._\\-]+)";
        Matcher matcher = Pattern.compile(regex).matcher(serverVersion);
        if (matcher.find() && matcher.groupCount() == 1) {
            return Optional.of(matcher.group(1));
        } else {
            return Optional.empty();
        }
    }

    /**
     * Notifies admins of the server of an updated version of the plugin. Can be
     * called from any thread.
     *
     * @param result
     *            The update result.
     */
    private void notifyServer(final UpdateResult result) {
        if (!result.hasNotification()) {
            return;
        }

        // Disable further update checks
        preference = UpdatePreference.DISABLED;

        // Notify admins of existing result
        if (plugin.getServer().isPrimaryThread()) {
            notifyServerFromServerThread(result);
        } else {
            plugin.getServer().getScheduler().runTask(plugin, new Runnable() {
                @Override
                public void run() {
                    notifyServerFromServerThread(result);
                }
            });
        }
    }

    private void notifyServerFromServerThread(UpdateResult result) {
        // Must be called from notifyServer
        // Result must have a notification

        UpdateNotifier notifier = new UpdateNotifier(translator, result);

        // Notify players
        plugin.getServer().getPluginManager().registerEvents(notifier, plugin);

        // Notify console
        notifier.sendNotification(plugin.getServer().getConsoleSender());
    }

    /**
     * Starts the update process. Does nothing if updates have been disabled.
     *
     * @throws IllegalStateException
     *             If this method was called earlier.
     */
    public void startUpdater() {
        if (!preference.checkForUpdates()) {
            return;
        }
        task.runTaskTimerAsynchronously(plugin, 1, CHECK_INTERVAL);
    }

    private void updateInstallSync(UpdateCheckResult result) throws IOException {
        Optional<String> minecraftVersion = getMinecraftVersion();

        if (result.getMinecraftVersions().containsAll(ImmutableSet.of(minecraftVersion.get()))) {
            // Notify that an update is available
            notifyServer(new UpdateResult(Status.MANUAL_UPDATE, result));
        } else {
            // Server version no longer supported
            notifyServer(new UpdateResult(Status.UNSUPPORTED_SERVER, result));
        }
    }

    /**
     * Blocking update method, must <b>not</b> be called from the server thread.
     */
    private void updateSync() {
        try {
            UpdateChecker checker = new UpdateChecker();
            UpdateCheckResult result = checker.checkForUpdatesSync(plugin);
            if (result.needsUpdate()) {
                updateInstallSync(result);
            } else {
                notifyServer(new UpdateResult(Status.NO_UPDATE, result));
            }
        } catch (IOException e) {
            plugin.getLogger().log(Level.WARNING, "Error during update check"
                    + " (you can disable automatic updates in the config file)", e);
            notifyServer(UpdateResult.failed());
        }
    }

}
src/main/java/nl/rutgerkok/blocklocker/Translator.java
public abstract class Translator {

    public enum Translation {
        COMMAND_CANNOT_BE_USED_BY_CONSOLE,
        COMMAND_CANNOT_EDIT_OWNER,
        COMMAND_LINE_NUMBER_OUT_OF_BOUNDS,
        COMMAND_NO_PERMISSION,
        COMMAND_NO_SIGN_SELECTED,
        COMMAND_PLAYER_NAME_TOO_LONG,
        COMMAND_PLUGIN_RELOADED,
        COMMAND_SIGN_NO_LONGER_PART_OF_PROTECTION,
        COMMAND_UPDATED_SIGN,
        PROTECTION_ADD_MORE_USERS_SIGN_INSTEAD,
        PROTECTION_BYPASSED,
        PROTECTION_CANNOT_CHANGE_SIGN,
        PROTECTION_CHEST_HINT,
        PROTECTION_CLAIMED_CONTAINER,
        PROTECTION_CLAIMED_MANUALLY,
        PROTECTION_EXPIRED,
        PROTECTION_IN_WILDERNESS,
        PROTECTION_IS_CLAIMED_BY,
        PROTECTION_NO_ACCESS,
        PROTECTION_NO_PERMISSION_FOR_CLAIM,
        PROTECTION_NOT_NEARBY,
        PROTECTION_SELECTED_SIGN,
        TAG_EVERYONE,
        TAG_MORE_USERS,
        TAG_PRIVATE,
        TAG_REDSTONE,
        TAG_TIMER,
        UPDATER_MORE_INFORMATION,
        UPDATER_UNSUPPORTED_SERVER,
        UPDATER_UPDATE_AVAILABLE;

        /**
         * Gets the key used in configuration files.
         */
        @Override
        public String toString() {
            return name().replaceFirst("_", ".").toLowerCase(Locale.ROOT);
        }
    }

    /**
     * Returns the translation with the given key. If no such translation
     * exists, the key is returned.
     *
     * @param key
     *            The key of the translation.
     * @return The translation, or the key if not found.
     */
    public abstract String get(Translation key);

    /**
     * Returns a list of all possible translations.
     *
     * @param key
     * 			The key of the translation.
     * @return A list of all possible translations, or the key (in a list) if not found.
     */
    public abstract List<String> getAll(Translation key);

    /**
     * Same as {@link #getAll(Translation)}, but with
     * {@link ChatColor#stripColor(String)} applied.
     *
     * @param key
     * 			The key of the translation.
     * @return A list of all possible translations, or the key (in a list) if not found.
     */
    public abstract List<String> getAllWithoutColor(Translation key);

    /**
     * Same as {@link #get(Translation)}, but with
     * {@link ChatColor#stripColor(String)} applied.
     *
     * @param key
     *            The key of the translation.
     * @return The translation, or the key if not found.
     */
    public abstract String getWithoutColor(Translation key);

    /**
     * Sends the specified message translated to the given player.
     *
     * @param player
     *            The player (or console) to the send the message to.
     * @param translation
     *            The message to send.
     */
    public abstract void sendMessage(CommandSender player, Translation translation);

    /**
     * Sends the specified message translated to the given player.
     *
     * @param player
     *            The player (or console) to the send the message to.
     * @param translation
     *            The message to send.
     * @param parameters
     *            Replacements for the message. {0} will be replaced by the
     *            first parameter, etc.
     */
    public final void sendMessage(CommandSender player, Translation translation, String... parameters) {
        String translated = get(translation);
        for (int i = 0; i < parameters.length; i++) {
            translated = translated.replace("{" + i + "}", parameters[i]);
        }
        player.sendMessage(translated);
    }
}
src/main/java/nl/rutgerkok/blocklocker/impl/group/PermissionsGroupSystem.java
public final class PermissionsGroupSystem extends GroupSystem {

    @Override
    public boolean isInGroup(Player player, String groupName) {
        return player.hasPermission(Permissions.getGroupNode(groupName));
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/location/LocationChecker.java
public interface LocationChecker {

    /**
     * Checks whether people can protect chests here.
     *
     * @param player
     *            The player placing the chest.
     * @param block
     *            The location.
     * @throws IllegalLocationException
     *             If the player is not allowed to place a chest here. The exception
     *             should include a message why.
     */
    void checkLocation(Player player, Block block) throws IllegalLocationException;

    /**
     * Checks whether people can protect chests here. For players that have the
     * {@link Permissions#CAN_WILDERNESS} permission node, this is always the case.
     *
     * @param player
     *            The player placing the chest.
     * @param block
     *            The location.
     * @throws IllegalLocationException
     *             If the player is not allowed to place a chest here. The exception
     *             should include a message why.
     */
    default void checkLocationAndPermission(Player player, Block block) throws IllegalLocationException {
        if (player.hasPermission(Permissions.CAN_WILDERNESS)) {
            return;
        }
        checkLocation(player, block);
    }

    /**
     * Gets whether this location checker must be kept when the plugin is reloaded
     * using the reload command of the plugin.
     *
     * <p>
     * When a location checker is removed on reload, it must be re-added after the
     * reload. For location checkers provided by other plugins this is problematic,
     * so they must return true. On the other hand, group systems included in
     * BlockLocker will be loaded again by BlockLocker, so they must return false.
     *
     * @return True if the group system must be kept on reload, false otherwise.
     */
    default boolean keepOnReload() {
        return true;
    }
}
src/main/java/nl/rutgerkok/blocklocker/impl/event/SignChangeListener.java
public class SignChangeListener extends EventListener {

    public SignChangeListener(BlockLockerPluginImpl plugin) {
        super(plugin);
    }

    private void handleSignNearbyProtection(SignChangeEvent event, Protection protection) {
        Block block = event.getBlock();
        Player player = event.getPlayer();

        Profile playerProfile = plugin.getProfileFactory().fromPlayer(player);
        Optional<SignType> newSignType = plugin.getSignParser().getSignType(event);
        boolean isExistingSign = this.isExistingSign(event.getBlock());

        // Only protection signs should be handled
        if (!newSignType.isPresent() && !isExistingSign) {
            return;
        }

        // Only the owner may add (or edit) signs nearby a protection
        if (!protection.isOwner(playerProfile) && !player.hasPermission(Permissions.CAN_BYPASS)) {
            plugin.getTranslator().sendMessage(player, Translation.PROTECTION_CANNOT_CHANGE_SIGN);
            event.setCancelled(true);
            return;
        }

        if (newSignType.get().isMainSign()) {
            // Edited a main sign (for an existing protection)
            if (isExistingSign) {
                // Make sure the owner name on the sign stays the same
                // (except for players with the correct permission)
                if (!player.hasPermission(Permissions.CAN_BYPASS)) {
                    Optional<Profile> owner = protection.getOwner();
                    if (owner.isPresent()) {
                        event.setLine(1, owner.get().getDisplayName());
                    }
                }
            } else {
                // Second main sign is not allowed
                plugin.getTranslator().sendMessage(player, Translation.PROTECTION_ADD_MORE_USERS_SIGN_INSTEAD);
                block.breakNaturally();
                event.setCancelled(true);
                return;
            }
        }

        // Mark protection for UUID update
        // Note that we can't use the Protection instance for this, it will
        // still use the outdated signs the next tick
        updateBlockForUniqueIdsSoon(block);
    }

    private void handleSignNotNearbyProtection(SignChangeEvent event) {
        Optional<SignType> parsedSign = plugin.getSignParser().getSignType(event);
        if (!parsedSign.isPresent()) {
            // Not trying to claim a container
            return;
        }

        SignType signType = parsedSign.get();
        Player player = event.getPlayer();
        Block block = event.getBlock();

        if (!player.hasPermission(Permissions.CAN_PROTECT)) {
            plugin.getTranslator().sendMessage(player, Translation.PROTECTION_NO_PERMISSION_FOR_CLAIM);
            block.breakNaturally();
            event.setCancelled(true);
            return;
        }

        // Don't allow placing signs in the wilderness
        try {
            plugin.getLocationCheckers().checkLocationAndPermission(player, block);
        } catch (IllegalLocationException e) {
            player.sendMessage(e.getTranslatedMessage(plugin.getTranslator()));
            block.breakNaturally();
            event.setCancelled(true);
            return;
        }

        // Only the main sign can be used to create new protections
        if (!signType.isMainSign()) {
            plugin.getTranslator().sendMessage(player, Translation.PROTECTION_NOT_NEARBY);
            block.breakNaturally();
            event.setCancelled(true);
            return;
        }

        // Sign must be attached to container
        if (!plugin.getProtectionFinder().isSignNearbyProtectable(block)) {
            plugin.getTranslator().sendMessage(player, Translation.PROTECTION_NOT_NEARBY);
            block.breakNaturally();
            event.setCancelled(true);
            return;
        }

		// Check event
		if (this.plugin.callEvent(new PlayerProtectionCreateEvent(event.getPlayer(), block)).isCancelled()) {
			block.breakNaturally();
			event.setCancelled(true);
			return;
		}

        // Make sure the owner name is on the second line
        event.setLine(1, event.getPlayer().getName());

        // We need to fetch UUIDs soon
        updateBlockForUniqueIdsSoon(block);

        // Send confirmation
        plugin.getTranslator().sendMessage(player, Translation.PROTECTION_CLAIMED_MANUALLY);
    }

    private boolean isExistingSign(Block block) {
        BlockState blockState = block.getState();
        if (blockState instanceof Sign) {
            return plugin.getSignParser().hasValidHeader((Sign) blockState);
        }
        return false;
    }

    @EventHandler(ignoreCancelled = true)
    public void onSignChange(SignChangeEvent event) {
        Optional<Protection> protection = plugin.getProtectionFinder().findExistingProtectionForNewSign(event.getBlock());

        if (protection.isPresent()) {
            // Changing a sign near a container
            handleSignNearbyProtection(event, protection.get());
        } else {
            // Changing a sign not nearby a container
            handleSignNotNearbyProtection(event);
        }
    }

    /**
     * Call this method for newly created protections. The next tick the
     * protection will be found and updated for uuids.
     *
     * @param block
     *            The block that will be part of the protection.
     */
    private void updateBlockForUniqueIdsSoon(final Block block) {
        plugin.runLater(new Runnable() {
            @Override
            public void run() {
                Optional<Protection> protection = plugin.getProtectionFinder().findProtection(block);
                if (protection.isPresent()) {
                    plugin.getProtectionUpdater().update(protection.get(), true);
                }
            }
        });
    }
}
src/main/java/nl/rutgerkok/blocklocker/impl/nms/NMSAccessorProvider.java
public class NMSAccessorProvider {
    /**
     * Gets the appropriate server-specific code.
     *
     * @return The code.
     */
    public static ServerSpecific create() {
        try {
            return new OldNMSAccessor();
        } catch (Exception e) {
            try {
                return new NMS116Accessor();
            } catch (Exception e2) {
                try {
                return new NMSAccessor();
                } catch (Exception e3) {
                    return new NoNMSAccessor();
                }
            }
        }
    }
}
src/main/java/nl/rutgerkok/blocklocker/ChestSettings.java
public interface ChestSettings extends ProtectableBlocksSettings {

    /**
     * Gets whether the given attack type is allowed to destroy protections.
     *
     * @param attackType
     *            The attack type.
     * @return True if the attack type is allowed to destroy protections, false
     *         otherwise.
     */
    boolean allowDestroyBy(AttackType attackType);

    /**
     * Returns whether containers of the same type require only one sign.
     * @return True if containers are connected, false otherwise.
     */
    boolean getConnectContainers();

    /**
     * Gets the actual date that chests must have activity after. If a chest
     * doesn't have activity after this date, it is considered expired.
     *
     * @return The date, or absent if chests never expire.
     */
    Optional<Date> getChestExpireDate();

    /**
     * Gets the default amount of ticks a door stays open before closing
     * automatically. When set to less than 1, the door is never closed
     * automatically. Players can override this value for a door.
     *
     * @return The amount.
     */
    int getDefaultDoorOpenSeconds();

    /**
     * Gets a mutable list of objects that specify additional protectables. You can
     * add your own instances here (that implement
     * {@link ProtectableBlocksSettings}).
     *
     * @return The mutable list.
     */
    List<ProtectableBlocksSettings> getExtraProtectables();

    /**
     * Gets the localized header for the given sign type, includes brackets and
     * colors.
     *
     * @param signType
     *            The type of the sign.
     * @param header
     * 			  The first line of the sign, to see which header to return.
     * @return The header.
     */
    String getFancyLocalizedHeader(SignType signType, String header);

    /**
     * Gets the type of the protection.
     *
     * @param block
     *            The main protection block.
     * @return Type of the protection.
     */
    Optional<ProtectionType> getProtectionType(Block block);

    /**
     * Gets the localized headers for the given sign type, without colors.
     *
     * @param signType
     *            The type of the sign.
     * @return All possible headers.
     */
    List<String> getSimpleLocalizedHeaders(SignType signType);

}
src/main/java/nl/rutgerkok/blocklocker/SignParser.java
public interface SignParser {

    /**
     * Gets the text-only display lines that should be displayed on the sign.
     * @param sign The sign.
     * @return The display text.
     */
    String[] getDisplayLines(ProtectionSign sign);

    /**
     * Gets the type of the sign.
     *
     * @param sign
     *            The sign.
     * @return The type of the sign.
     */
    Optional<SignType> getSignType(Sign sign);

    /**
     * Gets the type of the sign from the {@link SignChangeEvent}. The event
     * fires before the sign in the world is updated, so trying to read from the
     * world will result in outdated information.
     *
     * @param event
     *            The sign change event.
     * @return The type of the sign.
     */
    Optional<SignType> getSignType(SignChangeEvent event);

    /**
     * Gets whether the sign header is valid. Calling this method is the same as
     * calling {@code getSignType(sign).isPresent()}.
     *
     * @param sign
     *            The sign to check.
     * @return True if the header is valid, false otherwise.
     */
    boolean hasValidHeader(Sign sign);

    /**
     * Parses the given sign for all names on it.
     *
     * @param signBlock
     *            The sign to parse.
     * @return The parsed sign.
     */
    Optional<ProtectionSign> parseSign(Block signBlock);

    /**
     * Saves the contents of the given sign to the world.
     *
     * @param sign
     *            The sign to save.
     */
    void saveSign(ProtectionSign sign);

}
src/main/java/nl/rutgerkok/blocklocker/ProtectionUpdater.java
public interface ProtectionUpdater {

    /**
     * Does generic cleaning for the signs on the protection at some point in the
     * near future. Fixes the missing UUIDs in the given protection, by looking them
     * up if the player is online.
     *
     * @param protection
     *            The protection to fix. If the protection was already queued for an
     *            update, nothing happens.
     * @param newProtection
     *            True if this protection is new, false otherwise. Currently has no
     *            effect.
     */
    void update(Protection protection, boolean newProtection);

}
src/main/java/nl/rutgerkok/blocklocker/ProtectionFinder.java
public interface ProtectionFinder {

    /**
     * Gets the protection the sign is placed against. The contents of the sign
     * is not checked; in other words: the sign doesn't have to be part of the
     * protection (yet).
     *
     * @param signBlock
     *            The sign.
     * @return The protection, or empty if the block is not a sign or not placed
     *         against a protection.
     */
    Optional<Protection> findExistingProtectionForNewSign(Block signBlock);

    /**
     * Gets the protection the given block is part of. If the block is a sign,
     * the sign must already be filled in.
     *
     * @param block
     *            The block to search at.
     * @return The protection, if any.
     */
    Optional<Protection> findProtection(Block block);

    /**
     * Gets the protection the given block is part of. If the block is a sign,
     * the sign must already be filled in.
     *
     * @param block
     *            The block to search at.
     * @param searchMode
     *            The search mode.
     * @return The protection, if any.
     */
    Optional<Protection> findProtection(Block block, SearchMode searchMode);

    /**
     * Gets whether this block can be protected by a sign. This can either be
     * because it is itself a block that can be locked (like a chest) or because it
     * supports a block that is protectable (like the block below a door). Note that
     * this method returns false for signs, since you cannot protect a sign with
     * another sign. See {@link #isSignNearbyProtectable(Block)} for that case.
     * 
     * @param block
     *            The block to check.
     * @return True if the block is protectable, false otherwise.
     */
    boolean isProtectable(Block block);

    /**
     * Gets whether the given sign is near a block that can be protected. In
     * other words, this method returns true if the sign is attached to a chest,
     * furnace etc. or placed above a door.
     *
     * <p>
     * This method doesn't care whether the block that can be protected is
     * actually protected.
     * 
     * @param signBlock
     *            The sign block.
     * @return True if the sign is placed near a block that can be protected.
     */
    boolean isSignNearbyProtectable(Block signBlock);

    /**
     * Creates a new protection sign, ignoring the content already on the sign.
     * 
     * <p>
     * To inspect the contents of existing signs, use
     * {@link #findProtection(Block)}.
     * 
     * @param sign
     *            The sign.
     * @param signType
     *            Type to set the sign to.
     * @param onFirstLine
     *            Person to place on the first line.
     * @return The new protection sign.
     */
    ProtectionSign newProtectionSign(Sign sign, SignType signType, Profile onFirstLine);
}
src/main/java/nl/rutgerkok/blocklocker/ProtectableBlocksSettings.java
public interface ProtectableBlocksSettings {

    /**
     * Gets whether the block can be protected by any protection type (container,
     * door, etc.)
     *
     * @param block
     *            Block to check. Note: if the block can be protected not because of
     *            it's type/structure, but because of where it is located, it is
     *            better to check this using a {@link LocationChecker} instead.
     * @return True if the block can be protected by the given type, false
     *         otherwise.
     */
    boolean canProtect(Block block);

    /**
     * Gets whether the material can be protected by the given protection type.
     *
     * @param type
     *            Protection type that must be checked for being able to protect
     *            this type. Note: if the block can be protected not because of it's
     *            type/structure, but because of where it is located, it is better
     *            to check this using a {@link LocationChecker} instead.
     * @param block
     *            Block to check.
     *
     * @return True if the block can be protected by the given type, false
     *         otherwise.
     */
    boolean canProtect(ProtectionType type, Block block);
}
src/main/java/nl/rutgerkok/blocklocker/impl/blockfinder/BlockFinder.java
public abstract class BlockFinder {

    public static final BlockFace[] CARDINAL_FACES = { BlockFace.NORTH, BlockFace.EAST,
                BlockFace.SOUTH, BlockFace.WEST };
    private static final BlockFace[] SIGN_ATTACHMENT_FACES = { BlockFace.NORTH, BlockFace.EAST,
                BlockFace.SOUTH, BlockFace.WEST, BlockFace.UP };
    public static final BlockFace[] VERTICAL_FACES = { BlockFace.UP, BlockFace.DOWN };
    public static final BlockFace[] NORTH_EAST_SOUTH_WEST_UP_DOWN = { BlockFace.NORTH, BlockFace.EAST,
            BlockFace.SOUTH, BlockFace.WEST, BlockFace.UP, BlockFace.DOWN };

    /**
     * Creates a new block finder.
     *
     * @param parser
     *            The parser of signs.
     * @param connectContainers
     *            Whether containers must be connected.
     * @return The block finder.
     */
    public static BlockFinder create(SignParser parser, boolean connectContainers) {
        if (connectContainers) {
            return new ConnectedContainersBlockFinder(parser);
        } else {
            return new SeparateContainersBlockFinder(parser);
        }
    }

    protected final SignParser parser;

    BlockFinder(SignParser parser) {
        this.parser = Preconditions.checkNotNull(parser);
    }

    /**
     * Finds all attached signs to a block, that are valid protection signs.
     *
     * @param block
     *            The block to check attached signs on.
     * @return The signs.
     */
    public Collection<ProtectionSign> findAttachedSigns(Block block) {
        Collection<ProtectionSign> signs = new ArrayList<>();
        for (BlockFace face : SIGN_ATTACHMENT_FACES) {
            Block atPosition = block.getRelative(face);
            Material material = atPosition.getType();
            if (!Tag.WALL_SIGNS.isTagged(material) && !Tag.STANDING_SIGNS.isTagged(material)) {
                continue;
            }
            if (!isAttachedSign(atPosition, block)) {
                continue;
            }
            Optional<ProtectionSign> parsedSign = parser.parseSign(atPosition);
            if (parsedSign.isPresent()) {
                signs.add(parsedSign.get());
            }
        }
        return signs;
    }

    /**
     * Finds all attached signs to a block, that are valid protection signs.
     *
     * @param blocks
     *            The blocks to check attached signs on.
     * @return The signs.
     */
    public Collection<ProtectionSign> findAttachedSigns(Collection<Block> blocks) {
        if (blocks.size() == 1) {
            // Avoid creating a builder, iterator and extra set
            return findAttachedSigns(blocks.iterator().next());
        }

        ImmutableSet.Builder<ProtectionSign> signs = ImmutableSet.builder();
        for (Block block : blocks) {
            signs.addAll(findAttachedSigns(block));
        }
        return signs.build();
    }

    /**
     * Searches for containers of the same type attached to this container.
     *
     * @param block
     *            The container.
     * @return List of attached containers, including the given container.
     */
    public abstract List<Block> findContainerNeighbors(Block block);

    /**
     * Gets the block that supports the given block. If the returned block is
     * destroyed, the given block is destroyed too.
     *
     * For blocks that are self-supporting (most blocks in Minecraft), the
     * method returns the block itself.
     *
     * @param block
     *            The block.
     * @return The block the given block is attached on.
     */
    public Block findSupportingBlock(Block block) {
        BlockData data = block.getBlockData();
        if (data instanceof Gate) {
            return block.getRelative(BlockFace.DOWN);
        }
        if (data instanceof Directional) {
            return block.getRelative(((Directional) data).getFacing().getOppositeFace());
        }
        return block.getRelative(BlockFace.DOWN);
    }

    protected BlockFace getChestNeighborFaceOrNull(Block block) {
        BlockData blockData = block.getBlockData();
        if (!(blockData instanceof Chest)) {
            return null;
        }

        Chest chest = (Chest) blockData;
        if (chest.getType() == Type.SINGLE) {
            return null;
        }
        BlockFace towardsLeft = this.turn90Degrees(chest.getFacing());
        return chest.getType() == Type.LEFT ? towardsLeft : towardsLeft.getOppositeFace();
    }

    /**
     * Gets the parser for signs.
     *
     * @return The parser.
     */
    public SignParser getSignParser() {
        return parser;
    }

    /**
     * Checks if the sign at the given position is attached to the container.
     * Doens't check the text on the sign.
     *
     * @param signBlock
     *            The block that is a sign.
     * @param attachedTo
     *            The block the sign must be attached to. If this is not the
     *            case, the method returns false.
     * @return True if the direction and header of the sign are valid, false
     *         otherwise.
     */
    private boolean isAttachedSign(Block signBlock, Block attachedTo) {
        BlockFace requiredFace = signBlock.getFace(attachedTo);
        BlockData materialData = signBlock.getBlockData();
        BlockFace actualFace = BlockFace.DOWN;
        if (materialData instanceof WallSign) {
            actualFace = ((WallSign) materialData).getFacing().getOppositeFace();
        }
        return (actualFace == requiredFace);
    }

    protected BlockFace turn90Degrees(BlockFace face) {
        switch (face) {
            case NORTH:
                return BlockFace.EAST;
            case EAST:
                return BlockFace.SOUTH;
            case SOUTH:
                return BlockFace.WEST;
            case WEST:
                return BlockFace.NORTH;
            default:
                throw new IllegalArgumentException("Cannot handle " + face);
        }
    }

}
src/main/java/nl/rutgerkok/blocklocker/impl/location/TownyLocationChecker.java
public final class TownyLocationChecker implements LocationChecker {

    /**
     * Tests if the Towny plugin is installed.
     *
     * @return True if the factions plugin is installed, false otherwise.
     */
    public static boolean isAvailable() {
        try {
            JavaPlugin.getProvidingPlugin(Towny.class);
            return true;
        } catch (NoClassDefFoundError e) {
            return false;
        }
    }

    @Override
    public void checkLocation(Player player, Block block) throws IllegalLocationException {
        if (TownyAPI.getInstance().isWilderness(block)) {
            throw new IllegalLocationException(Translation.PROTECTION_IN_WILDERNESS);
        }
    }

    @Override
    public boolean keepOnReload() {
        return false; // built-in, so it will be re-added on reload
    }

}
src/main/java/nl/rutgerkok/blocklocker/impl/group/ScoreboardGroupSystem.java
public final class ScoreboardGroupSystem extends GroupSystem {

    @Override
    public boolean isInGroup(Player player, String groupName) {
        Scoreboard mainScoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
        Team team = mainScoreboard.getEntryTeam(player.getName());
        if (team == null) {
            return false;
        }
        return team.getName().equalsIgnoreCase(groupName);
    }

    @Override
    public boolean keepOnReload() {
        // BlockLocker will re-add the group system
        return false;
    }

}
src/main/java/nl/rutgerkok/blocklocker/impl/event/BlockDestroyListener.java
public class BlockDestroyListener extends EventListener {

    public BlockDestroyListener(BlockLockerPluginImpl plugin) {
        super(plugin);
    }

    private Optional<ProtectionSign> asMainSign(Block block) {
        Material material = block.getType();
        if (!Tag.WALL_SIGNS.isTagged(material) && !Tag.STANDING_SIGNS.isTagged(material)) {
            return Optional.empty();
        }

        Optional<ProtectionSign> protectionSign = plugin.getSignParser().parseSign(block);
        if (!protectionSign.isPresent()) {
            return Optional.empty();
        }
        if (!protectionSign.get().getType().isMainSign()) {
            return Optional.empty();
        }
        return protectionSign;
    }

    private void destroyOtherSigns(ProtectionSign protectionSign, Protection protection) {
        for (ProtectionSign foundSign : protection.getSigns()) {
            if (!foundSign.equals(protectionSign)) {
                foundSign.getLocation().getBlock().breakNaturally();
            }
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlockBreak(BlockBreakEvent event) {
        Block block = event.getBlock();
        Optional<Protection> protection = plugin.getProtectionFinder().findProtection(block);
        if (!protection.isPresent()) {
            return;
        }

        Player player = event.getPlayer();
        Profile profile = plugin.getProfileFactory().fromPlayer(player);
        if (!protection.get().isOwner(profile)) {
            if (player.hasPermission(Permissions.CAN_BYPASS)) {
                String ownerName = protection.get().getOwnerDisplayName();
                plugin.getTranslator().sendMessage(player, Translation.PROTECTION_BYPASSED, ownerName);
            } else if (isExpired(protection.get())){
                plugin.getTranslator().sendMessage(player, Translation.PROTECTION_EXPIRED);
            } else {
                event.setCancelled(true);
                return;
            }
        }

        Optional<ProtectionSign> mainSign = asMainSign(block);
        if (mainSign.isPresent()) {
            destroyOtherSigns(mainSign.get(), protection.get());
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlockBurnEvent(BlockBurnEvent event) {
        if (plugin.getChestSettings().allowDestroyBy(AttackType.FIRE)) {
            return;
        }
        if (isProtected(event.getBlock())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlockExplodeEvent(BlockExplodeEvent event) {
        // Generally caused by a Bed, but when the event is triggered the bed is no longer there so we can't check that
        AttackType attackType = AttackType.BLOCK_EXPLOSION;
        if (plugin.getChestSettings().allowDestroyBy(attackType)) {
            return;
        }
        for (Iterator<Block> it = event.blockList().iterator(); it.hasNext();) {
            Block block = it.next();
            if (isProtected(block)) {
                it.remove();
            }
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlockPistonExtend(BlockPistonExtendEvent event) {
        if (plugin.getChestSettings().allowDestroyBy(AttackType.PISTON)) {
            return;
        }
        if (anyProtected(event.getBlocks())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlockPistonRetract(BlockPistonRetractEvent event) {
        if (plugin.getChestSettings().allowDestroyBy(AttackType.PISTON)) {
            return;
        }
        if (event.isSticky() && anyProtected(event.getBlocks())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onEntityChangeBlock(EntityChangeBlockEvent event) {
        AttackType attackType = AttackType.UNKNOWN;
        if (event instanceof EntityBreakDoorEvent) {
            attackType = AttackType.ZOMBIE;
        } else if (event.getEntity() instanceof Enderman) {
            attackType = AttackType.ENDERMAN;
        }
        if (plugin.getChestSettings().allowDestroyBy(attackType)) {
            return;
        }
        if (isProtected(event.getBlock())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onEntityExplodeEvent(EntityExplodeEvent event) {
        AttackType attackType = AttackType.UNKNOWN;
        Entity attacker = event.getEntity();
        if (attacker instanceof TNTPrimed) {
            attackType = AttackType.TNT;
        } else if (attacker instanceof Creeper) {
            attackType = AttackType.CREEPER;
        } else if (attacker instanceof Fireball) {
            if (((Fireball) attacker).getShooter() instanceof Ghast) {
                attackType = AttackType.GHAST;
            }
        }
        if (plugin.getChestSettings().allowDestroyBy(attackType)) {
            return;
        }
        for (Iterator<Block> it = event.blockList().iterator(); it.hasNext();) {
            Block block = it.next();
            if (isProtected(block)) {
                it.remove();
            }
        }
    }

    @EventHandler
    public void onRedstone(BlockRedstoneEvent event) {
        if (event.getNewCurrent() == event.getOldCurrent()) {
            return;
        }

        if (isRedstoneDenied(event.getBlock())) {
            event.setNewCurrent(event.getOldCurrent());
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onStructureGrow(StructureGrowEvent event) {
        if (plugin.getChestSettings().allowDestroyBy(AttackType.SAPLING)) {
            return;
        }
        // Check deleted blocks
        List<BlockState> blocks = event.getBlocks();
        for (Iterator<BlockState> it = blocks.iterator(); it.hasNext();) {
            BlockState blockState = it.next();

            if (blockState.getType() == Material.AIR) {
                // Almost all replaced blocks are air, so this is a cheap,
                // zero-allocation way out
                continue;
            }

            if (isProtected(blockState.getBlock())) {
                it.remove();
            }
        }
    }
}


Question: import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collection;
import java.util.Locale;
import java.util.Optional;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.event.Event;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import nl.rutgerkok.blocklocker.BlockLockerPlugin;
import nl.rutgerkok.blocklocker.ChestSettings;
import nl.rutgerkok.blocklocker.HopperCache;
import nl.rutgerkok.blocklocker.ProfileFactory;
import nl.rutgerkok.blocklocker.ProtectableBlocksSettings;
import nl.rutgerkok.blocklocker.ProtectionFinder;
import nl.rutgerkok.blocklocker.ProtectionUpdater;
import nl.rutgerkok.blocklocker.SignParser;
import nl.rutgerkok.blocklocker.SignSelector;
import nl.rutgerkok.blocklocker.Translator;
import nl.rutgerkok.blocklocker.group.CombinedGroupSystem;
import nl.rutgerkok.blocklocker.group.GroupSystem;
import nl.rutgerkok.blocklocker.impl.blockfinder.BlockFinder;
import nl.rutgerkok.blocklocker.impl.event.BlockDestroyListener;
import nl.rutgerkok.blocklocker.impl.event.BlockLockerCommand;
import nl.rutgerkok.blocklocker.impl.event.BlockPlaceListener;
import nl.rutgerkok.blocklocker.impl.event.InteractListener;
import nl.rutgerkok.blocklocker.impl.event.SignChangeListener;
import nl.rutgerkok.blocklocker.impl.group.FactionsGroupSystem;
import nl.rutgerkok.blocklocker.impl.group.GuildsGroupSystem;
import nl.rutgerkok.blocklocker.impl.group.PermissionsGroupSystem;
import nl.rutgerkok.blocklocker.impl.group.ScoreboardGroupSystem;
import nl.rutgerkok.blocklocker.impl.group.SimpleClansGroupSystem;
import nl.rutgerkok.blocklocker.impl.group.TownyGroupSystem;
import nl.rutgerkok.blocklocker.impl.group.mcMMOGroupSystem;
import nl.rutgerkok.blocklocker.impl.location.TownyLocationChecker;
import nl.rutgerkok.blocklocker.impl.nms.NMSAccessorProvider;
import nl.rutgerkok.blocklocker.impl.nms.ServerSpecific;
import nl.rutgerkok.blocklocker.impl.profile.ProfileFactoryImpl;
import nl.rutgerkok.blocklocker.impl.updater.Updater;
import nl.rutgerkok.blocklocker.location.CombinedLocationChecker;
import nl.rutgerkok.blocklocker.location.LocationChecker;
    }

    @Override
    public SignParser getSignParser() {
        return signParser;
    }

    @Override
    public SignSelector getSignSelector() {
        return signSelector;
    }

    @Override
    public Translator getTranslator() {
        return translator;
    }

    private void loadGroupSystems() {
        this.combinedGroupSystem = new CombinedGroupSystem();
        this.combinedGroupSystem.addSystem(new PermissionsGroupSystem());
        this.combinedGroupSystem.addSystem(new ScoreboardGroupSystem());

        if (FactionsGroupSystem.isAvailable()) {
            this.combinedGroupSystem.addSystem(new FactionsGroupSystem());
        }
        if (TownyGroupSystem.isAvailable()) {
            this.combinedGroupSystem.addSystem(new TownyGroupSystem());
        }
        if (mcMMOGroupSystem.isAvailable()) {
            this.combinedGroupSystem.addSystem(new mcMMOGroupSystem());
        }
        if (GuildsGroupSystem.isAvailable()) {
            this.combinedGroupSystem.addSystem(new GuildsGroupSystem());
        }
        if (SimpleClansGroupSystem.isAvailable()) {
            this.combinedGroupSystem.addSystem(new SimpleClansGroupSystem());
        }
    }

    private void loadLocationCheckers() {
        this.combinedLocationChecker = new CombinedLocationChecker();
        if (TownyLocationChecker.isAvailable()) {
            this.combinedLocationChecker.addChecker(new TownyLocationChecker());
        }
    }

    private void loadServices() {
        // Configuration
        saveDefaultConfig();
        config = new Config(getLogger(), getConfig());

        // Connections with external systems
        loadGroupSystems();
        loadLocationCheckers();

        // Translation
        translator = loadTranslations(config.getLanguageFileName());

        // Parsers and finders
        profileFactory = new ProfileFactoryImpl(combinedGroupSystem, translator);
        chestSettings = new ChestSettingsImpl(translator, config);
        signParser = new SignParserImpl(chestSettings, nms, profileFactory);
        BlockFinder blockFinder = BlockFinder.create(signParser, config.getConnectContainers());
        protectionFinder = new ProtectionFinderImpl(blockFinder, chestSettings);
        protectionUpdater = new ProtectionUpdaterImpl(getServer(), signParser, profileFactory);
        signSelector = new SignSelectorImpl(this);
        redstoneProtectCache = new HopperCacheImpl(this);
    }

    private Translator loadTranslations(String fileName) {
        File file = new File(getDataFolder(), fileName);
        Configuration config = YamlConfiguration.loadConfiguration(file);
        Optional<Configuration> defaultsForLanguage = getJarConfig(fileName);
        if (defaultsForLanguage.isPresent()) {
            config.addDefaults(defaultsForLanguage.get());
        } else {
            Configuration defaultsForEnglish = getJarConfig(Config.DEFAULT_TRANSLATIONS_FILE)
                    .orElseGet(YamlConfiguration::new);
            config.addDefaults(defaultsForEnglish);
        }

        ConfigTranslator translator = new ConfigTranslator(config);
        if (translator.needsSave()) {
            getLogger().info("Saving translations");
            try {
                translator.save(file);
            } catch (IOException e) {
                getLogger().log(Level.SEVERE, "Failed to save translation file", e);
            }
        }
        return translator;
    }

    @Override
    public void onEnable() {
        // NMS checks
        try {
            this.nms = NMSAccessorProvider.create();
        } catch (Throwable t) {
            getLogger()
                    .log(Level.SEVERE, "This Minecraft version is not supported. Find another version of the plugin, if available.", t);
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        loadServices();

        // Events
        registerEvents();

        // Updater
        new Updater(config.getUpdatePreference(), translator, this).startUpdater();
    }

    /**
     * Registers all events of this plugin.
     */
    private void registerEvents() {
        PluginManager plugins = Bukkit.getPluginManager();
        plugins.registerEvents(new BlockDestroyListener(this), this);
Answer:         plugins.registerEvents(new BlockPlaceListener(this), this);