Java tutorial
/** * 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; } }