org.pircbotx.UserChannelDao.java Source code

Java tutorial

Introduction

Here is the source code for org.pircbotx.UserChannelDao.java

Source

/**
 * Copyright (C) 2010-2014 Leon Blakey <lord.quackstar at gmail.com>
 *
 * This file is part of PircBotX.
 *
 * PircBotX is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * PircBotX is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * PircBotX. If not, see <http://www.gnu.org/licenses/>.
 */
package org.pircbotx;

import static com.google.common.base.Preconditions.*;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;
import java.io.Closeable;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Synchronized;
import org.apache.commons.lang3.StringUtils;
import org.pircbotx.exception.DaoException;
import org.pircbotx.hooks.events.UserListEvent;
import org.pircbotx.snapshot.ChannelSnapshot;
import org.pircbotx.snapshot.UserChannelDaoSnapshot;
import org.pircbotx.snapshot.UserChannelMapSnapshot;
import org.pircbotx.snapshot.UserSnapshot;

/**
 * Model that creates and tracks Users and Channel and maintains relationships.
 * This includes channel users, channel op/voice/etc users, private messages,
 * etc
 * <p>
 * All methods will throw a {@link NullPointerException} when any argument is
 * null
 *
 * @see User
 * @see Channel
 * @author Leon Blakey
 */
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class UserChannelDao<U extends User, C extends Channel> implements Closeable {
    protected final PircBotX bot;
    protected final Configuration.BotFactory botFactory;
    protected final Locale locale;
    protected final Object accessLock = new Object();
    protected final UserChannelMap<U, C> mainMap;
    protected final EnumMap<UserLevel, UserChannelMap<U, C>> levelsMap;
    protected final Map<String, U> userNickMap;
    protected final Map<String, C> channelNameMap;
    protected final Map<String, U> privateUsers;

    protected UserChannelDao(PircBotX bot, Configuration.BotFactory botFactory) {
        this.bot = bot;
        this.botFactory = botFactory;
        this.locale = bot.getConfiguration().getLocale();
        this.mainMap = new UserChannelMap<U, C>();
        this.userNickMap = Maps.newHashMap();
        this.channelNameMap = Maps.newHashMap();
        this.privateUsers = Maps.newHashMap();

        //Initialize levels map with a UserChannelMap for each level
        this.levelsMap = Maps.newEnumMap(UserLevel.class);
        for (UserLevel level : UserLevel.values())
            levelsMap.put(level, new UserChannelMap<U, C>());
    }

    /**
     * Lookup user by nick, throwing a {@link DaoException} if not found
     *
     * @param nick The nick of the user
     * @return Known active {@link User}
     * @throws DaoException If user does not exist, exception will contain
     * {@link org.pircbotx.exception.DaoException.Reason#UnknownUser} and the
     * nick that doesn't exist
     */
    @Synchronized("accessLock")
    public U getUser(@NonNull String nick) throws DaoException {
        checkArgument(StringUtils.isNotBlank(nick), "Cannot get a blank user");
        U user = userNickMap.get(nick.toLowerCase(locale));
        if (user != null)
            return user;

        //Does not exist
        throw new DaoException(DaoException.Reason.UnknownUser, nick);
    }

    /**
     * Lookup user by UserHostmask, throwing a {@link DaoException} if not found
     *
     * @param userHostmask The hostmask of the user
     * @return Known active {@link User}
     * @throws DaoException If user does not exist, exception will contain
     * {@link org.pircbotx.exception.DaoException.Reason#UnknownUserHostmask},
     * hostmask, and wrapped exception with nick
     */
    @Synchronized("accessLock")
    public U getUser(@NonNull UserHostmask userHostmask) {
        try {
            //Rarely we don't get the full hostmask
            //eg, the server setting your usermode when you connect to the server
            if (userHostmask.getNick() == null)
                return getUser(userHostmask.getHostmask());
            return getUser(userHostmask.getNick());
        } catch (Exception e) {
            //Does not exist, wrap with detail about hostmask
            throw new DaoException(DaoException.Reason.UnknownUserHostmask, userHostmask.toString(), e);
        }
    }

    /**
     * Create a user from a hostmask, internally called when a valid, real user
     * contacts us
     *
     * @param userHostmask The hostmask of the user
     * @return Active {@link User} that was created
     */
    @Synchronized("accessLock")
    @SuppressWarnings("unchecked")
    public U createUser(@NonNull UserHostmask userHostmask) {
        if (containsUser(userHostmask))
            throw new RuntimeException("Cannot create a user from hostmask that already exists: " + userHostmask);
        U user = (U) botFactory.createUser(userHostmask);
        userNickMap.put(userHostmask.getNick().toLowerCase(locale), user);
        return user;
    }

    /**
     * @deprecated Renamed {@link #containsUser(java.lang.String) } to match
     * Java Collections API
     * @see #containsUser(java.lang.String)
     */
    @Synchronized("accessLock")
    @Deprecated
    public boolean userExists(@NonNull String nick) {
        return containsUser(nick);
    }

    /**
     * Check if user exists by nick
     *
     * @param nick Nick of user
     * @return True if user exists
     */
    @Synchronized("accessLock")
    public boolean containsUser(@NonNull String nick) {
        String nickLowercase = nick.toLowerCase(locale);
        return userNickMap.containsKey(nickLowercase) || privateUsers.containsKey(nickLowercase);
    }

    /**
     * Check if user exists by hostmask
     *
     * @param hostmask Hostmask of user
     * @return True if user exists
     */
    @Synchronized("accessLock")
    public boolean containsUser(@NonNull UserHostmask hostmask) {
        return containsUser(hostmask.getNick());
    }

    /**
     * Get all currently known users, except from just joined channels where the
     * WHO response hasn't finished (listen for {@link UserListEvent} instead)
     *
     * @return An immutable set of the currently known users
     * @see UserListEvent
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<U> getAllUsers() {
        return ImmutableSortedSet.copyOf(userNickMap.values());
    }

    @Synchronized("accessLock")
    protected void addUserToChannel(@NonNull U user, @NonNull C channel) {
        mainMap.addUserToChannel(user, channel);
    }

    @Synchronized("accessLock")
    protected void addUserToPrivate(@NonNull U user) {
        String nick = user.getNick().toLowerCase(locale);
        privateUsers.put(nick, user);
    }

    @Synchronized("accessLock")
    protected void addUserToLevel(@NonNull UserLevel level, @NonNull U user, @NonNull C channel) {
        levelsMap.get(level).addUserToChannel(user, channel);
    }

    @Synchronized("accessLock")
    protected void removeUserFromLevel(@NonNull UserLevel level, @NonNull U user, @NonNull C channel) {
        levelsMap.get(level).removeUserFromChannel(user, channel);
    }

    /**
     * Gets all currently known users in a channel who do not hold a UserLevel
     * (op/voice/etc). A {@link UserListEvent} for the channel must of been
     * dispatched before this method will return complete results
     *
     * @param channel Known channel
     * @return An immutable sorted set of Users
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<U> getNormalUsers(@NonNull C channel) {
        Set<U> remainingUsers = new HashSet<U>(mainMap.getUsers(channel));
        for (UserChannelMap<U, C> curLevelMap : levelsMap.values())
            remainingUsers.removeAll(curLevelMap.getUsers(channel));
        return ImmutableSortedSet.copyOf(remainingUsers);
    }

    /**
     * Gets all currently known users in a channel that hold the specified
     * UserLevel. A {@link UserListEvent} for the channel must of been
     * dispatched before this method will return complete results
     *
     * @param channel Known channel
     * @param level Level users must hold
     * @return An immutable sorted set of Users
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<U> getUsers(@NonNull C channel, @NonNull UserLevel level) {
        return levelsMap.get(level).getUsers(channel);
    }

    /**
     * Gets all currently known levels (op/voice/etc) a user holds in the
     * channel. A {@link UserListEvent} for the channel must of been dispatched
     * before this method will return complete results
     *
     * @param channel Known channel
     * @param user Known user
     * @return An immutable sorted set of UserLevels
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<UserLevel> getLevels(@NonNull C channel, @NonNull U user) {
        ImmutableSortedSet.Builder<UserLevel> builder = ImmutableSortedSet.naturalOrder();
        for (Map.Entry<UserLevel, UserChannelMap<U, C>> curEntry : levelsMap.entrySet())
            if (curEntry.getValue().containsEntry(user, channel))
                builder.add(curEntry.getKey());
        return builder.build();
    }

    /**
     * Gets all currently known channels the user is a part of as a normal user.
     * A {@link UserListEvent} for all channels must of been dispatched before
     * this method will return complete results
     *
     * @param user Known user
     * @return An immutable sorted set of Channels
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<C> getNormalUserChannels(@NonNull U user) {
        Set<C> remainingChannels = new HashSet<C>(mainMap.getChannels(user));
        for (UserChannelMap<U, C> curLevelMap : levelsMap.values())
            remainingChannels.removeAll(curLevelMap.getChannels(user));
        return ImmutableSortedSet.copyOf(remainingChannels);
    }

    /**
     * Gets all currently known channels the user is a part of with the
     * specified level. A {@link UserListEvent} for all channels must of been
     * dispatched before this method will return complete results
     *
     * @param user Known user
     * @return An immutable sorted set of Channels
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<C> getChannels(@NonNull U user, @NonNull UserLevel level) {
        return levelsMap.get(level).getChannels(user);
    }

    @Synchronized("accessLock")
    protected void removeUserFromChannel(@NonNull U user, @NonNull C channel) {
        mainMap.removeUserFromChannel(user, channel);
        for (UserChannelMap<U, C> curLevelMap : levelsMap.values())
            curLevelMap.removeUserFromChannel(user, channel);

        if (!privateUsers.values().contains(user) && !mainMap.containsUser(user))
            //Completely remove user
            userNickMap.remove(user.getNick().toLowerCase(locale));
    }

    @Synchronized("accessLock")
    protected void removeUser(@NonNull U user) {
        mainMap.removeUser(user);
        for (UserChannelMap<U, C> curLevelMap : levelsMap.values())
            curLevelMap.removeUser(user);

        //Remove remaining locations
        userNickMap.remove(user.getNick().toLowerCase(locale));
        privateUsers.remove(user.getNick().toLowerCase(locale));
    }

    @Synchronized("accessLock")
    protected boolean levelContainsUser(@NonNull UserLevel level, @NonNull C channel, @NonNull U user) {
        return levelsMap.get(level).containsEntry(user, channel);
    }

    @Synchronized("accessLock")
    protected void renameUser(@NonNull U user, @NonNull String newNick) {
        String oldNick = user.getNick();

        user.setNick(newNick);
        userNickMap.remove(oldNick.toLowerCase(locale));
        userNickMap.put(newNick.toLowerCase(locale), user);
    }

    /**
     * Lookup channel by name, throwing a {@link DaoException} if not found
     *
     * @param name Name of channel (eg #pircbotx)
     * @return A known channel
     */
    @Synchronized("accessLock")
    public C getChannel(@NonNull String name) throws DaoException {
        checkArgument(StringUtils.isNotBlank(name), "Cannot get a blank channel");
        C chan = channelNameMap.get(name.toLowerCase(locale));
        if (chan != null)
            return chan;

        //This could potentially be a mode message, strip off prefixes till we get a channel
        String modePrefixes = bot.getConfiguration().getUserLevelPrefixes();
        if (modePrefixes.contains(Character.toString(name.charAt(0)))) {
            String nameTrimmed = name.toLowerCase(locale);
            do {
                nameTrimmed = nameTrimmed.substring(1);
                chan = channelNameMap.get(nameTrimmed);
                if (chan != null)
                    return chan;
            } while (modePrefixes.contains(Character.toString(nameTrimmed.charAt(0))));
        }

        //Channel does not exist
        throw new DaoException(DaoException.Reason.UnknownChannel, name);
    }

    /**
     * Creates a known channel, internally called when we join a channel
     *
     * @param name
     */
    @Synchronized("accessLock")
    @SuppressWarnings("unchecked")
    public C createChannel(@NonNull String name) {
        C chan = (C) botFactory.createChannel(bot, name);
        channelNameMap.put(name.toLowerCase(locale), chan);
        return chan;
    }

    /**
     * @deprecated Renamed {@link #containsChannel(java.lang.String) } to match
     * the Java Collections API
     * @see #containsChannel(java.lang.String)
     */
    @Deprecated
    public boolean channelExists(@NonNull String name) {
        return containsChannel(name);
    }

    /**
     * Check if we are currently in the given channel
     *
     * @param name Channel name (eg #pircbotx)
     * @return True if we are still connected to the channel
     */
    @Synchronized("accessLock")
    public boolean containsChannel(@NonNull String name) {
        if (channelNameMap.containsKey(name.toLowerCase(locale)))
            return true;

        //This could potentially be a mode message, strip off prefixes till we get a channel
        String modePrefixes = bot.getConfiguration().getUserLevelPrefixes();
        if (modePrefixes.contains(Character.toString(name.charAt(0)))) {
            String nameTrimmed = name.toLowerCase(locale);
            do {
                nameTrimmed = nameTrimmed.substring(1);
                if (channelNameMap.containsKey(nameTrimmed))
                    return true;
            } while (modePrefixes.contains(Character.toString(nameTrimmed.charAt(0))));
        }

        //Nope, doesn't exist
        return false;
    }

    /**
     * Get all currently known users in a channel
     *
     * @param channel Known channel
     * @return An immutable set of users
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<U> getUsers(@NonNull C channel) {
        return mainMap.getUsers(channel);
    }

    /**
     * Get all currently joined channels
     *
     * @return An immutable set of channels
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<C> getAllChannels() {
        return ImmutableSortedSet.copyOf(channelNameMap.values());
    }

    /**
     * Get <i>channels we're joined to</i> that the user is joined to as well
     *
     * @param user A known user
     * @return An immutable set of channels
     */
    @Synchronized("accessLock")
    public ImmutableSortedSet<C> getChannels(@NonNull U user) {
        return mainMap.getChannels(user);
    }

    @Synchronized("accessLock")
    protected void removeChannel(@NonNull C channel) {
        mainMap.removeChannel(channel);
        for (UserChannelMap<U, C> curLevelMap : levelsMap.values())
            curLevelMap.removeChannel(channel);

        //Remove remaining locations
        channelNameMap.remove(channel.getName());
    }

    /**
     * Gets the bots own user object.
     *
     * @return The user object representing this bot
     */
    @Synchronized("accessLock")
    public User getUserBot() {
        return getUser(bot.getNick());
    }

    /**
     * Clears all internal maps
     */
    @Synchronized("accessLock")
    public void close() {
        mainMap.clear();
        for (UserChannelMap<U, C> curLevelMap : levelsMap.values())
            curLevelMap.clear();
        channelNameMap.clear();
        privateUsers.clear();
        userNickMap.clear();
    }

    /**
     * Create an immutable snapshot (copy) of all of contained Users, Channels,
     * and mappings, VERY EXPENSIVE.
     *
     * @return Copy of entire model
     */
    @Synchronized("accessLock")
    public UserChannelDaoSnapshot createSnapshot() {
        //Create snapshots of all users and channels
        Map<U, UserSnapshot> userSnapshotMap = Maps.newHashMapWithExpectedSize(userNickMap.size());
        for (U curUser : userNickMap.values())
            userSnapshotMap.put(curUser, curUser.createSnapshot());
        Map<C, ChannelSnapshot> channelSnapshotMap = Maps.newHashMapWithExpectedSize(channelNameMap.size());
        for (C curChannel : channelNameMap.values())
            channelSnapshotMap.put(curChannel, curChannel.createSnapshot());

        //Make snapshots of the relationship maps using the above user and channel snapshots
        UserChannelMapSnapshot mainMapSnapshot = mainMap.createSnapshot(userSnapshotMap, channelSnapshotMap);
        EnumMap<UserLevel, UserChannelMap<UserSnapshot, ChannelSnapshot>> levelsMapSnapshot = Maps
                .newEnumMap(UserLevel.class);
        for (Map.Entry<UserLevel, UserChannelMap<U, C>> curLevel : levelsMap.entrySet())
            levelsMapSnapshot.put(curLevel.getKey(),
                    curLevel.getValue().createSnapshot(userSnapshotMap, channelSnapshotMap));
        ImmutableBiMap.Builder<String, UserSnapshot> userNickMapSnapshotBuilder = ImmutableBiMap.builder();
        for (Map.Entry<String, U> curNickEntry : userNickMap.entrySet())
            userNickMapSnapshotBuilder.put(curNickEntry.getKey(), userSnapshotMap.get(curNickEntry.getValue()));
        ImmutableBiMap.Builder<String, ChannelSnapshot> channelNameMapSnapshotBuilder = ImmutableBiMap.builder();
        for (Map.Entry<String, C> curName : channelNameMap.entrySet())
            channelNameMapSnapshotBuilder.put(curName.getKey(), channelSnapshotMap.get(curName.getValue()));
        ImmutableBiMap.Builder<String, UserSnapshot> privateUserSnapshotBuilder = ImmutableBiMap.builder();
        for (Map.Entry<String, U> curNickEntry : privateUsers.entrySet())
            privateUserSnapshotBuilder.put(curNickEntry.getKey(), userSnapshotMap.get(curNickEntry.getValue()));

        //Finally can create the snapshot object
        UserChannelDaoSnapshot daoSnapshot = new UserChannelDaoSnapshot(bot, locale, mainMapSnapshot,
                levelsMapSnapshot, userNickMapSnapshotBuilder.build(), channelNameMapSnapshotBuilder.build(),
                privateUserSnapshotBuilder.build());

        //Tell UserSnapshots and ChannelSnapshots what the new backing dao is
        for (UserSnapshot curUserSnapshot : userSnapshotMap.values())
            curUserSnapshot.setDao(daoSnapshot);
        for (ChannelSnapshot curChannelSnapshot : channelSnapshotMap.values())
            curChannelSnapshot.setDao(daoSnapshot);

        //Finally
        return daoSnapshot;
    }
}