From 253bedcdaccab94452748673efa93aec60478cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Tue, 29 Aug 2023 14:17:38 +0200 Subject: [PATCH 01/15] Add ChannelCacheView --- .../jda/api/entities/channel/ChannelType.java | 38 +- .../jda/api/utils/cache/ChannelCacheView.java | 27 ++ .../net/dv8tion/jda/internal/JDAImpl.java | 70 +--- .../jda/internal/entities/EntityBuilder.java | 72 ++-- .../jda/internal/entities/GuildImpl.java | 53 +-- .../internal/handle/ChannelDeleteHandler.java | 18 +- .../internal/requests/WebSocketClient.java | 9 +- .../dv8tion/jda/internal/utils/Helpers.java | 5 + .../utils/cache/ChannelCacheViewImpl.java | 341 ++++++++++++++++++ 9 files changed, 457 insertions(+), 176 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java create mode 100644 src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/ChannelType.java b/src/main/java/net/dv8tion/jda/api/entities/channel/ChannelType.java index 56d01dbaa8..a3df84dd49 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/ChannelType.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/ChannelType.java @@ -28,63 +28,71 @@ public enum ChannelType /** * A {@link TextChannel TextChannel}, Guild-Only. */ - TEXT(0, 0, true), + TEXT(TextChannel.class, 0, 0, true), /** * A {@link PrivateChannel PrivateChannel}. */ - PRIVATE(1, -1), + PRIVATE(PrivateChannel.class, 1, -1), /** * A {@link VoiceChannel VoiceChannel}, Guild-Only. */ - VOICE(2, 1, true), + VOICE(VoiceChannel.class, 2, 1, true), /** * A Group. (unused) */ - GROUP(3, -1), + GROUP(PrivateChannel.class, 3, -1), /** * A {@link Category Category}, Guild-Only. */ - CATEGORY(4, 2, true), + CATEGORY(Category.class, 4, 2, true), /** * A {@link NewsChannel NewsChannel}, Guild-Only. */ - NEWS(5, 0, true), + NEWS(NewsChannel.class, 5, 0, true), /** * A {@link StageChannel StageChannel}, Guild-Only. */ - STAGE(13, 1, true), + STAGE(StageChannel.class, 13, 1, true), - GUILD_NEWS_THREAD(10, -1, true), - GUILD_PUBLIC_THREAD(11, -1, true), - GUILD_PRIVATE_THREAD(12, -1, true), + GUILD_NEWS_THREAD(ThreadChannel.class, 10, -1, true), + GUILD_PUBLIC_THREAD(ThreadChannel.class, 11, -1, true), + GUILD_PRIVATE_THREAD(ThreadChannel.class, 12, -1, true), /** * A {@link net.dv8tion.jda.api.entities.channel.concrete.ForumChannel ForumChannel}, Guild-Only. */ - FORUM(15, 0, true), + FORUM(ForumChannel.class, 15, 0, true), /** * Unknown Discord channel type. Should never happen and would only possibly happen if Discord implemented a new * channel type and JDA had yet to implement support for it. */ - UNKNOWN(-1, -2); + UNKNOWN(Channel.class, -1, -2); private final int sortBucket; private final int id; private final boolean isGuild; + private final Class clazz; - ChannelType(int id, int sortBucket) + ChannelType(Class clazz, int id, int sortBucket) { - this(id, sortBucket, false); + this(clazz, id, sortBucket, false); } - ChannelType(int id, int sortBucket, boolean isGuild) + ChannelType(Class clazz, int id, int sortBucket, boolean isGuild) { + this.clazz = clazz; this.id = id; this.sortBucket = sortBucket; this.isGuild = isGuild; } + @Nonnull + public Class getInterface() + { + return this.clazz; + } + /** * The sorting bucket for this channel type. * diff --git a/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java new file mode 100644 index 0000000000..ce13ad3d0b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.utils.cache; + +import net.dv8tion.jda.api.entities.channel.Channel; + +import javax.annotation.Nonnull; + +public interface ChannelCacheView extends SnowflakeCacheView +{ + @Nonnull + ChannelCacheView ofType(@Nonnull Class type); +} diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index 4db04cdfdd..1dbf5d1487 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -26,6 +26,7 @@ import net.dv8tion.jda.api.audio.hooks.ConnectionStatus; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.*; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; import net.dv8tion.jda.api.entities.sticker.StickerPack; @@ -75,6 +76,7 @@ import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.*; import net.dv8tion.jda.internal.utils.cache.AbstractCacheView; +import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; import net.dv8tion.jda.internal.utils.config.AuthorizationConfig; import net.dv8tion.jda.internal.utils.config.MetaConfig; @@ -101,15 +103,8 @@ public class JDAImpl implements JDA protected final SnowflakeCacheViewImpl userCache = new SnowflakeCacheViewImpl<>(User.class, User::getName); protected final SnowflakeCacheViewImpl guildCache = new SnowflakeCacheViewImpl<>(Guild.class, Guild::getName); - protected final SnowflakeCacheViewImpl categories = new SnowflakeCacheViewImpl<>(Category.class, Channel::getName); - protected final SnowflakeCacheViewImpl textChannelCache = new SnowflakeCacheViewImpl<>(TextChannel.class, Channel::getName); - protected final SnowflakeCacheViewImpl newsChannelCache = new SnowflakeCacheViewImpl<>(NewsChannel.class, Channel::getName); - protected final SnowflakeCacheViewImpl voiceChannelCache = new SnowflakeCacheViewImpl<>(VoiceChannel.class, Channel::getName); - protected final SnowflakeCacheViewImpl stageChannelCache = new SnowflakeCacheViewImpl<>(StageChannel.class, Channel::getName); - protected final SnowflakeCacheViewImpl threadChannelsCache = new SnowflakeCacheViewImpl<>(ThreadChannel.class, Channel::getName); - protected final SnowflakeCacheViewImpl forumChannelsCache = new SnowflakeCacheViewImpl<>(ForumChannel.class, Channel::getName); - protected final SnowflakeCacheViewImpl privateChannelCache = new SnowflakeCacheViewImpl<>(PrivateChannel.class, Channel::getName); - protected final LinkedList privateChannelLRU = new LinkedList<>(); + protected final ChannelCacheViewImpl channelCache = new ChannelCacheViewImpl<>(Channel.class); + protected final ArrayDeque privateChannelLRU = new ArrayDeque<>(); protected final AbstractCacheView audioManagers = new CacheView.SimpleCacheView<>(AudioManager.class, m -> m.getGuild().getName()); @@ -268,7 +263,7 @@ public void usedPrivateChannel(long id) if (privateChannelLRU.size() > 10) // This could probably be a config option { long removed = privateChannelLRU.removeLast(); - privateChannelCache.remove(removed); + channelCache.remove(ChannelType.PRIVATE, removed); } } } @@ -730,56 +725,56 @@ public SnowflakeCacheView getScheduledEventCache() @Override public SnowflakeCacheView getCategoryCache() { - return categories; + return channelCache.ofType(Category.class); } @Nonnull @Override public SnowflakeCacheView getTextChannelCache() { - return textChannelCache; + return channelCache.ofType(TextChannel.class); } @Nonnull @Override public SnowflakeCacheView getNewsChannelCache() { - return newsChannelCache; + return channelCache.ofType(NewsChannel.class); } @Nonnull @Override public SnowflakeCacheView getVoiceChannelCache() { - return voiceChannelCache; + return channelCache.ofType(VoiceChannel.class); } @Nonnull @Override public SnowflakeCacheView getStageChannelCache() { - return stageChannelCache; + return channelCache.ofType(StageChannel.class); } @Nonnull @Override public SnowflakeCacheView getThreadChannelCache() { - return threadChannelsCache; + return channelCache.ofType(ThreadChannel.class); } @Nonnull @Override public SnowflakeCacheView getForumChannelCache() { - return forumChannelsCache; + return channelCache.ofType(ForumChannel.class); } @Nonnull @Override public SnowflakeCacheView getPrivateChannelCache() { - return privateChannelCache; + return channelCache.ofType(PrivateChannel.class); } @Override @@ -1243,44 +1238,9 @@ public SnowflakeCacheViewImpl getGuildsView() return guildCache; } - public SnowflakeCacheViewImpl getCategoriesView() + public ChannelCacheViewImpl getChannelsView() { - return categories; - } - - public SnowflakeCacheViewImpl getTextChannelsView() - { - return textChannelCache; - } - - public SnowflakeCacheViewImpl getNewsChannelView() - { - return newsChannelCache; - } - - public SnowflakeCacheViewImpl getVoiceChannelsView() - { - return voiceChannelCache; - } - - public SnowflakeCacheViewImpl getStageChannelView() - { - return stageChannelCache; - } - - public SnowflakeCacheViewImpl getThreadChannelsView() - { - return threadChannelsCache; - } - - public SnowflakeCacheViewImpl getForumChannelsView() - { - return forumChannelsCache; - } - - public SnowflakeCacheViewImpl getPrivateChannelsView() - { - return privateChannelCache; + return this.channelCache; } public AbstractCacheView getAudioManagersView() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index 903cf4f006..fe4afa1e17 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -29,6 +29,7 @@ import net.dv8tion.jda.api.entities.Guild.Timeout; import net.dv8tion.jda.api.entities.Guild.VerificationLevel; import net.dv8tion.jda.api.entities.MessageEmbed.*; +import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer; import net.dv8tion.jda.api.entities.channel.attribute.IWebhookContainer; @@ -68,6 +69,7 @@ import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.JDALogger; import net.dv8tion.jda.internal.utils.UnlockHook; +import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; @@ -1076,21 +1078,20 @@ public Category createCategory(GuildImpl guild, DataObject json, long guildId) { boolean playbackCache = false; final long id = json.getLong("id"); - CategoryImpl channel = (CategoryImpl) getJDA().getCategoriesView().get(id); + CategoryImpl channel = (CategoryImpl) getJDA().getChannelsView().getElementById(id); if (channel == null) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildCategoryView = guild.getCategoriesView(), - categoryView = getJDA().getCategoriesView(); + SnowflakeCacheViewImpl guildCategoryView = guild.getCategoriesView(); + ChannelCacheViewImpl categoryView = getJDA().getChannelsView(); try ( UnlockHook glock = guildCategoryView.writeLock(); UnlockHook jlock = categoryView.writeLock()) { channel = new CategoryImpl(id, guild); guildCategoryView.getMap().put(id, channel); - playbackCache = categoryView.getMap().put(id, channel) == null; + playbackCache = categoryView.put(channel) == null; } } @@ -1114,21 +1115,20 @@ public TextChannel createTextChannel(GuildImpl guildObj, DataObject json, long g { boolean playbackCache = false; final long id = json.getLong("id"); - TextChannelImpl channel = (TextChannelImpl) getJDA().getTextChannelsView().get(id); + TextChannelImpl channel = (TextChannelImpl) getJDA().getTextChannelById(id); if (channel == null) { if (guildObj == null) guildObj = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildTextView = guildObj.getTextChannelsView(), - textView = getJDA().getTextChannelsView(); + SnowflakeCacheViewImpl guildTextView = guildObj.getTextChannelsView(); + ChannelCacheViewImpl textView = getJDA().getChannelsView(); try ( UnlockHook glock = guildTextView.writeLock(); UnlockHook jlock = textView.writeLock()) { channel = new TextChannelImpl(id, guildObj); guildTextView.getMap().put(id, channel); - playbackCache = textView.getMap().put(id, channel) == null; + playbackCache = textView.put(channel) == null; } } @@ -1158,21 +1158,20 @@ public NewsChannel createNewsChannel(GuildImpl guildObj, DataObject json, long g { boolean playbackCache = false; final long id = json.getLong("id"); - NewsChannelImpl channel = (NewsChannelImpl) getJDA().getNewsChannelView().get(id); + NewsChannelImpl channel = (NewsChannelImpl) getJDA().getNewsChannelById(id); if (channel == null) { if (guildObj == null) guildObj = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildNewsView = guildObj.getNewsChannelView(), - newsView = getJDA().getNewsChannelView(); + SnowflakeCacheViewImpl guildNewsView = guildObj.getNewsChannelView(); + ChannelCacheViewImpl newsView = getJDA().getChannelsView(); try ( UnlockHook glock = guildNewsView.writeLock(); UnlockHook jlock = newsView.writeLock()) { channel = new NewsChannelImpl(id, guildObj); guildNewsView.getMap().put(id, channel); - playbackCache = newsView.getMap().put(id, channel) == null; + playbackCache = newsView.put(channel) == null; } } @@ -1199,21 +1198,20 @@ public VoiceChannel createVoiceChannel(GuildImpl guild, DataObject json, long gu { boolean playbackCache = false; final long id = json.getLong("id"); - VoiceChannelImpl channel = ((VoiceChannelImpl) getJDA().getVoiceChannelsView().get(id)); + VoiceChannelImpl channel = (VoiceChannelImpl) getJDA().getVoiceChannelById(id); if (channel == null) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildVoiceView = guild.getVoiceChannelsView(), - voiceView = getJDA().getVoiceChannelsView(); + SnowflakeCacheViewImpl guildVoiceView = guild.getVoiceChannelsView(); + ChannelCacheViewImpl voiceView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildVoiceView.writeLock(); UnlockHook jlock = voiceView.writeLock()) { channel = new VoiceChannelImpl(id, guild); guildVoiceView.getMap().put(id, channel); - playbackCache = voiceView.getMap().put(id, channel) == null; + playbackCache = voiceView.put(channel) == null; } } @@ -1244,21 +1242,20 @@ public StageChannel createStageChannel(GuildImpl guild, DataObject json, long gu { boolean playbackCache = false; final long id = json.getLong("id"); - StageChannelImpl channel = ((StageChannelImpl) getJDA().getStageChannelView().get(id)); + StageChannelImpl channel = (StageChannelImpl) getJDA().getStageChannelById(id); if (channel == null) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildStageView = guild.getStageChannelsView(), - stageView = getJDA().getStageChannelView(); + SnowflakeCacheViewImpl guildStageView = guild.getStageChannelsView(); + ChannelCacheViewImpl stageView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildStageView.writeLock(); UnlockHook jlock = stageView.writeLock()) { channel = new StageChannelImpl(id, guild); guildStageView.getMap().put(id, channel); - playbackCache = stageView.getMap().put(id, channel) == null; + playbackCache = stageView.put(channel) == null; } } @@ -1299,19 +1296,18 @@ public ThreadChannel createThreadChannel(GuildImpl guild, DataObject json, long if (parent == null) throw new IllegalArgumentException(MISSING_CHANNEL); - ThreadChannelImpl channel = ((ThreadChannelImpl) getJDA().getThreadChannelsView().get(id)); + ThreadChannelImpl channel = ((ThreadChannelImpl) getJDA().getThreadChannelById(id)); if (channel == null) { - SnowflakeCacheViewImpl - guildThreadView = guild.getThreadChannelsView(), - threadView = getJDA().getThreadChannelsView(); + SnowflakeCacheViewImpl guildThreadView = guild.getThreadChannelsView(); + ChannelCacheViewImpl threadView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildThreadView.writeLock(); UnlockHook jlock = threadView.writeLock()) { channel = new ThreadChannelImpl(id, guild, type); guildThreadView.getMap().put(id, channel); - playbackCache = threadView.getMap().put(id, channel) == null; + playbackCache = threadView.put(channel) == null; } } @@ -1383,21 +1379,20 @@ public ForumChannel createForumChannel(GuildImpl guild, DataObject json, long gu { boolean playbackCache = false; final long id = json.getLong("id"); - ForumChannelImpl channel = (ForumChannelImpl) getJDA().getForumChannelsView().get(id); + ForumChannelImpl channel = (ForumChannelImpl) getJDA().getForumChannelById(id); if (channel == null) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildView = guild.getForumChannelsView(), - globalView = getJDA().getForumChannelsView(); + SnowflakeCacheViewImpl guildView = guild.getForumChannelsView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildView.writeLock(); UnlockHook jlock = globalView.writeLock()) { channel = new ForumChannelImpl(id, guild); guildView.getMap().put(id, channel); - playbackCache = globalView.getMap().put(id, channel) == null; + playbackCache = globalView.put(channel) == null; } } @@ -1497,11 +1492,8 @@ public PrivateChannel createPrivateChannel(DataObject json, UserImpl user) private void cachePrivateChannel(PrivateChannelImpl priv) { - SnowflakeCacheViewImpl privateView = getJDA().getPrivateChannelsView(); - try (UnlockHook hook = privateView.writeLock()) - { - privateView.getMap().put(priv.getIdLong(), priv); - } + ChannelCacheViewImpl privateView = getJDA().getChannelsView(); + privateView.put(priv); api.usedPrivateChannel(priv.getIdLong()); getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, priv.getIdLong()); } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index d8e386a4a1..31d7dcb78c 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -76,10 +76,7 @@ import net.dv8tion.jda.internal.utils.EntityString; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.UnlockHook; -import net.dv8tion.jda.internal.utils.cache.AbstractCacheView; -import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; +import net.dv8tion.jda.internal.utils.cache.*; import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; import okhttp3.MediaType; import okhttp3.MultipartBody; @@ -157,51 +154,11 @@ public void invalidate() { //Remove everything from global cache // this prevents some race-conditions for getting audio managers from guilds - SnowflakeCacheViewImpl guildView = getJDA().getGuildsView(); - SnowflakeCacheViewImpl stageView = getJDA().getStageChannelView(); - SnowflakeCacheViewImpl textView = getJDA().getTextChannelsView(); - SnowflakeCacheViewImpl threadView = getJDA().getThreadChannelsView(); - SnowflakeCacheViewImpl newsView = getJDA().getNewsChannelView(); - SnowflakeCacheViewImpl forumView = getJDA().getForumChannelsView(); - SnowflakeCacheViewImpl voiceView = getJDA().getVoiceChannelsView(); - SnowflakeCacheViewImpl categoryView = getJDA().getCategoriesView(); - - guildView.remove(id); - - try (UnlockHook hook = stageView.writeLock()) - { - getStageChannelCache() - .forEachUnordered(chan -> stageView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = textView.writeLock()) - { - getTextChannelCache() - .forEachUnordered(chan -> textView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = threadView.writeLock()) - { - getThreadChannelsView() - .forEachUnordered(chan -> threadView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = newsView.writeLock()) - { - getNewsChannelCache() - .forEachUnordered(chan -> newsView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = forumView.writeLock()) - { - getForumChannelCache() - .forEachUnordered(chan -> forumView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = voiceView.writeLock()) - { - getVoiceChannelCache() - .forEachUnordered(chan -> voiceView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = categoryView.writeLock()) + + ChannelCacheViewImpl channelsView = getJDA().getChannelsView(); + try (UnlockHook hook = channelsView.writeLock()) { - getCategoryCache() - .forEachUnordered(chan -> categoryView.getMap().remove(chan.getIdLong())); + getChannels().forEach(channel -> channelsView.remove(channel.getType(), channel.getIdLong())); } // Clear audio connection diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java index 2ef2ccdc6f..63f3900f04 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java @@ -16,7 +16,6 @@ package net.dv8tion.jda.internal.handle; -import net.dv8tion.jda.api.entities.ScheduledEvent; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.*; import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent; @@ -24,7 +23,6 @@ import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.GuildImpl; import net.dv8tion.jda.internal.requests.WebSocketClient; -import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; public class ChannelDeleteHandler extends SocketHandler { @@ -53,7 +51,7 @@ protected Long handleInternally(DataObject content) { case TEXT: { - TextChannel channel = getJDA().getTextChannelsView().remove(channelId); + TextChannel channel = getJDA().getChannelsView().remove(ChannelType.TEXT, channelId); if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a text channel that is not yet cached. JSON: {}", content); @@ -69,7 +67,7 @@ protected Long handleInternally(DataObject content) } case NEWS: { - NewsChannel channel = getJDA().getNewsChannelView().remove(channelId); + NewsChannel channel = getJDA().getChannelsView().remove(ChannelType.NEWS, channelId); if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a news channel that is not yet cached. JSON: {}", content); @@ -85,7 +83,7 @@ protected Long handleInternally(DataObject content) } case VOICE: { - VoiceChannel channel = getJDA().getVoiceChannelsView().remove(channelId); + VoiceChannel channel = getJDA().getChannelsView().remove(ChannelType.VOICE, channelId); if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a voice channel that is not yet cached. JSON: {}", content); @@ -109,7 +107,7 @@ protected Long handleInternally(DataObject content) } case STAGE: { - StageChannel channel = getJDA().getStageChannelView().remove(channelId); + StageChannel channel = getJDA().getChannelsView().remove(ChannelType.STAGE, channelId); if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a stage channel that is not yet cached. JSON: {}", content); @@ -126,7 +124,8 @@ protected Long handleInternally(DataObject content) case CATEGORY: { - Category category = getJDA().getCategoriesView().remove(channelId); + Category category = getJDA().getChannelsView().remove(ChannelType.CATEGORY, channelId); +// Category category = getJDA().getCategoriesView().remove(channelId); if (category == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a category channel that is not yet cached. JSON: {}", content); @@ -142,7 +141,7 @@ protected Long handleInternally(DataObject content) } case FORUM: { - ForumChannel channel = getJDA().getForumChannelsView().remove(channelId); + ForumChannel channel = getJDA().getChannelsView().remove(ChannelType.FORUM, channelId); if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a forum channel that is not yet cached. JSON: {}", content); @@ -158,8 +157,7 @@ protected Long handleInternally(DataObject content) } case PRIVATE: { - SnowflakeCacheViewImpl privateView = getJDA().getPrivateChannelsView(); - PrivateChannel channel = privateView.remove(channelId); + PrivateChannel channel = getJDA().getChannelsView().remove(ChannelType.PRIVATE, channelId); if (channel == null) { diff --git a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java index 1467d81cba..8c23acc42a 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -801,14 +801,7 @@ protected void invalidate() locked("Interrupted while trying to invalidate chunk/sync queue", chunkSyncQueue::clear); - api.getTextChannelsView().clear(); - api.getVoiceChannelsView().clear(); - api.getCategoriesView().clear(); - api.getNewsChannelView().clear(); - api.getPrivateChannelsView().clear(); - api.getStageChannelView().clear(); - api.getThreadChannelsView().clear(); - api.getForumChannelsView().clear(); + api.getChannelsView().clear(); api.getGuildsView().clear(); api.getUsersView().clear(); diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java index 84fa4ab311..dd35790c2f 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -233,6 +233,11 @@ public static String[] split(String input, String match) return out.toArray(new String[0]); } + public static boolean equals(String a, String b, boolean ignoreCase) + { + return ignoreCase ? a == b || (a != null && b != null && a.equalsIgnoreCase(b)) : Objects.equals(a, b); + } + // ## CollectionUtils ## public static boolean deepEquals(Collection first, Collection second) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java new file mode 100644 index 0000000000..708fcb6d0a --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -0,0 +1,341 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.utils.cache; + +import gnu.trove.map.TLongObjectMap; +import gnu.trove.map.hash.TLongObjectHashMap; +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.utils.ClosableIterator; +import net.dv8tion.jda.api.utils.LockIterator; +import net.dv8tion.jda.api.utils.MiscUtil; +import net.dv8tion.jda.api.utils.cache.ChannelCacheView; +import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.Helpers; +import net.dv8tion.jda.internal.utils.UnlockHook; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ChannelCacheViewImpl extends ReadWriteLockCache implements ChannelCacheView +{ + private final EnumMap> caches = new EnumMap<>(ChannelType.class); + + public ChannelCacheViewImpl(Class type) + { + for (ChannelType channelType : ChannelType.values()) + { + Class clazz = channelType.getInterface(); + if (channelType != ChannelType.UNKNOWN && type.isAssignableFrom(clazz)) + caches.put(channelType, new TLongObjectHashMap<>()); + } + } + + @SuppressWarnings("unchecked") + public TLongObjectMap getMap(@Nonnull ChannelType type) + { + if (!lock.writeLock().isHeldByCurrentThread()) + throw new IllegalStateException("Cannot access map directly without holding write lock!"); + return (TLongObjectMap) caches.get(type); + } + + @Nullable + @SuppressWarnings("unchecked") + public C put(C element) + { + try (UnlockHook hook = writeLock()) + { + return (C) caches.get(element.getType()).put(element.getIdLong(), element); + } + } + + @Nullable + @SuppressWarnings("unchecked") + public C remove(ChannelType type, long id) + { + try (UnlockHook hook = writeLock()) + { + T removed = caches.get(type).remove(id); + return (C) removed; + } + } + + public void clear() + { + try (UnlockHook hook = writeLock()) + { + caches.values().forEach(TLongObjectMap::clear); + } + } + + @Nonnull + @Override + public ChannelCacheView ofType(@Nonnull Class type) + { + return new FilteredCacheView<>(type); + } + + @Nonnull + @Override + public List asList() + { + return applyStream(stream -> stream.collect(Helpers.toUnmodifiableList())); + } + + @Nonnull + @Override + public Set asSet() + { + return applyStream(stream -> stream.collect( + Collectors.collectingAndThen( + Collectors.toSet(), + Collections::unmodifiableSet + ))); + } + + @Nonnull + @Override + public ClosableIterator lockedIterator() + { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + MiscUtil.tryLock(readLock); + try + { + Iterator directIterator = caches.values().stream().flatMap(map -> map.valueCollection().stream()).iterator(); + return new LockIterator<>(directIterator, readLock); + } + catch (Throwable t) + { + readLock.unlock(); + throw t; + } + } + + @Override + public long size() + { + try (UnlockHook hook = readLock()) + { + return caches.values().stream().mapToLong(TLongObjectMap::size).sum(); + } + } + + @Override + public boolean isEmpty() + { + try (UnlockHook hook = readLock()) + { + return caches.values().stream().allMatch(TLongObjectMap::isEmpty); + } + } + + @Nonnull + @Override + public List getElementsByName(@Nonnull String name, boolean ignoreCase) + { + Checks.notEmpty(name, "Name"); + return applyStream(stream -> + stream + .filter((channel) -> Helpers.equals(channel.getName(), name, ignoreCase)) + .collect(Collectors.toList()) + ); + } + + @Nonnull + @Override + public Stream stream() + { + return this.asList().stream(); + } + + @Nonnull + @Override + public Stream parallelStream() + { + return this.asList().parallelStream(); + } + + @Nullable + @Override + public T getElementById(long id) + { + try (UnlockHook hook = readLock()) + { + for (TLongObjectMap cache : caches.values()) + { + T element = cache.get(id); + if (element != null) + return element; + } + return null; + } + } + + @Nonnull + @Override + public Iterator iterator() + { + return stream().iterator(); + } + + private final class FilteredCacheView implements ChannelCacheView + { + private final Class type; + private final ChannelType concreteType; + + private FilteredCacheView(Class type) + { + Checks.notNull(type, "Channel Type"); + this.type = type; + ChannelType concrete = null; + for (ChannelType channelType : ChannelType.values()) + { + if (channelType != ChannelType.UNKNOWN && type.equals(channelType.getInterface())) + { + concrete = channelType; + break; + } + } + this.concreteType = concrete; + } + + @Nonnull + @Override + public List asList() + { + return applyStream(stream -> stream.collect(Helpers.toUnmodifiableList())); + } + + @Nonnull + @Override + public Set asSet() + { + return applyStream(stream -> + stream.collect( + Collectors.collectingAndThen( + Collectors.toSet(), + Collections::unmodifiableSet)) + ); + } + + @Nonnull + @Override + @SuppressWarnings("unchecked") + public ClosableIterator lockedIterator() + { + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + MiscUtil.tryLock(readLock); + try + { + Iterator directIterator = concreteType != null + ? (Iterator) caches.get(concreteType).valueCollection().iterator() + : caches.entrySet() + .stream() + .filter((entry) -> type.isAssignableFrom(entry.getKey().getInterface())) + .map(Map.Entry::getValue) + .flatMap(map -> map.valueCollection().stream()) + .filter(type::isInstance) + .map(type::cast) + .iterator(); + return new LockIterator<>(directIterator, readLock); + } + catch (Throwable t) + { + readLock.unlock(); + throw t; + } + } + + @Override + public long size() + { + try (UnlockHook hook = readLock()) + { + return concreteType != null ? caches.get(concreteType).size() : applyStream(Stream::count); + } + } + + @Override + public boolean isEmpty() + { + try (UnlockHook hook = readLock()) + { + return concreteType != null ? caches.get(concreteType).isEmpty() : applyStream(stream -> !stream.findAny().isPresent()); + } + } + + @Nonnull + @Override + public List getElementsByName(@Nonnull String name, boolean ignoreCase) + { + Checks.notEmpty(name, "Name"); + return applyStream(stream -> + stream + .filter(channel -> Helpers.equals(channel.getName(), name, ignoreCase)) + .collect(Collectors.toList()) + ); + } + + @Nonnull + @Override + public Stream stream() + { + return asList().stream(); + } + + @Nonnull + @Override + public Stream parallelStream() + { + return asList().parallelStream(); + } + + @Nonnull + @Override + public ChannelCacheView ofType(@Nonnull Class type) + { + return ChannelCacheViewImpl.this.ofType(type); + } + + @Nullable + @Override + public C getElementById(long id) + { + try (UnlockHook hook = readLock()) + { + if (concreteType != null) + return type.cast(caches.get(concreteType).get(id)); + + T element = ChannelCacheViewImpl.this.getElementById(id); + if (type.isInstance(element)) + return type.cast(element); + return null; + } + } + + @Nonnull + @Override + public Iterator iterator() + { + return asList().iterator(); + } + } +} From c6d0d36114cc2bebbaefa17fe39c7e5b4afd9f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Tue, 29 Aug 2023 14:26:10 +0200 Subject: [PATCH 02/15] Fix some errors --- .../jda/internal/handle/ChannelUpdateHandler.java | 13 +++++++------ .../jda/internal/handle/ThreadDeleteHandler.java | 6 +++++- .../jda/internal/handle/ThreadUpdateHandler.java | 7 ++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java index 842cc20218..89be728fba 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.IPermissionHolder; import net.dv8tion.jda.api.entities.PermissionOverride; +import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelFlag; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer; @@ -53,6 +54,7 @@ import net.dv8tion.jda.internal.entities.channel.mixin.middleman.AudioChannelMixin; import net.dv8tion.jda.internal.requests.WebSocketClient; import net.dv8tion.jda.internal.utils.UnlockHook; +import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; @@ -220,7 +222,7 @@ private AbstractGuildChannelImpl handleChannelTypeChange(AbstractGuildChannel { //This assumes that if we're moving to a TextChannel that we're transitioning from a NewsChannel NewsChannel newsChannel = (NewsChannel) channel; - getJDA().getNewsChannelView().remove(newsChannel.getIdLong()); + getJDA().getChannelsView().remove(ChannelType.NEWS, newsChannel.getIdLong()); guild.getNewsChannelView().remove(newsChannel.getIdLong()); TextChannelImpl textChannel = (TextChannelImpl) builder.createTextChannel(guild, content, guild.getIdLong()); @@ -240,7 +242,7 @@ private AbstractGuildChannelImpl handleChannelTypeChange(AbstractGuildChannel { //This assumes that if we're moving to a NewsChannel that we're transitioning from a TextChannel TextChannel textChannel = (TextChannel) channel; - getJDA().getTextChannelsView().remove(textChannel.getIdLong()); + getJDA().getChannelsView().remove(ChannelType.TEXT, textChannel.getIdLong()); guild.getTextChannelsView().remove(textChannel.getIdLong()); NewsChannelImpl newsChannel = (NewsChannelImpl) builder.createNewsChannel(guild, content, guild.getIdLong()); @@ -367,16 +369,15 @@ private void handleHideChildThreads(IThreadContainer channel) for (ThreadChannel thread : threads) { GuildImpl guild = (GuildImpl) channel.getGuild(); - SnowflakeCacheViewImpl - guildThreadView = guild.getThreadChannelsView(), - threadView = getJDA().getThreadChannelsView(); + SnowflakeCacheViewImpl guildThreadView = guild.getThreadChannelsView(); + ChannelCacheViewImpl threadView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildThreadView.writeLock(); UnlockHook jlock = threadView.writeLock()) { //TODO-threads: When we figure out how member chunking is going to work for thread related members // we may need to revisit this to ensure they kicked out of the cache if needed. - threadView.getMap().remove(thread.getIdLong()); + threadView.remove(thread.getType(), thread.getIdLong()); guildThreadView.getMap().remove(thread.getIdLong()); } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java index 57a83b034f..47cd3c637f 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java @@ -16,12 +16,14 @@ package net.dv8tion.jda.internal.handle; +import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.GuildImpl; import net.dv8tion.jda.internal.requests.WebSocketClient; +import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; public class ThreadDeleteHandler extends SocketHandler { @@ -40,13 +42,15 @@ protected Long handleInternally(DataObject content) GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); final long threadId = content.getLong("id"); - ThreadChannel thread = getJDA().getThreadChannelsView().remove(threadId); + ChannelCacheViewImpl channelsView = getJDA().getChannelsView(); + ThreadChannel thread = (ThreadChannel) channelsView.getElementById(threadId); if (thread == null || guild == null) { WebSocketClient.LOG.debug("THREAD_DELETE attempted to delete a thread that is not yet cached. JSON: {}", content); return null; } + channelsView.remove(thread.getType(), threadId); guild.getThreadChannelsView().remove(threadId); getJDA().handleEvent( diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java index fb25f5e74c..7fedbc0d3e 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java @@ -17,6 +17,7 @@ package net.dv8tion.jda.internal.handle; import gnu.trove.set.TLongSet; +import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelFlag; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.events.channel.update.*; @@ -27,7 +28,7 @@ import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.entities.channel.concrete.ThreadChannelImpl; import net.dv8tion.jda.internal.utils.Helpers; -import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; +import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; import java.util.List; @@ -191,9 +192,9 @@ protected Long handleInternally(DataObject content) if (thread.isArchived()) { SortedSnowflakeCacheViewImpl guildView = thread.getGuild().getThreadChannelsView(); - SnowflakeCacheViewImpl globalView = api.getThreadChannelsView(); + ChannelCacheViewImpl globalView = api.getChannelsView(); guildView.remove(threadId); - globalView.remove(threadId); + globalView.remove(thread.getType(), threadId); } return null; From b46c71bd38ef0295255282bb93f65cf6528422f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Tue, 29 Aug 2023 14:42:30 +0200 Subject: [PATCH 03/15] Unify thread cache maps --- .../utils/cache/ChannelCacheViewImpl.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index 708fcb6d0a..380d88d5bd 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -43,18 +43,23 @@ public ChannelCacheViewImpl(Class type) { for (ChannelType channelType : ChannelType.values()) { + channelType = normalizeKey(channelType); Class clazz = channelType.getInterface(); if (channelType != ChannelType.UNKNOWN && type.isAssignableFrom(clazz)) caches.put(channelType, new TLongObjectHashMap<>()); } } + // Store all threads under the same channel type, makes it easier because the interface is shared + private ChannelType normalizeKey(ChannelType type) + { + return type.isThread() ? ChannelType.GUILD_PUBLIC_THREAD : type; + } + @SuppressWarnings("unchecked") - public TLongObjectMap getMap(@Nonnull ChannelType type) + private TLongObjectMap getMap(@Nonnull ChannelType type) { - if (!lock.writeLock().isHeldByCurrentThread()) - throw new IllegalStateException("Cannot access map directly without holding write lock!"); - return (TLongObjectMap) caches.get(type); + return (TLongObjectMap) caches.get(normalizeKey(type)); } @Nullable @@ -63,7 +68,7 @@ public C put(C element) { try (UnlockHook hook = writeLock()) { - return (C) caches.get(element.getType()).put(element.getIdLong(), element); + return (C) getMap(element.getType()).put(element.getIdLong(), element); } } @@ -73,7 +78,7 @@ public C remove(ChannelType type, long id) { try (UnlockHook hook = writeLock()) { - T removed = caches.get(type).remove(id); + T removed = getMap(type).remove(id); return (C) removed; } } @@ -208,6 +213,7 @@ private FilteredCacheView(Class type) ChannelType concrete = null; for (ChannelType channelType : ChannelType.values()) { + channelType = normalizeKey(channelType); if (channelType != ChannelType.UNKNOWN && type.equals(channelType.getInterface())) { concrete = channelType; @@ -246,7 +252,7 @@ public ClosableIterator lockedIterator() try { Iterator directIterator = concreteType != null - ? (Iterator) caches.get(concreteType).valueCollection().iterator() + ? (Iterator) getMap(concreteType).valueCollection().iterator() : caches.entrySet() .stream() .filter((entry) -> type.isAssignableFrom(entry.getKey().getInterface())) @@ -269,7 +275,7 @@ public long size() { try (UnlockHook hook = readLock()) { - return concreteType != null ? caches.get(concreteType).size() : applyStream(Stream::count); + return concreteType != null ? getMap(concreteType).size() : applyStream(Stream::count); } } @@ -278,7 +284,7 @@ public boolean isEmpty() { try (UnlockHook hook = readLock()) { - return concreteType != null ? caches.get(concreteType).isEmpty() : applyStream(stream -> !stream.findAny().isPresent()); + return concreteType != null ? getMap(concreteType).isEmpty() : applyStream(stream -> !stream.findAny().isPresent()); } } @@ -322,7 +328,7 @@ public C getElementById(long id) try (UnlockHook hook = readLock()) { if (concreteType != null) - return type.cast(caches.get(concreteType).get(id)); + return type.cast(getMap(concreteType).get(id)); T element = ChannelCacheViewImpl.this.getElementById(id); if (type.isInstance(element)) From 87f94f471c82cd66850d547c4ed960dd8eae3ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 2 Sep 2023 11:44:21 +0200 Subject: [PATCH 04/15] Add sorted cache view and use it in guild --- .../utils/cache/SortedChannelCacheView.java | 27 +++ .../jda/internal/entities/GuildImpl.java | 10 +- .../utils/cache/ChannelCacheViewImpl.java | 43 ++-- .../cache/SortedChannelCacheViewImpl.java | 193 ++++++++++++++++++ 4 files changed, 251 insertions(+), 22 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java create mode 100644 src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java diff --git a/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java b/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java new file mode 100644 index 0000000000..03295d8c77 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.api.utils.cache; + +import net.dv8tion.jda.api.entities.channel.Channel; + +import javax.annotation.Nonnull; + +public interface SortedChannelCacheView> extends ChannelCacheView, SortedSnowflakeCacheView +{ + @Nonnull + SortedChannelCacheView ofType(@Nonnull Class type); +} diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index 31d7dcb78c..1939dab7a4 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -99,14 +99,8 @@ public class GuildImpl implements Guild private final long id; private final JDAImpl api; - private final SortedSnowflakeCacheViewImpl categoryCache = new SortedSnowflakeCacheViewImpl<>(Category.class, Channel::getName, Comparator.naturalOrder()); private final SortedSnowflakeCacheViewImpl scheduledEventCache = new SortedSnowflakeCacheViewImpl<>(ScheduledEvent.class, ScheduledEvent::getName, Comparator.naturalOrder()); - private final SortedSnowflakeCacheViewImpl voiceChannelCache = new SortedSnowflakeCacheViewImpl<>(VoiceChannel.class, Channel::getName, Comparator.naturalOrder()); - private final SortedSnowflakeCacheViewImpl textChannelCache = new SortedSnowflakeCacheViewImpl<>(TextChannel.class, Channel::getName, Comparator.naturalOrder()); - private final SortedSnowflakeCacheViewImpl newsChannelCache = new SortedSnowflakeCacheViewImpl<>(NewsChannel.class, Channel::getName, Comparator.naturalOrder()); - private final SortedSnowflakeCacheViewImpl stageChannelCache = new SortedSnowflakeCacheViewImpl<>(StageChannel.class, Channel::getName, Comparator.naturalOrder()); - private final SortedSnowflakeCacheViewImpl threadChannelCache = new SortedSnowflakeCacheViewImpl<>(ThreadChannel.class, Channel::getName, Comparator.naturalOrder()); - private final SortedSnowflakeCacheViewImpl forumChannelCache = new SortedSnowflakeCacheViewImpl<>(ForumChannel.class, Channel::getName, Comparator.naturalOrder()); + private final SortedChannelCacheViewImpl channelCache = new SortedChannelCacheViewImpl<>(GuildChannel.class); private final SortedSnowflakeCacheViewImpl roleCache = new SortedSnowflakeCacheViewImpl<>(Role.class, Role::getName, Comparator.reverseOrder()); private final SnowflakeCacheViewImpl emojicache = new SnowflakeCacheViewImpl<>(RichCustomEmoji.class, RichCustomEmoji::getName); private final SnowflakeCacheViewImpl stickerCache = new SnowflakeCacheViewImpl<>(GuildSticker.class, GuildSticker::getName); @@ -698,7 +692,7 @@ public SortedSnowflakeCacheView getScheduledEventCache() @Override public SortedSnowflakeCacheView getCategoryCache() { - return categoryCache; + return channelCache.ofType(Category.class); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index 380d88d5bd..440d0103fa 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -32,12 +32,13 @@ import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; -public class ChannelCacheViewImpl extends ReadWriteLockCache implements ChannelCacheView +public class ChannelCacheViewImpl extends ReadWriteLockCache implements ChannelCacheView { - private final EnumMap> caches = new EnumMap<>(ChannelType.class); + protected final EnumMap> caches = new EnumMap<>(ChannelType.class); public ChannelCacheViewImpl(Class type) { @@ -51,13 +52,13 @@ public ChannelCacheViewImpl(Class type) } // Store all threads under the same channel type, makes it easier because the interface is shared - private ChannelType normalizeKey(ChannelType type) + protected ChannelType normalizeKey(ChannelType type) { return type.isThread() ? ChannelType.GUILD_PUBLIC_THREAD : type; } @SuppressWarnings("unchecked") - private TLongObjectMap getMap(@Nonnull ChannelType type) + protected TLongObjectMap getMap(@Nonnull ChannelType type) { return (TLongObjectMap) caches.get(normalizeKey(type)); } @@ -98,22 +99,36 @@ public ChannelCacheView ofType(@Nonnull Class type) return new FilteredCacheView<>(type); } + @Override + public void forEach(Consumer action) + { + try (UnlockHook hook = readLock()) + { + for (TLongObjectMap cache : caches.values()) + { + cache.valueCollection().forEach(action); + } + } + } + @Nonnull @Override public List asList() { - return applyStream(stream -> stream.collect(Helpers.toUnmodifiableList())); + List list = getCachedList(); + if (list == null) + list = cache((List) applyStream(stream -> stream.collect(Collectors.toList()))); + return list; } @Nonnull @Override public Set asSet() { - return applyStream(stream -> stream.collect( - Collectors.collectingAndThen( - Collectors.toSet(), - Collections::unmodifiableSet - ))); + Set set = getCachedSet(); + if (set == null) + set = cache((Set) applyStream(stream -> stream.collect(Collectors.toSet()))); + return set; } @Nonnull @@ -201,12 +216,12 @@ public Iterator iterator() return stream().iterator(); } - private final class FilteredCacheView implements ChannelCacheView + protected class FilteredCacheView implements ChannelCacheView { - private final Class type; - private final ChannelType concreteType; + protected final Class type; + protected final ChannelType concreteType; - private FilteredCacheView(Class type) + protected FilteredCacheView(Class type) { Checks.notNull(type, "Channel Type"); this.type = type; diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java new file mode 100644 index 0000000000..a4771c1249 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java @@ -0,0 +1,193 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.utils.cache; + +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.utils.cache.SortedChannelCacheView; +import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.Helpers; +import net.dv8tion.jda.internal.utils.UnlockHook; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SortedChannelCacheViewImpl> extends ChannelCacheViewImpl implements SortedChannelCacheView +{ + public SortedChannelCacheViewImpl(Class type) + { + super(type); + } + + @Nonnull + @Override + public SortedChannelCacheView ofType(@Nonnull Class type) + { + return new SortedFilteredCacheView<>(type); + } + + @Nonnull + @Override + public List asList() + { + List list = getCachedList(); + if (list == null) + list = cache(new ArrayList<>(asSet())); + return list; + } + + @Nonnull + @Override + public NavigableSet asSet() + { + NavigableSet set = (NavigableSet) getCachedSet(); + if (set == null) + set = cache((NavigableSet) applyStream(stream -> stream.collect(Collectors.toCollection(TreeSet::new)))); + return set; + } + + @Override + public void forEachUnordered(@Nonnull Consumer action) + { + super.forEach(action); + } + + @Override + public void forEach(@Nonnull Consumer action) + { + asSet().forEach(action); + } + + @Nonnull + @Override + public List getElementsByName(@Nonnull String name) + { + List elements = super.getElementsByName(name); + elements.sort(Comparator.naturalOrder()); + return elements; + } + + @Nonnull + @Override + public Stream streamUnordered() + { + try (UnlockHook hook = readLock()) + { + return caches.values().stream().flatMap(cache -> cache.valueCollection().stream()).collect(Collectors.toList()).stream(); + } + } + + @Nonnull + @Override + public Stream parallelStreamUnordered() + { + return streamUnordered().parallel(); + } + + @Override + public Spliterator spliterator() + { + return asSet().spliterator(); + } + + @Nonnull + @Override + public Iterator iterator() + { + return asSet().iterator(); + } + + private class SortedFilteredCacheView extends FilteredCacheView implements SortedChannelCacheView + { + protected SortedFilteredCacheView(Class type) + { + super(type); + } + + @Nonnull + @Override + public List asList() + { + return applyStream(stream -> + stream + .sorted() + .collect(Helpers.toUnmodifiableList()) + ); + } + + @Nonnull + @Override + public NavigableSet asSet() + { + return applyStream(stream -> + stream.collect( + Collectors.collectingAndThen( + Collectors.toCollection(TreeSet::new), + Collections::unmodifiableNavigableSet)) + ); + } + + @Nonnull + @Override + public List getElementsByName(@Nonnull String name, boolean ignoreCase) + { + Checks.notEmpty(name, "Name"); + return applyStream(stream -> + stream + .filter(it -> Helpers.equals(name, it.getName(), ignoreCase)) + .sorted() + .collect(Collectors.toList()) + ); + } + + @Nonnull + @Override + public Stream streamUnordered() + { + List elements = applyStream(stream -> stream.filter(type::isInstance).collect(Collectors.toList())); + return elements.stream(); + } + + @Nonnull + @Override + public Stream parallelStreamUnordered() + { + return stream().parallel(); + } + + @Nonnull + @Override + public SortedChannelCacheView ofType(@Nonnull Class type) + { + return SortedChannelCacheViewImpl.this.ofType(type); + } + + @Override + public void forEachUnordered(@Nonnull Consumer action) + { + super.forEach(action); + } + + @Override + public void forEach(Consumer action) + { + stream().forEach(action); + } + } +} From d5a4e8781e5ba48995ea782f82e8585f7e98731b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 2 Dec 2023 20:58:43 +0100 Subject: [PATCH 05/15] Improve guild implementation and fix merge issues --- .../net/dv8tion/jda/internal/JDAImpl.java | 2 +- .../jda/internal/entities/EntityBuilder.java | 83 +++++---- .../jda/internal/entities/GuildImpl.java | 166 +++++------------- .../channel/concrete/NewsChannelImpl.java | 7 - .../channel/concrete/StageChannelImpl.java | 7 - .../channel/concrete/TextChannelImpl.java | 7 - .../channel/concrete/VoiceChannelImpl.java | 8 - .../AbstractStandardGuildChannelImpl.java | 5 +- .../internal/handle/ChannelDeleteHandler.java | 16 +- .../internal/handle/ChannelUpdateHandler.java | 6 +- .../handle/GuildMemberRemoveHandler.java | 2 +- .../internal/handle/GuildUpdateHandler.java | 8 +- .../internal/handle/ThreadDeleteHandler.java | 2 +- .../internal/handle/ThreadUpdateHandler.java | 8 +- .../utils/cache/ChannelCacheViewImpl.java | 17 ++ 15 files changed, 124 insertions(+), 220 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index 19d9deb86d..1265ce8f6f 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -775,7 +775,7 @@ public SnowflakeCacheView getForumChannelCache() @Override public SnowflakeCacheView getMediaChannelCache() { - return mediaChannelsCache; + return channelCache.ofType(MediaChannel.class); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index 7c8cf9ca43..a5b948da1c 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -1081,15 +1081,15 @@ public Category createCategory(GuildImpl guild, DataObject json, long guildId) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl guildCategoryView = guild.getCategoriesView(); - ChannelCacheViewImpl categoryView = getJDA().getChannelsView(); + ChannelCacheViewImpl guildView = guild.getChannelView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook glock = guildCategoryView.writeLock(); - UnlockHook jlock = categoryView.writeLock()) + UnlockHook glock = guildView.writeLock(); + UnlockHook jlock = globalView.writeLock()) { channel = new CategoryImpl(id, guild); - guildCategoryView.getMap().put(id, channel); - playbackCache = categoryView.put(channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1118,15 +1118,15 @@ public TextChannel createTextChannel(GuildImpl guildObj, DataObject json, long g { if (guildObj == null) guildObj = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl guildTextView = guildObj.getTextChannelsView(); - ChannelCacheViewImpl textView = getJDA().getChannelsView(); + ChannelCacheViewImpl guildView = guildObj.getChannelView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook glock = guildTextView.writeLock(); - UnlockHook jlock = textView.writeLock()) + UnlockHook glock = guildView.writeLock(); + UnlockHook jlock = globalView.writeLock()) { channel = new TextChannelImpl(id, guildObj); - guildTextView.getMap().put(id, channel); - playbackCache = textView.put(channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1161,15 +1161,15 @@ public NewsChannel createNewsChannel(GuildImpl guildObj, DataObject json, long g { if (guildObj == null) guildObj = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl guildNewsView = guildObj.getNewsChannelView(); - ChannelCacheViewImpl newsView = getJDA().getChannelsView(); + ChannelCacheViewImpl guildView = guildObj.getChannelView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook glock = guildNewsView.writeLock(); - UnlockHook jlock = newsView.writeLock()) + UnlockHook glock = guildView.writeLock(); + UnlockHook jlock = globalView.writeLock()) { channel = new NewsChannelImpl(id, guildObj); - guildNewsView.getMap().put(id, channel); - playbackCache = newsView.put(channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1201,15 +1201,15 @@ public VoiceChannel createVoiceChannel(GuildImpl guild, DataObject json, long gu { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl guildVoiceView = guild.getVoiceChannelsView(); - ChannelCacheViewImpl voiceView = getJDA().getChannelsView(); + ChannelCacheViewImpl guildView = guild.getChannelView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook vlock = guildVoiceView.writeLock(); - UnlockHook jlock = voiceView.writeLock()) + UnlockHook glock = guildView.writeLock(); + UnlockHook jlock = globalView.writeLock()) { channel = new VoiceChannelImpl(id, guild); - guildVoiceView.getMap().put(id, channel); - playbackCache = voiceView.put(channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1245,15 +1245,15 @@ public StageChannel createStageChannel(GuildImpl guild, DataObject json, long gu { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl guildStageView = guild.getStageChannelsView(); - ChannelCacheViewImpl stageView = getJDA().getChannelsView(); + ChannelCacheViewImpl guildView = guild.getChannelView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook vlock = guildStageView.writeLock(); - UnlockHook jlock = stageView.writeLock()) + UnlockHook glock = guildView.writeLock(); + UnlockHook jlock = globalView.writeLock()) { channel = new StageChannelImpl(id, guild); - guildStageView.getMap().put(id, channel); - playbackCache = stageView.put(channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1302,7 +1302,7 @@ public ThreadChannel createThreadChannel(GuildImpl guild, DataObject json, long ThreadChannelImpl channel = ((ThreadChannelImpl) getJDA().getThreadChannelById(id)); if (channel == null) { - SnowflakeCacheViewImpl guildThreadView = guild.getThreadChannelsView(); + ChannelCacheViewImpl guildThreadView = guild.getChannelView(); ChannelCacheViewImpl threadView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildThreadView.writeLock(); @@ -1311,7 +1311,7 @@ public ThreadChannel createThreadChannel(GuildImpl guild, DataObject json, long channel = new ThreadChannelImpl(id, guild, type); if (modifyCache) { - guildThreadView.getMap().put(id, channel); + guildThreadView.put(channel); playbackCache = threadView.put(channel) == null; } } @@ -1390,14 +1390,14 @@ public ForumChannel createForumChannel(GuildImpl guild, DataObject json, long gu { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl guildView = guild.getForumChannelsView(); + ChannelCacheViewImpl guildView = guild.getChannelView(); ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook vlock = guildView.writeLock(); + UnlockHook glock = guildView.writeLock(); UnlockHook jlock = globalView.writeLock()) { channel = new ForumChannelImpl(id, guild); - guildView.getMap().put(id, channel); + guildView.put(channel); playbackCache = globalView.put(channel) == null; } } @@ -1437,21 +1437,20 @@ public MediaChannel createMediaChannel(GuildImpl guild, DataObject json, long gu { boolean playbackCache = false; final long id = json.getLong("id"); - MediaChannelImpl channel = (MediaChannelImpl) getJDA().getMediaChannelsView().get(id); + MediaChannelImpl channel = (MediaChannelImpl) getJDA().getChannelsView().getElementById(id); if (channel == null) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildView = guild.getMediaChannelsView(), - globalView = getJDA().getMediaChannelsView(); + ChannelCacheViewImpl guildView = guild.getChannelView(); + ChannelCacheViewImpl globalView = getJDA().getChannelsView(); try ( - UnlockHook vlock = guildView.writeLock(); + UnlockHook glock = guildView.writeLock(); UnlockHook jlock = globalView.writeLock()) { channel = new MediaChannelImpl(id, guild); - guildView.getMap().put(id, channel); - playbackCache = globalView.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index f36337e2c1..d90978d965 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.api.entities.automod.build.AutoModRuleData; import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer; import net.dv8tion.jda.api.entities.channel.concrete.*; import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; @@ -189,71 +190,22 @@ public void invalidate() public void uncacheChannel(GuildChannel channel, boolean keepThreads) { long id = channel.getIdLong(); - switch (channel.getType()) - { - case TEXT: - api.getTextChannelsView().remove(id); - this.getTextChannelsView().remove(id); - break; - case NEWS: - api.getNewsChannelView().remove(id); - this.getNewsChannelView().remove(id); - break; - case MEDIA: - api.getMediaChannelsView().remove(id); - this.getMediaChannelsView().remove(id); - break; - case FORUM: - api.getForumChannelsView().remove(id); - this.getForumChannelsView().remove(id); - break; - case VOICE: - api.getVoiceChannelsView().remove(id); - this.getVoiceChannelsView().remove(id); - break; - case STAGE: - api.getStageChannelView().remove(id); - this.getStageChannelsView().remove(id); - break; - case CATEGORY: - api.getCategoriesView().remove(id); - this.getCategoriesView().remove(id); - break; - case GUILD_NEWS_THREAD: - case GUILD_PUBLIC_THREAD: - case GUILD_PRIVATE_THREAD: - api.getThreadChannelsView().remove(id); - this.getThreadChannelsView().remove(id); - break; - } + if (channelCache.remove(channel.getType(), id) != null) + api.getChannelsView().remove(channel.getType(), id); if (!keepThreads && channel instanceof IThreadContainer) { // Remove dangling threads - SortedSnowflakeCacheViewImpl localView = this.getThreadChannelsView(); - SnowflakeCacheViewImpl globalView = api.getThreadChannelsView(); + SortedChannelCacheViewImpl localView = this.getChannelView(); + ChannelCacheViewImpl globalView = api.getChannelsView(); Predicate predicate = thread -> channel.equals(thread.getParentChannel()); try (UnlockHook hook1 = localView.writeLock(); UnlockHook hook2 = globalView.writeLock()) { - localView.getMap().valueCollection().removeIf(predicate); - globalView.getMap().valueCollection().removeIf(predicate); + localView.removeIf(ThreadChannel.class, predicate); + globalView.removeIf(ThreadChannel.class, predicate); } } - - // This might be too presumptuous, Channel#getParent still returns null regardless if the category is uncached -// if (channel instanceof Category) -// { -// for (Channel chan : guild.getChannels()) -// { -// if (!(chan instanceof ICategorizableChannelMixin)) -// continue; -// -// ICategorizableChannelMixin categoizable = (ICategorizableChannelMixin) chan; -// if (categoizable.getParentCategoryIdLong() == id) -// categoizable.setParentCategory(0L); -// } -// } } @Nonnull @@ -770,49 +722,49 @@ public SortedSnowflakeCacheView getCategoryCache() @Override public SortedSnowflakeCacheView getTextChannelCache() { - return textChannelCache; + return channelCache.ofType(TextChannel.class); } @Nonnull @Override public SortedSnowflakeCacheView getNewsChannelCache() { - return newsChannelCache; + return channelCache.ofType(NewsChannel.class); } @Nonnull @Override public SortedSnowflakeCacheView getVoiceChannelCache() { - return voiceChannelCache; + return channelCache.ofType(VoiceChannel.class); } @Nonnull @Override public SortedSnowflakeCacheView getForumChannelCache() { - return forumChannelCache; + return channelCache.ofType(ForumChannel.class); } @Nonnull @Override public SnowflakeCacheView getMediaChannelCache() { - return mediaChannelCache; + return channelCache.ofType(MediaChannel.class); } @Nonnull @Override public SortedSnowflakeCacheView getStageChannelCache() { - return stageChannelCache; + return channelCache.ofType(StageChannel.class); } @Nonnull @Override public SortedSnowflakeCacheView getThreadChannelCache() { - return threadChannelCache; + return channelCache.ofType(ThreadChannel.class); } @Nonnull @@ -840,38 +792,35 @@ public SnowflakeCacheView getStickerCache() @Override public List getChannels(boolean includeHidden) { + if (includeHidden) + return channelCache.applyStream(stream -> stream.filter(it -> !it.getType().isThread()).sorted().collect(Helpers.toUnmodifiableList())); + Member self = getSelfMember(); - Predicate filterHidden = includeHidden ? (it) -> true : it -> self.hasPermission(it, Permission.VIEW_CHANNEL); - - SnowflakeCacheViewImpl categories = getCategoriesView(); - SnowflakeCacheViewImpl voice = getVoiceChannelsView(); - SnowflakeCacheViewImpl stage = getStageChannelsView(); - SnowflakeCacheViewImpl text = getTextChannelsView(); - SnowflakeCacheViewImpl news = getNewsChannelView(); - SnowflakeCacheViewImpl forum = getForumChannelsView(); - SnowflakeCacheViewImpl media = getMediaChannelsView(); - - List channels = new ArrayList<>((int) (categories.size() + voice.size() + stage.size() + text.size() + news.size() + forum.size() + media.size())); - - voice.acceptStream(stream -> stream.filter(filterHidden).forEach(channels::add)); - stage.acceptStream(stream -> stream.filter(filterHidden).forEach(channels::add)); - text.acceptStream(stream -> stream.filter(filterHidden).forEach(channels::add)); - news.acceptStream(stream -> stream.filter(filterHidden).forEach(channels::add)); - forum.acceptStream(stream -> stream.filter(filterHidden).forEach(channels::add)); - media.acceptStream(stream -> stream.filter(filterHidden).forEach(channels::add)); - - categories.forEach(category -> - { - if (!includeHidden && category.getChannels().stream().noneMatch(filterHidden)) - return; - channels.add(category); + Map> grouped = new HashMap<>(); + channelCache.forEachUnordered(channel -> { + if (channel.getType().isThread() || !self.hasPermission(channel, Permission.VIEW_CHANNEL)) return; + + if (channel instanceof ICategorizableChannel) + { + Category category = ((ICategorizableChannel) channel).getParentCategory(); + grouped.computeIfAbsent(category, (k) -> new HashSet<>()).add(channel); + grouped.computeIfAbsent(null, (k) -> new HashSet<>()).add(category); + } + else + { + grouped.computeIfAbsent(null, (k) -> new HashSet<>()).add(channel); + } }); - // See AbstractGuildChannelImpl#compareTo for details on how this achieves the canonical order of the client - Collections.sort(channels); + for (Map.Entry> entry : grouped.entrySet()) + { + if (entry.getKey() == null) continue; + if (entry.getValue().isEmpty()) + grouped.get(null).remove(entry.getKey()); + } - return Collections.unmodifiableList(channels); + return grouped.values().stream().flatMap(Set::stream).sorted().collect(Helpers.toUnmodifiableList()); } @Nonnull @@ -2253,44 +2202,9 @@ public SortedSnowflakeCacheViewImpl getScheduledEventsView() return scheduledEventCache; } - public SortedSnowflakeCacheViewImpl getCategoriesView() - { - return categoryCache; - } - - public SortedSnowflakeCacheViewImpl getTextChannelsView() - { - return textChannelCache; - } - - public SortedSnowflakeCacheViewImpl getNewsChannelView() - { - return newsChannelCache; - } - - public SortedSnowflakeCacheViewImpl getVoiceChannelsView() - { - return voiceChannelCache; - } - - public SortedSnowflakeCacheViewImpl getStageChannelsView() - { - return stageChannelCache; - } - - public SortedSnowflakeCacheViewImpl getThreadChannelsView() - { - return threadChannelCache; - } - - public SortedSnowflakeCacheViewImpl getForumChannelsView() - { - return forumChannelCache; - } - - public SortedSnowflakeCacheViewImpl getMediaChannelsView() + public SortedChannelCacheViewImpl getChannelView() { - return mediaChannelCache; + return channelCache; } public SortedSnowflakeCacheViewImpl getRolesView() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/NewsChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/NewsChannelImpl.java index c42b1d124c..a771c09ee0 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/NewsChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/NewsChannelImpl.java @@ -108,11 +108,4 @@ public NewsChannelManager getManager() { return new NewsChannelManagerImpl(this); } - - // -- Abstract hooks -- - @Override - protected void onPositionChange() - { - getGuild().getNewsChannelView().clearCachedLists(); - } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/StageChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/StageChannelImpl.java index 8c2ba3c6ac..4c93acb7bb 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/StageChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/StageChannelImpl.java @@ -280,11 +280,4 @@ public StageChannelImpl setLatestMessageIdLong(long latestMessageId) this.latestMessageId = latestMessageId; return this; } - - // -- Abstract Hooks -- - @Override - protected void onPositionChange() - { - getGuild().getStageChannelsView().clearCachedLists(); - } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/TextChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/TextChannelImpl.java index 867cc4bbc2..5f9b00cebf 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/TextChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/TextChannelImpl.java @@ -105,11 +105,4 @@ public TextChannelImpl setSlowmode(int slowmode) this.slowmode = slowmode; return this; } - - // -- Abstract hooks -- - @Override - protected void onPositionChange() - { - getGuild().getTextChannelsView().clearCachedLists(); - } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/VoiceChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/VoiceChannelImpl.java index a9f3148599..1f36c741e4 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/VoiceChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/concrete/VoiceChannelImpl.java @@ -209,12 +209,4 @@ public VoiceChannelImpl setLatestMessageIdLong(long latestMessageId) this.latestMessageId = latestMessageId; return this; } - - // -- Abstract Hooks -- - - @Override - protected void onPositionChange() - { - getGuild().getVoiceChannelsView().clearCachedLists(); - } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractStandardGuildChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractStandardGuildChannelImpl.java index 6552b8c38b..3f28c46714 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractStandardGuildChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractStandardGuildChannelImpl.java @@ -70,5 +70,8 @@ public T setPosition(int position) return (T) this; } - protected abstract void onPositionChange(); + protected final void onPositionChange() + { + guild.getChannelView().clearCachedLists(); + } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java index 46392deeb3..2d061b62b1 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java @@ -58,7 +58,7 @@ protected Long handleInternally(DataObject content) return null; } - guild.getTextChannelsView().remove(channel.getIdLong()); + guild.getChannelView().remove(channel); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, @@ -74,7 +74,7 @@ protected Long handleInternally(DataObject content) return null; } - guild.getNewsChannelView().remove(channel.getIdLong()); + guild.getChannelView().remove(channel); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, @@ -98,7 +98,7 @@ protected Long handleInternally(DataObject content) // { // manager.closeAudioConnection(ConnectionStatus.DISCONNECTED_CHANNEL_DELETED); // } - guild.getVoiceChannelsView().remove(channel.getIdLong()); + guild.getChannelView().remove(channel); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, @@ -114,7 +114,7 @@ protected Long handleInternally(DataObject content) return null; } - guild.getStageChannelsView().remove(channel.getIdLong()); + guild.getChannelView().remove(channel); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, @@ -132,7 +132,7 @@ protected Long handleInternally(DataObject content) return null; } - guild.getCategoriesView().remove(channelId); + guild.getChannelView().remove(category); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, @@ -148,7 +148,7 @@ protected Long handleInternally(DataObject content) return null; } - guild.getForumChannelsView().remove(channel.getIdLong()); + guild.getChannelView().remove(channel); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, @@ -157,14 +157,14 @@ protected Long handleInternally(DataObject content) } case MEDIA: { - MediaChannel channel = getJDA().getMediaChannelsView().remove(channelId); + MediaChannel channel = getJDA().getChannelsView().remove(ChannelType.MEDIA, channelId); if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a media channel that is not yet cached. JSON: {}", content); return null; } - guild.getMediaChannelsView().remove(channel.getIdLong()); + guild.getChannelView().remove(channel); getJDA().handleEvent( new ChannelDeleteEvent( getJDA(), responseNumber, diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java index 9f35e2bbd5..d18b3b7475 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java @@ -34,6 +34,7 @@ import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.forums.ForumTag; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.emoji.EmojiUnion; import net.dv8tion.jda.api.events.channel.forum.ForumTagAddEvent; @@ -62,7 +63,6 @@ import net.dv8tion.jda.internal.requests.WebSocketClient; import net.dv8tion.jda.internal.utils.UnlockHook; import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; import java.util.ArrayList; @@ -351,7 +351,7 @@ private void handleHideChildThreads(IThreadContainer channel) for (ThreadChannel thread : threads) { GuildImpl guild = (GuildImpl) channel.getGuild(); - SnowflakeCacheViewImpl guildThreadView = guild.getThreadChannelsView(); + ChannelCacheViewImpl guildThreadView = guild.getChannelView(); ChannelCacheViewImpl threadView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildThreadView.writeLock(); @@ -360,7 +360,7 @@ private void handleHideChildThreads(IThreadContainer channel) //TODO-threads: When we figure out how member chunking is going to work for thread related members // we may need to revisit this to ensure they kicked out of the cache if needed. threadView.remove(thread.getType(), thread.getIdLong()); - guildThreadView.getMap().remove(thread.getIdLong()); + guildThreadView.remove(thread); } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java index 2c09c8a181..f1b78ad7a7 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java @@ -75,7 +75,7 @@ protected Long handleInternally(DataObject content) { // WebSocketClient.LOG.debug("Received GUILD_MEMBER_REMOVE for a Member that does not exist in the specified Guild. UserId: {} GuildId: {}", userId, id); // Remove user from voice channel if applicable - guild.getVoiceChannelsView().forEachUnordered((channel) -> { + guild.getVoiceChannelCache().forEachUnordered((channel) -> { VoiceChannelImpl impl = (VoiceChannelImpl) channel; Member connected = impl.getConnectedMembersMap().remove(userId); if (connected != null) // user left channel! diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java index 15bedb0e28..e6a2810cfe 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java @@ -82,13 +82,13 @@ protected Long handleInternally(DataObject content) Guild.Timeout afkTimeout = Guild.Timeout.fromKey(content.getInt("afk_timeout")); DiscordLocale locale = DiscordLocale.from(content.getString("preferred_locale", "en-US")); VoiceChannel afkChannel = content.isNull("afk_channel_id") - ? null : guild.getVoiceChannelsView().get(content.getLong("afk_channel_id")); + ? null : guild.getChannelById(VoiceChannel.class, content.getLong("afk_channel_id")); TextChannel systemChannel = content.isNull("system_channel_id") - ? null : guild.getTextChannelsView().get(content.getLong("system_channel_id")); + ? null : guild.getChannelById(TextChannel.class, content.getLong("system_channel_id")); TextChannel rulesChannel = content.isNull("rules_channel_id") - ? null : guild.getTextChannelsView().get(content.getLong("rules_channel_id")); + ? null : guild.getChannelById(TextChannel.class, content.getLong("rules_channel_id")); TextChannel communityUpdatesChannel = content.isNull("public_updates_channel_id") - ? null : guild.getTextChannelsView().get(content.getLong("public_updates_channel_id")); + ? null : guild.getChannelById(TextChannel.class, content.getLong("public_updates_channel_id")); Set features; if (!content.isNull("features")) { diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java index 47cd3c637f..c61eacc79a 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java @@ -51,7 +51,7 @@ protected Long handleInternally(DataObject content) } channelsView.remove(thread.getType(), threadId); - guild.getThreadChannelsView().remove(threadId); + guild.getChannelView().remove(thread); getJDA().handleEvent( new ChannelDeleteEvent( diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java index ffdff938c6..6f2ab353a7 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java @@ -20,6 +20,7 @@ import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelFlag; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.events.channel.update.*; import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.data.DataArray; @@ -29,7 +30,6 @@ import net.dv8tion.jda.internal.entities.channel.concrete.ThreadChannelImpl; import net.dv8tion.jda.internal.utils.Helpers; import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; import java.util.List; import java.util.Objects; @@ -195,10 +195,10 @@ protected Long handleInternally(DataObject content) if (thread.isArchived()) { - SortedSnowflakeCacheViewImpl guildView = thread.getGuild().getThreadChannelsView(); + ChannelCacheViewImpl guildView = thread.getGuild().getChannelView(); ChannelCacheViewImpl globalView = api.getChannelsView(); - guildView.remove(threadId); - globalView.remove(thread.getType(), threadId); + guildView.remove(thread); + globalView.remove(thread); } return null; diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index 440d0103fa..14ddf126e7 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -33,6 +33,7 @@ import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -84,6 +85,22 @@ public C remove(ChannelType type, long id) } } + public C remove(C channel) + { + return remove(channel.getType(), channel.getIdLong()); + } + + public void removeIf(Class typeFilter, Predicate predicate) + { + try (UnlockHook hook = writeLock()) + { + for (TLongObjectMap map : caches.values()) + { + map.valueCollection().removeIf(c -> typeFilter.isInstance(c) && predicate.test(typeFilter.cast(c))); + } + } + } + public void clear() { try (UnlockHook hook = writeLock()) From 7732e1ddf4767fb0bedc8e8bd3eae45dca72d6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 3 Dec 2023 11:15:21 +0100 Subject: [PATCH 06/15] Add getPositionInCategory --- .../net/dv8tion/jda/api/entities/Guild.java | 20 ++++++++++--- .../attribute/ICategorizableChannel.java | 14 +++++++++- .../attribute/IPositionableChannel.java | 2 -- .../entities/channel/concrete/Category.java | 28 ++++++++----------- .../ICategorizableChannelManager.java | 2 +- .../jda/api/utils/cache/ChannelCacheView.java | 23 +++++++++++++++ .../utils/cache/SortedChannelCacheView.java | 13 +++++++++ .../jda/internal/entities/GuildImpl.java | 7 +++++ .../utils/cache/ChannelCacheViewImpl.java | 2 +- 9 files changed, 85 insertions(+), 26 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index 10af96368b..fb98d77b19 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -59,10 +59,7 @@ import net.dv8tion.jda.api.utils.FileUpload; import net.dv8tion.jda.api.utils.ImageProxy; import net.dv8tion.jda.api.utils.MiscUtil; -import net.dv8tion.jda.api.utils.cache.CacheFlag; -import net.dv8tion.jda.api.utils.cache.MemberCacheView; -import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; -import net.dv8tion.jda.api.utils.cache.SortedSnowflakeCacheView; +import net.dv8tion.jda.api.utils.cache.*; import net.dv8tion.jda.api.utils.concurrent.Task; import net.dv8tion.jda.internal.interactions.CommandDataImpl; import net.dv8tion.jda.internal.requests.DeferredRestAction; @@ -1561,6 +1558,21 @@ default List getScheduledEvents() @Override SortedSnowflakeCacheView getForumChannelCache(); + /** + * {@link SortedChannelCacheView SortedChannelCacheView} of {@link GuildChannel}. + * + *

Provides cache access to all channels of this guild, including thread channels (unlike {@link #getChannels()}). + * The cache view attempts to provide a sorted list, based on how channels are displayed in the client. + * Various methods like {@link SortedChannelCacheView#forEachUnordered(Consumer)} or {@link SortedChannelCacheView#lockedIterator()} + * bypass sorting for optimization reasons. + * + *

It is possible to filter the channels to more specific types using {@link SortedChannelCacheView#ofType(Class)}. + * + * @return {@link SortedChannelCacheView SortedChannelCacheView} + */ + @Nonnull + SortedChannelCacheView getGuildChannelCache(); + /** * Populated list of {@link GuildChannel channels} for this guild. *
This includes all types of channels, except for threads. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/ICategorizableChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/ICategorizableChannel.java index 80e42151b6..d890949a78 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/ICategorizableChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/ICategorizableChannel.java @@ -31,12 +31,24 @@ * @see Category * @see net.dv8tion.jda.api.entities.Guild#getCategories() */ -public interface ICategorizableChannel extends GuildChannel, IPermissionContainer +public interface ICategorizableChannel extends GuildChannel, IPermissionContainer, IPositionableChannel { @Override @Nonnull ICategorizableChannelManager getManager(); + /** + * Computes the relative position of this channel in the {@link #getParentCategory() parent category}. + *
This is effectively the same as {@code getParentCategory().getChannels().indexOf(channel)}. + * + * @return The relative position in the parent category, or {@code -1} if no parent is set + */ + default int getPositionInCategory() + { + Category parent = getParentCategory(); + return parent == null ? -1 : parent.getChannels().indexOf(this); + } + /** * Get the snowflake of the {@link Category} that contains this channel. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IPositionableChannel.java b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IPositionableChannel.java index 03d50a898b..e7d5936f65 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IPositionableChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IPositionableChannel.java @@ -34,8 +34,6 @@ public interface IPositionableChannel extends GuildChannel @Nonnull IPositionableChannelManager getManager(); - //TODO-v5: We should probably introduce getPositionInCategory (name pending) that returns index in Category#getChannels or -1 - /** * The position of this channel in the channel list of the guild. *
This does not account for thread channels, as they do not have positions. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java b/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java index 5ff2369865..2db5bb8163 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java @@ -21,20 +21,17 @@ import net.dv8tion.jda.api.entities.IPermissionHolder; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.Channel; -import net.dv8tion.jda.api.entities.channel.attribute.ICopyableChannel; -import net.dv8tion.jda.api.entities.channel.attribute.IMemberContainer; -import net.dv8tion.jda.api.entities.channel.attribute.IPermissionContainer; -import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.*; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.managers.channel.concrete.CategoryManager; import net.dv8tion.jda.api.requests.restaction.ChannelAction; import net.dv8tion.jda.api.requests.restaction.order.CategoryOrderAction; import net.dv8tion.jda.api.requests.restaction.order.ChannelOrderAction; import net.dv8tion.jda.api.requests.restaction.order.OrderAction; +import net.dv8tion.jda.internal.utils.Helpers; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -43,8 +40,6 @@ * Represents a channel category in the official Discord API. *
Categories are used to keep order in a Guild by dividing the channels into groups. * - * @since 3.4.0 - * * @see Guild#getCategoryCache() * @see Guild#getCategories() * @see Guild#getCategoriesByName(String, boolean) @@ -66,16 +61,15 @@ public interface Category extends GuildChannel, ICopyableChannel, IPositionableC @Nonnull default List getChannels() { - List channels = new ArrayList<>(); - channels.addAll(getTextChannels()); - channels.addAll(getVoiceChannels()); - channels.addAll(getStageChannels()); - channels.addAll(getNewsChannels()); - channels.addAll(getForumChannels()); - channels.addAll(getMediaChannels()); - Collections.sort(channels); - - return Collections.unmodifiableList(channels); + return getGuild() + .getGuildChannelCache() + .applyStream(stream -> stream + .filter(ICategorizableChannel.class::isInstance) + .map(ICategorizableChannel.class::cast) + .filter(it -> this.equals(it.getParentCategory())) + .sorted() + .collect(Helpers.toUnmodifiableList()) + ); } /** diff --git a/src/main/java/net/dv8tion/jda/api/managers/channel/attribute/ICategorizableChannelManager.java b/src/main/java/net/dv8tion/jda/api/managers/channel/attribute/ICategorizableChannelManager.java index b6bc4ef1f9..3bc820dbf2 100644 --- a/src/main/java/net/dv8tion/jda/api/managers/channel/attribute/ICategorizableChannelManager.java +++ b/src/main/java/net/dv8tion/jda/api/managers/channel/attribute/ICategorizableChannelManager.java @@ -37,7 +37,7 @@ * @param The manager type */ public interface ICategorizableChannelManager> - extends ChannelManager, IPermissionContainerManager + extends ChannelManager, IPermissionContainerManager, IPositionableChannelManager { /** * Sets the {@link Category Parent Category} diff --git a/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java index ce13ad3d0b..a0f0f227e8 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java @@ -17,11 +17,34 @@ package net.dv8tion.jda.api.utils.cache; import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; import javax.annotation.Nonnull; +/** + * Specialized {@link SnowflakeCacheView} type used for handling channels. + *
This type caches all relevant channel types, including threads. + * + *

Internally, this cache view makes a distinction between the varying {@link ChannelType ChannelTypes} and provides convenient methods to access a filtered subset. + * + * @param + * The channel type + */ public interface ChannelCacheView extends SnowflakeCacheView { + /** + * Creates a decorator around this cache, filtered to only provide access to the given type. + * + * @param type + * The type class (Like {@code TextChannel.class}) + * @param + * The type parameter + * + * @throws IllegalArgumentException + * If null is provided + * + * @return The filtered cache view + */ @Nonnull ChannelCacheView ofType(@Nonnull Class type); } diff --git a/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java b/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java index 03295d8c77..7e46c4f3f2 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java @@ -16,10 +16,23 @@ package net.dv8tion.jda.api.utils.cache; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; import javax.annotation.Nonnull; +/** + * Specialized {@link ChannelCacheView} type used for handling sorted lists of channels. + *
Sorting is done with respect to the positioning in the official Discord client, by comparing positions and category information. + * + *

Internally, this cache view makes a distinction between the varying {@link ChannelType ChannelTypes} and provides convenient methods to access a filtered subset. + * + * @param + * The channel type + * + * @see Guild#getChannels() + */ public interface SortedChannelCacheView> extends ChannelCacheView, SortedSnowflakeCacheView { @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index d90978d965..a27bdf914e 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -767,6 +767,13 @@ public SortedSnowflakeCacheView getThreadChannelCache() return channelCache.ofType(ThreadChannel.class); } + @Nonnull + @Override + public SortedChannelCacheViewImpl getGuildChannelCache() + { + return channelCache; + } + @Nonnull @Override public SortedSnowflakeCacheView getRoleCache() diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index 14ddf126e7..addd999644 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -240,7 +240,7 @@ protected class FilteredCacheView implements ChannelCacheView protected FilteredCacheView(Class type) { - Checks.notNull(type, "Channel Type"); + Checks.notNull(type, "Type"); this.type = type; ChannelType concrete = null; for (ChannelType channelType : ChannelType.values()) From 2a035dfc3613fc4de2203d21f7e34b42bf9c36e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 17 Dec 2023 16:16:09 +0100 Subject: [PATCH 07/15] Optimize channel cache filtering --- src/main/java/net/dv8tion/jda/api/JDA.java | 12 ------ .../entities/channel/concrete/Category.java | 3 +- .../net/dv8tion/jda/internal/JDAImpl.java | 6 +++ .../jda/internal/entities/GuildImpl.java | 9 +++++ .../utils/cache/ChannelCacheViewImpl.java | 39 ++++++++++++++----- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 30b18a5bca..f8e8982445 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -19,7 +19,6 @@ import net.dv8tion.jda.annotations.ForRemoval; import net.dv8tion.jda.annotations.Incubating; import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.attribute.IGuildChannelContainer; import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; @@ -1478,17 +1477,6 @@ default List getScheduledEventsByName(@Nonnull String name, bool return getScheduledEventCache().getElementsByName(name, ignoreCase); } - @Nullable - @Override - default T getChannelById(@Nonnull Class type, long id) - { - Checks.notNull(type, "Class"); - Channel channel = getPrivateChannelById(id); - if (channel != null) - return type.isInstance(channel) ? type.cast(channel) : null; - return IGuildChannelContainer.super.getChannelById(type, id); - } - /** * {@link net.dv8tion.jda.api.utils.cache.SnowflakeCacheView SnowflakeCacheView} of * all cached {@link PrivateChannel PrivateChannels} visible to this JDA session. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java b/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java index 2db5bb8163..dea7266c78 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java @@ -63,9 +63,8 @@ default List getChannels() { return getGuild() .getGuildChannelCache() + .ofType(ICategorizableChannel.class) .applyStream(stream -> stream - .filter(ICategorizableChannel.class::isInstance) - .map(ICategorizableChannel.class::cast) .filter(it -> this.equals(it.getParentCategory())) .sorted() .collect(Helpers.toUnmodifiableList()) diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index 1265ce8f6f..20c8b29e23 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -800,6 +800,12 @@ public PrivateChannel getPrivateChannelById(long id) return channel; } + @Override + public T getChannelById(@Nonnull Class type, long id) + { + return channelCache.ofType(type).getElementById(id); + } + @Nonnull @Override public CacheRestAction openPrivateChannelById(long userId) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index a27bdf914e..4c908f5e55 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -774,6 +774,15 @@ public SortedChannelCacheViewImpl getGuildChannelCache() return channelCache; } + @Override + @SuppressWarnings("unchecked") + public T getChannelById(@Nonnull Class type, long id) + { + return GuildChannel.class.isAssignableFrom(type) + ? (T) channelCache.ofType((Class) type).getElementById(id) + : null; + } + @Nonnull @Override public SortedSnowflakeCacheView getRoleCache() diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index addd999644..3fc03b77f6 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -255,6 +255,20 @@ protected FilteredCacheView(Class type) this.concreteType = concrete; } + protected boolean isOfChannelType(ChannelType channelType) + { + return channelType != null && type.isAssignableFrom(channelType.getInterface()); + } + + @SuppressWarnings("unchecked") + protected Stream> filteredMaps() + { + return caches.entrySet() + .stream() + .filter(entry -> isOfChannelType(entry.getKey())) + .map(entry -> (TLongObjectMap) entry.getValue()); + } + @Nonnull @Override public List asList() @@ -285,10 +299,7 @@ public ClosableIterator lockedIterator() { Iterator directIterator = concreteType != null ? (Iterator) getMap(concreteType).valueCollection().iterator() - : caches.entrySet() - .stream() - .filter((entry) -> type.isAssignableFrom(entry.getKey().getInterface())) - .map(Map.Entry::getValue) + : filteredMaps() .flatMap(map -> map.valueCollection().stream()) .filter(type::isInstance) .map(type::cast) @@ -307,7 +318,11 @@ public long size() { try (UnlockHook hook = readLock()) { - return concreteType != null ? getMap(concreteType).size() : applyStream(Stream::count); + return concreteType != null + ? getMap(concreteType).size() + : filteredMaps() + .mapToLong(TLongObjectMap::size) + .sum(); } } @@ -316,7 +331,10 @@ public boolean isEmpty() { try (UnlockHook hook = readLock()) { - return concreteType != null ? getMap(concreteType).isEmpty() : applyStream(stream -> !stream.findAny().isPresent()); + return concreteType != null + ? getMap(concreteType).isEmpty() + : filteredMaps() + .allMatch(TLongObjectMap::isEmpty); } } @@ -362,10 +380,11 @@ public C getElementById(long id) if (concreteType != null) return type.cast(getMap(concreteType).get(id)); - T element = ChannelCacheViewImpl.this.getElementById(id); - if (type.isInstance(element)) - return type.cast(element); - return null; + return filteredMaps() + .map(it -> it.get(id)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); } } From c8893996c31a94c5fccddcabd16125956358b43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 17 Dec 2023 16:33:07 +0100 Subject: [PATCH 08/15] Cleanup --- .../net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java | 2 +- .../dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java index c61eacc79a..b9e055f883 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ThreadDeleteHandler.java @@ -43,7 +43,7 @@ protected Long handleInternally(DataObject content) final long threadId = content.getLong("id"); ChannelCacheViewImpl channelsView = getJDA().getChannelsView(); - ThreadChannel thread = (ThreadChannel) channelsView.getElementById(threadId); + ThreadChannel thread = channelsView.ofType(ThreadChannel.class).getElementById(threadId); if (thread == null || guild == null) { WebSocketClient.LOG.debug("THREAD_DELETE attempted to delete a thread that is not yet cached. JSON: {}", content); diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index 3fc03b77f6..c31a391f4f 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -301,8 +301,6 @@ public ClosableIterator lockedIterator() ? (Iterator) getMap(concreteType).valueCollection().iterator() : filteredMaps() .flatMap(map -> map.valueCollection().stream()) - .filter(type::isInstance) - .map(type::cast) .iterator(); return new LockIterator<>(directIterator, readLock); } From fd1017b901eb30972e20b5ccad73a2f52c26226c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 17 Dec 2023 16:34:45 +0100 Subject: [PATCH 09/15] Fix compile errors --- .../jda/internal/handle/VoiceChannelStatusUpdateHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/VoiceChannelStatusUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/VoiceChannelStatusUpdateHandler.java index a366e038f0..1a5096e3ab 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/VoiceChannelStatusUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/VoiceChannelStatusUpdateHandler.java @@ -36,7 +36,7 @@ protected Long handleInternally(DataObject content) return guildId; long id = content.getUnsignedLong("id"); - VoiceChannelImpl channel = (VoiceChannelImpl) getJDA().getVoiceChannelsView().getElementById(id); + VoiceChannelImpl channel = (VoiceChannelImpl) getJDA().getVoiceChannelById(id); if (channel == null) { From 0394756c84e447696193de69f2dcec56675f85c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 30 Dec 2023 14:07:38 +0100 Subject: [PATCH 10/15] Add ChannelCacheViewTest --- build.gradle.kts | 1 + .../middleman/AbstractGuildChannelImpl.java | 74 +---- .../jda/internal/utils/ChannelUtil.java | 88 ++++++ .../channel/ChannelCacheViewTest.java | 292 ++++++++++++++++++ 4 files changed, 383 insertions(+), 72 deletions(-) create mode 100644 src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java diff --git a/build.gradle.kts b/build.gradle.kts index beabd400c6..f89f21d0ab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") testImplementation("org.reflections:reflections:0.10.2") + testImplementation("org.mockito:mockito-core:5.8.0") } val compileJava: JavaCompile by tasks diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java index c24058af19..b8b648d9fa 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java @@ -16,15 +16,11 @@ package net.dv8tion.jda.internal.entities.channel.middleman; -import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; -import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; -import net.dv8tion.jda.api.entities.channel.concrete.Category; -import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.internal.entities.GuildImpl; import net.dv8tion.jda.internal.entities.channel.AbstractChannelImpl; import net.dv8tion.jda.internal.entities.channel.mixin.middleman.GuildChannelMixin; -import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.ChannelUtil; import javax.annotation.Nonnull; @@ -48,72 +44,6 @@ public GuildImpl getGuild() @Override public int compareTo(@Nonnull GuildChannel o) { - Checks.notNull(o, "Channel"); - - // Check thread positions - ThreadChannel thisThread = this instanceof ThreadChannel ? (ThreadChannel) this : null; - ThreadChannel otherThread = o instanceof ThreadChannel ? (ThreadChannel) o : null; - - if (thisThread != null && otherThread == null) - return thisThread.getParentChannel().compareTo(o); - if (thisThread == null && otherThread != null) - return this.compareTo(otherThread.getParentChannel()); - if (thisThread != null) - { - // If they are threads on the same channel - if (thisThread.getParentChannel().equals(otherThread.getParentChannel())) - return Long.compare(o.getIdLong(), id); // threads are ordered ascending by age - // If they are threads on different channels - return thisThread.getParentChannel().compareTo(otherThread.getParentChannel()); - } - - // Check category positions - Category thisParent = this instanceof ICategorizableChannel ? ((ICategorizableChannel) this).getParentCategory() : null; - Category otherParent = o instanceof ICategorizableChannel ? ((ICategorizableChannel) o).getParentCategory() : null; - - if (thisParent != null && otherParent == null) - { - if (o instanceof Category) - { - // The other channel is the parent category of this channel - if (o.equals(thisParent)) - return 1; - // The other channel is another category - return thisParent.compareTo(o); - } - return 1; - } - if (thisParent == null && otherParent != null) - { - if (this instanceof Category) - { - // This channel is parent of other channel - if (this.equals(otherParent)) - return -1; - // This channel is a category higher than the other channel's parent category - return this.compareTo(otherParent); //safe use of recursion since no circular parents exist - } - return -1; - } - // Both channels are in different categories, compare the categories instead - if (thisParent != null && !thisParent.equals(otherParent)) - return thisParent.compareTo(otherParent); - - // Check sort bucket (text/message is above audio) - if (getType().getSortBucket() != o.getType().getSortBucket()) - return Integer.compare(getType().getSortBucket(), o.getType().getSortBucket()); - - // Check actual position - if (o instanceof IPositionableChannel && this instanceof IPositionableChannel) - { - IPositionableChannel oPositionableChannel = (IPositionableChannel) o; - IPositionableChannel thisPositionableChannel = (IPositionableChannel) this; - - if (thisPositionableChannel.getPositionRaw() != oPositionableChannel.getPositionRaw()) - return Integer.compare(thisPositionableChannel.getPositionRaw(), oPositionableChannel.getPositionRaw()); - } - - // last resort by id - return Long.compareUnsigned(id, o.getIdLong()); + return ChannelUtil.compare(this, o); } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java b/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java index 1f70447799..3af368d358 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java @@ -18,6 +18,11 @@ import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; +import net.dv8tion.jda.api.entities.channel.concrete.Category; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import java.util.EnumSet; @@ -45,4 +50,87 @@ public static T safeChannelCast(Object instance, Class to String cleanedClassName = instance.getClass().getSimpleName().replace("Impl", ""); throw new IllegalStateException(Helpers.format("Cannot convert channel of type %s to %s!", cleanedClassName, toObjectClass.getSimpleName())); } + + public static int compare(GuildChannel a, GuildChannel b) + { + Checks.notNull(b, "Channel"); + + // Check thread positions + ThreadChannel thisThread = a instanceof ThreadChannel ? (ThreadChannel) a : null; + ThreadChannel otherThread = b instanceof ThreadChannel ? (ThreadChannel) b : null; + + if (thisThread != null && otherThread == null) + { + // Thread should be below its parent + if (thisThread.getParentChannel().getIdLong() == b.getIdLong()) + return 1; + // Otherwise compare parents + return thisThread.getParentChannel().compareTo(b); + } + if (thisThread == null && otherThread != null) + { + // Thread should be below its parent + if (otherThread.getParentChannel().getIdLong() == a.getIdLong()) + return -1; + // Otherwise compare parents + return a.compareTo(otherThread.getParentChannel()); + } + if (thisThread != null) + { + // If they are threads on the same channel + if (thisThread.getParentChannel().getIdLong() == otherThread.getParentChannel().getIdLong()) + return Long.compare(b.getIdLong(), a.getIdLong()); // threads are ordered ascending by age + // If they are threads on different channels + return thisThread.getParentChannel().compareTo(otherThread.getParentChannel()); + } + + // Check category positions + Category thisParent = a instanceof ICategorizableChannel ? ((ICategorizableChannel) a).getParentCategory() : null; + Category otherParent = b instanceof ICategorizableChannel ? ((ICategorizableChannel) b).getParentCategory() : null; + + if (thisParent != null && otherParent == null) + { + if (b instanceof Category) + { + // The other channel is the parent category of this channel + if (b.getIdLong() == thisParent.getIdLong()) + return 1; + // The other channel is another category + return thisParent.compareTo(b); + } + return 1; + } + if (thisParent == null && otherParent != null) + { + if (a instanceof Category) + { + // This channel is parent of other channel + if (a.getIdLong() == otherParent.getIdLong()) + return -1; + // This channel is a category higher than the other channel's parent category + return a.compareTo(otherParent); //safe use of recursion since no circular parents exist + } + return -1; + } + // Both channels are in different categories, compare the categories instead + if (thisParent != null && !thisParent.equals(otherParent)) + return thisParent.compareTo(otherParent); + + // Check sort bucket (text/message is above audio) + if (a.getType().getSortBucket() != b.getType().getSortBucket()) + return Integer.compare(a.getType().getSortBucket(), b.getType().getSortBucket()); + + // Check actual position + if (b instanceof IPositionableChannel && a instanceof IPositionableChannel) + { + IPositionableChannel oPositionableChannel = (IPositionableChannel) b; + IPositionableChannel thisPositionableChannel = (IPositionableChannel) a; + + if (thisPositionableChannel.getPositionRaw() != oPositionableChannel.getPositionRaw()) + return Integer.compare(thisPositionableChannel.getPositionRaw(), oPositionableChannel.getPositionRaw()); + } + + // last resort by id + return Long.compareUnsigned(a.getIdLong(), b.getIdLong()); + } } diff --git a/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java b/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java new file mode 100644 index 0000000000..70b4bd4eac --- /dev/null +++ b/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.entities.channel; + +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.IPostContainer; +import net.dv8tion.jda.api.entities.channel.concrete.Category; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.unions.IThreadContainerUnion; +import net.dv8tion.jda.api.utils.cache.SortedChannelCacheView; +import net.dv8tion.jda.internal.utils.ChannelUtil; +import net.dv8tion.jda.internal.utils.cache.SortedChannelCacheViewImpl; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ChannelCacheViewTest +{ + private static long counter = 0; + + private static final String VALID_SORT_ORDER = String.join("\n", + "TEXT without parent", + "NEWS without parent", + "TEXT parent of GUILD_PRIVATE_THREAD", + "GUILD_PRIVATE_THREAD", + "NEWS parent of GUILD_NEWS_THREAD", + "GUILD_NEWS_THREAD", + "FORUM parent of GUILD_PUBLIC_THREAD", + "GUILD_PUBLIC_THREAD", + "FORUM without parent", + "MEDIA without parent", + "VOICE without parent", + "STAGE without parent", + "CATEGORY parent of TEXT", + "TEXT with parent", + "CATEGORY parent of VOICE", + "VOICE with parent", + "CATEGORY without parent", + "CATEGORY parent of NEWS", + "NEWS with parent", + "CATEGORY parent of STAGE", + "STAGE with parent", + "CATEGORY parent of FORUM", + "FORUM with parent", + "CATEGORY parent of MEDIA", + "MEDIA with parent" + ); + + @SuppressWarnings("unchecked") + private static T mockChannel(ChannelType type, String name) + { + return (T) mockChannel(type.getInterface(), type, name); + } + + @SafeVarargs + private static T mockChannel(Class clazz, ChannelType type, String name, Class... extraInterfaces) + { + T mock = extraInterfaces.length > 0 ? mock(clazz, withSettings().extraInterfaces(extraInterfaces)) : mock(clazz); + when(mock.getType()) + .thenReturn(type); + when(mock.toString()) + .thenReturn(name); + when(mock.getName()) + .thenReturn(name); + when(mock.getIdLong()) + .thenReturn(type.ordinal() + (counter++)); + if (IPositionableChannel.class.isAssignableFrom(clazz)) + { + IPositionableChannel positionable = (IPositionableChannel) mock; + when(positionable.getPositionRaw()) + .thenReturn(type.ordinal() + (int) (counter++)); + } + if (GuildChannel.class.isAssignableFrom(clazz)) + { + GuildChannel comparable = (GuildChannel) mock; + when(comparable.compareTo(any())) + .then((args) -> ChannelUtil.compare((GuildChannel) args.getMock(), args.getArgument(0))); + } + return mock; + } + + private static IThreadContainerUnion getThreadContainer(ChannelType threadType) + { + switch (threadType) + { + case GUILD_PRIVATE_THREAD: + return mockChannel(IThreadContainerUnion.class, ChannelType.TEXT, "TEXT parent of " + threadType, GuildMessageChannel.class); + case GUILD_NEWS_THREAD: + return mockChannel(IThreadContainerUnion.class, ChannelType.NEWS, "NEWS parent of " + threadType, GuildMessageChannel.class); + case GUILD_PUBLIC_THREAD: + return mockChannel(IThreadContainerUnion.class, ChannelType.FORUM, "FORUM parent of " + threadType, IPostContainer.class); + default: + throw new IllegalStateException("Cannot map unknown thread type " + threadType); + } + } + + private static SortedChannelCacheViewImpl getMockedGuildCache() + { + SortedChannelCacheViewImpl view = new SortedChannelCacheViewImpl<>(GuildChannel.class); + + for (ChannelType type : ChannelType.values()) + { + Class channelType = type.getInterface(); + + if (ICategorizableChannel.class.isAssignableFrom(channelType)) + { + Category category = mockChannel(ChannelType.CATEGORY, "CATEGORY parent of " + type); + ICategorizableChannel channel = mockChannel(type, type + " with parent"); + long categoryId = category.getIdLong(); + + when(channel.getParentCategoryIdLong()) + .thenReturn(categoryId); + when(channel.getParentCategory()) + .thenReturn(category); + + view.put(category); + view.put(channel); + + GuildChannel noParent = mockChannel(type, type + " without parent"); + view.put(noParent); + } + else if (ThreadChannel.class.isAssignableFrom(channelType)) + { + IThreadContainerUnion parent = getThreadContainer(type); + ChannelType containerType = parent.getType(); + when(parent.toString()) + .thenReturn(containerType + " parent of " + type); + + ThreadChannel thread = mockChannel(type, type.name()); + when(thread.getParentChannel()) + .thenReturn(parent); + + view.put(parent); + view.put(thread); + } + else if (GuildChannel.class.isAssignableFrom(channelType)) + { + GuildChannel channel = mockChannel(type, type + " without parent"); + view.put(channel); + } + } + + return view; + } + + private static String toListString(Stream stream) + { + return stream.map(Objects::toString).collect(Collectors.joining("\n")); + } + + @Test + void testSortedStream() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.stream()); + assertEquals(VALID_SORT_ORDER, output); + + output = toListString(cache.parallelStream()); + assertEquals(VALID_SORT_ORDER, output); + } + + @Test + void testUnsortedStream() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.streamUnordered()); + assertNotEquals(VALID_SORT_ORDER, output); + + output = toListString(cache.parallelStreamUnordered()); + assertNotEquals(VALID_SORT_ORDER, output); + + output = cache.applyStream(ChannelCacheViewTest::toListString); + assertNotEquals(VALID_SORT_ORDER, output); + } + + @Test + void testAsListWorks() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.asList().stream()); + + assertEquals(VALID_SORT_ORDER, output); + + SortedChannelCacheView voiceView = cache.ofType(VoiceChannel.class); + List fromOfType = voiceView.asList(); + List voiceChannelFilter = cache.applyStream(stream -> stream.filter(VoiceChannel.class::isInstance).collect(Collectors.toList())); + + assertEquals(voiceView.size(), voiceChannelFilter.size()); + assertTrue(fromOfType.containsAll(voiceChannelFilter), "The filtered CacheView must contain all of VoiceChannel"); + assertTrue(voiceChannelFilter.containsAll(fromOfType), "The filtered CacheView must contain exactly all of VoiceChannel"); + } + + @Test + void testAsSetWorks() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.asSet().stream()); + + assertEquals(VALID_SORT_ORDER, output); + + SortedChannelCacheView voiceView = cache.ofType(VoiceChannel.class); + Set fromOfType = voiceView.asSet(); + Set voiceChannelFilter = cache.applyStream(stream -> stream.filter(VoiceChannel.class::isInstance).collect(Collectors.toSet())); + + assertEquals(voiceView.size(), voiceChannelFilter.size()); + assertTrue(fromOfType.containsAll(voiceChannelFilter), "The filtered CacheView must contain all of VoiceChannel"); + assertTrue(voiceChannelFilter.containsAll(fromOfType), "The filtered CacheView must contain exactly all of VoiceChannel"); + } + + @Test + void testSizeWorks() + { + SortedChannelCacheView cache = getMockedGuildCache(); + NavigableSet asSet = cache.asSet(); + + assertEquals(asSet.size(), cache.size()); + + SortedChannelCacheView ofTypeMessage = cache.ofType(GuildMessageChannel.class); + Set filterMessageType = asSet.stream().filter(GuildMessageChannel.class::isInstance).collect(Collectors.toSet()); + + assertEquals(filterMessageType.size(), ofTypeMessage.size()); + } + + @Test + void testEmptyWorks() + { + SortedChannelCacheView empty = new SortedChannelCacheViewImpl<>(GuildChannel.class); + + assertTrue(empty.isEmpty(), "New cache must be empty"); + + SortedChannelCacheViewImpl filled = getMockedGuildCache(); + + assertFalse(filled.ofType(GuildMessageChannel.class).isEmpty(), "Filtered cache must not be empty before remove"); + + filled.removeIf(GuildMessageChannel.class, (c) -> true); + + assertFalse(filled.isEmpty(), "Filled cache must not be empty"); + assertTrue(filled.ofType(GuildMessageChannel.class).isEmpty(), "Filtered cache must be empty"); + } + + @Test + void testRemoveWorks() + { + SortedChannelCacheViewImpl cache = getMockedGuildCache(); + Supplier> getByName = () -> cache.getElementsByName("TEXT without parent", true); + Supplier> getOfType = () -> cache.ofType(GuildMessageChannel.class).asList(); + + GuildChannel textWithoutParent = getByName.get().get(0); + + assertSame(textWithoutParent, cache.remove(textWithoutParent), "Remove returns instance"); + assertTrue(getByName.get().isEmpty(), "Channel should be removed"); + + List messageChannels = getOfType.get(); + + assertFalse(messageChannels.isEmpty(), "Message channels should not be removed"); + + cache.removeIf(GuildChannel.class, GuildMessageChannel.class::isInstance); + + messageChannels = getOfType.get(); + + assertTrue(messageChannels.isEmpty(), "Message channels should be removed"); + } +} From a6d1f0a2ede831392faa2d9905f65e56dddfa012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 6 Jan 2024 16:16:51 +0100 Subject: [PATCH 11/15] Apply review suggestions --- .../jda/internal/entities/GuildImpl.java | 59 +++--- .../internal/handle/ChannelDeleteHandler.java | 174 +++--------------- .../utils/cache/ChannelCacheViewImpl.java | 69 +++---- .../cache/SortedChannelCacheViewImpl.java | 6 +- 4 files changed, 92 insertions(+), 216 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index 4c908f5e55..f0ca644783 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -92,6 +92,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -150,6 +151,7 @@ public void invalidate() { //Remove everything from global cache // this prevents some race-conditions for getting audio managers from guilds + getJDA().getGuildsView().remove(id); ChannelCacheViewImpl channelsView = getJDA().getChannelsView(); try (UnlockHook hook = channelsView.writeLock()) @@ -809,34 +811,49 @@ public SnowflakeCacheView getStickerCache() public List getChannels(boolean includeHidden) { if (includeHidden) - return channelCache.applyStream(stream -> stream.filter(it -> !it.getType().isThread()).sorted().collect(Helpers.toUnmodifiableList())); + { + return channelCache.applyStream(stream -> + stream.filter(it -> !it.getType().isThread()) + .sorted() + .collect(Helpers.toUnmodifiableList()) + ); + } + + // When we remove hidden channels there are 2 considerations to account for: + // + // 1. A channel is not visible if we don't have VIEW_CHANNEL permissions + // 2. A category is not visible if we don't see any channels within it + // + // For this reason we first resolve the category channel lists individually, and then filter out empty categories. + // + // Note: We avoid using Category#getChannels because it would iterate the entire cache each time. + // This is an optimization to avoid many unnecessary iterations. Member self = getSelfMember(); + // We group all channels by their categories first, so that we can later check which categories have no visible channels Map> grouped = new HashMap<>(); - channelCache.forEachUnordered(channel -> { + // Using sets here ensures deduplication + Function> newSet = k -> new HashSet<>(); + channelCache.ofType(ICategorizableChannel.class).forEachUnordered(channel -> + { + // Hide threads and inaccessible channels if (channel.getType().isThread() || !self.hasPermission(channel, Permission.VIEW_CHANNEL)) return; - - if (channel instanceof ICategorizableChannel) - { - Category category = ((ICategorizableChannel) channel).getParentCategory(); - grouped.computeIfAbsent(category, (k) -> new HashSet<>()).add(channel); - grouped.computeIfAbsent(null, (k) -> new HashSet<>()).add(category); - } - else - { - grouped.computeIfAbsent(null, (k) -> new HashSet<>()).add(channel); - } + // Group all categorized channels to their respective category (or null if no parent is set) + Category category = channel.getParentCategory(); + grouped.computeIfAbsent(category, newSet).add(channel); + // Categories are not nested, so their parent is also null, we just always add it to that group + // Empty categories will never show up here, since no categorizable channel will add them to this group + grouped.computeIfAbsent(null, newSet).add(category); }); - for (Map.Entry> entry : grouped.entrySet()) - { - if (entry.getKey() == null) continue; - if (entry.getValue().isEmpty()) - grouped.get(null).remove(entry.getKey()); - } - - return grouped.values().stream().flatMap(Set::stream).sorted().collect(Helpers.toUnmodifiableList()); + // Finally, sort them in the expected order + return grouped + .values() + .stream() + .flatMap(Set::stream) + .sorted() // See ChannelUtil.compare + .collect(Helpers.toUnmodifiableList()); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java index 2d061b62b1..53826d54cf 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java @@ -17,7 +17,8 @@ package net.dv8tion.jda.internal.handle; import net.dv8tion.jda.api.entities.channel.ChannelType; -import net.dv8tion.jda.api.entities.channel.concrete.*; +import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; @@ -47,160 +48,35 @@ protected Long handleInternally(DataObject content) GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); final long channelId = content.getLong("id"); - switch (type) + if (guild == null) { - case TEXT: - { - TextChannel channel = getJDA().getChannelsView().remove(ChannelType.TEXT, channelId); - if (channel == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a text channel that is not yet cached. JSON: {}", content); - return null; - } - - guild.getChannelView().remove(channel); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case NEWS: - { - NewsChannel channel = getJDA().getChannelsView().remove(ChannelType.NEWS, channelId); - if (channel == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a news channel that is not yet cached. JSON: {}", content); - return null; - } - - guild.getChannelView().remove(channel); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case VOICE: - { - VoiceChannel channel = getJDA().getChannelsView().remove(ChannelType.VOICE, channelId); - if (channel == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a voice channel that is not yet cached. JSON: {}", content); - return null; - } - - // This is done in the AudioWebSocket already - // We use this instead of getAudioManager(Guild) so we don't create a new instance. Efficiency! - // AudioManagerImpl manager = (AudioManagerImpl) getJDA().getAudioManagersView().get(guild.getIdLong()); - // if (manager != null && manager.isConnected() - // && manager.getConnectedChannel().getIdLong() == channel.getIdLong()) - // { - // manager.closeAudioConnection(ConnectionStatus.DISCONNECTED_CHANNEL_DELETED); - // } - guild.getChannelView().remove(channel); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case STAGE: - { - StageChannel channel = getJDA().getChannelsView().remove(ChannelType.STAGE, channelId); - if (channel == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a stage channel that is not yet cached. JSON: {}", content); - return null; - } - - guild.getChannelView().remove(channel); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - - case CATEGORY: - { - Category category = getJDA().getChannelsView().remove(ChannelType.CATEGORY, channelId); -// Category category = getJDA().getCategoriesView().remove(channelId); - if (category == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a category channel that is not yet cached. JSON: {}", content); - return null; - } - - guild.getChannelView().remove(category); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - category)); - break; - } - case FORUM: - { - ForumChannel channel = getJDA().getChannelsView().remove(ChannelType.FORUM, channelId); - if (channel == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a forum channel that is not yet cached. JSON: {}", content); - return null; - } - - guild.getChannelView().remove(channel); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case MEDIA: - { - MediaChannel channel = getJDA().getChannelsView().remove(ChannelType.MEDIA, channelId); - if (channel == null || guild == null) - { - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a media channel that is not yet cached. JSON: {}", content); - return null; - } - - guild.getChannelView().remove(channel); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case PRIVATE: - { - PrivateChannel channel = getJDA().getChannelsView().remove(ChannelType.PRIVATE, channelId); - - if (channel == null) - { -// getJDA().getEventCache().cache(EventCache.Type.CHANNEL, channelId, () -> handle(responseNumber, allContent)); - WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a private channel that is not yet cached. JSON: {}", content); - return null; - } - - break; - } - case GROUP: - WebSocketClient.LOG.warn("Received a CHANNEL_DELETE for a channel of type GROUP which is not supported!"); - return null; - default: - WebSocketClient.LOG.debug("CHANNEL_DELETE provided an unknown channel type. JSON: {}", content); + PrivateChannel channel = getJDA().getChannelsView().remove(ChannelType.PRIVATE, channelId); + if (channel == null) + WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a private channel that is not yet cached. JSON: {}", content); + return null; } - if (guild != null) + GuildChannel channel = guild.getChannelById(GuildChannel.class, channelId); + if (channel == null) { - // Deleting any scheduled events associated to the deleted channel as they are deleted when the channel gets deleted. - // There is no delete event for the deletion of scheduled events in this case, so we do this to keep the cache in sync. - String channelId1 = Long.toUnsignedString(channelId); - guild.getScheduledEventsView().stream() - .filter(scheduledEvent -> scheduledEvent.getType().isChannel() && scheduledEvent.getLocation().equals(channelId1)) - .forEach(scheduledEvent -> guild.getScheduledEventsView().remove(scheduledEvent.getIdLong())); + WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a guild channel that is not yet cached. JSON: {}", content); + return null; } + guild.uncacheChannel(channel, false); + + getJDA().handleEvent( + new ChannelDeleteEvent( + getJDA(), responseNumber, + channel)); + + // Deleting any scheduled events associated to the deleted channel as they are deleted when the channel gets deleted. + // There is no delete event for the deletion of scheduled events in this case, so we do this to keep the cache in sync. + String location = Long.toUnsignedString(channelId); + guild.getScheduledEventsView().stream() + .filter(scheduledEvent -> scheduledEvent.getType().isChannel() && scheduledEvent.getLocation().equals(location)) + .forEach(scheduledEvent -> guild.getScheduledEventsView().remove(scheduledEvent.getIdLong())); + getJDA().getEventCache().clear(EventCache.Type.CHANNEL, channelId); return null; } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index c31a391f4f..11000ddb11 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -90,14 +90,11 @@ public C remove(C channel) return remove(channel.getType(), channel.getIdLong()); } - public void removeIf(Class typeFilter, Predicate predicate) + public void removeIf(Class typeFilter, Predicate predicate) { try (UnlockHook hook = writeLock()) { - for (TLongObjectMap map : caches.values()) - { - map.valueCollection().removeIf(c -> typeFilter.isInstance(c) && predicate.test(typeFilter.cast(c))); - } + ofType(typeFilter).removeIf(predicate); } } @@ -111,7 +108,7 @@ public void clear() @Nonnull @Override - public ChannelCacheView ofType(@Nonnull Class type) + public FilteredCacheView ofType(@Nonnull Class type) { return new FilteredCacheView<>(type); } @@ -156,7 +153,10 @@ public ClosableIterator lockedIterator() MiscUtil.tryLock(readLock); try { - Iterator directIterator = caches.values().stream().flatMap(map -> map.valueCollection().stream()).iterator(); + Iterator directIterator = caches.values() + .stream() + .flatMap(map -> map.valueCollection().stream()) + .iterator(); return new LockIterator<>(directIterator, readLock); } catch (Throwable t) @@ -233,42 +233,30 @@ public Iterator iterator() return stream().iterator(); } - protected class FilteredCacheView implements ChannelCacheView + public class FilteredCacheView implements ChannelCacheView { protected final Class type; - protected final ChannelType concreteType; + protected final List> filteredMaps; + @SuppressWarnings("unchecked") protected FilteredCacheView(Class type) { Checks.notNull(type, "Type"); this.type = type; - ChannelType concrete = null; - for (ChannelType channelType : ChannelType.values()) - { - channelType = normalizeKey(channelType); - if (channelType != ChannelType.UNKNOWN && type.equals(channelType.getInterface())) - { - concrete = channelType; - break; - } - } - this.concreteType = concrete; - } - protected boolean isOfChannelType(ChannelType channelType) - { - return channelType != null && type.isAssignableFrom(channelType.getInterface()); + this.filteredMaps = caches.entrySet() + .stream() + .filter(entry -> entry.getKey() != null && type.isAssignableFrom(entry.getKey().getInterface())) + .map(entry -> (TLongObjectMap) entry.getValue()) + .collect(Collectors.toList()); } - @SuppressWarnings("unchecked") - protected Stream> filteredMaps() + protected void removeIf(Predicate filter) { - return caches.entrySet() - .stream() - .filter(entry -> isOfChannelType(entry.getKey())) - .map(entry -> (TLongObjectMap) entry.getValue()); + this.filteredMaps.forEach(map -> map.valueCollection().removeIf(filter)); } + @Nonnull @Override public List asList() @@ -297,9 +285,8 @@ public ClosableIterator lockedIterator() MiscUtil.tryLock(readLock); try { - Iterator directIterator = concreteType != null - ? (Iterator) getMap(concreteType).valueCollection().iterator() - : filteredMaps() + Iterator directIterator =filteredMaps + .stream() .flatMap(map -> map.valueCollection().stream()) .iterator(); return new LockIterator<>(directIterator, readLock); @@ -316,9 +303,8 @@ public long size() { try (UnlockHook hook = readLock()) { - return concreteType != null - ? getMap(concreteType).size() - : filteredMaps() + return filteredMaps + .stream() .mapToLong(TLongObjectMap::size) .sum(); } @@ -329,9 +315,8 @@ public boolean isEmpty() { try (UnlockHook hook = readLock()) { - return concreteType != null - ? getMap(concreteType).isEmpty() - : filteredMaps() + return filteredMaps + .stream() .allMatch(TLongObjectMap::isEmpty); } } @@ -375,10 +360,8 @@ public C getElementById(long id) { try (UnlockHook hook = readLock()) { - if (concreteType != null) - return type.cast(getMap(concreteType).get(id)); - - return filteredMaps() + return filteredMaps + .stream() .map(it -> it.get(id)) .filter(Objects::nonNull) .findFirst() diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java index a4771c1249..f7cbc38693 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/SortedChannelCacheViewImpl.java @@ -37,7 +37,7 @@ public SortedChannelCacheViewImpl(Class type) @Nonnull @Override - public SortedChannelCacheView ofType(@Nonnull Class type) + public SortedFilteredCacheView ofType(@Nonnull Class type) { return new SortedFilteredCacheView<>(type); } @@ -113,7 +113,7 @@ public Iterator iterator() return asSet().iterator(); } - private class SortedFilteredCacheView extends FilteredCacheView implements SortedChannelCacheView + public class SortedFilteredCacheView extends FilteredCacheView implements SortedChannelCacheView { protected SortedFilteredCacheView(Class type) { @@ -152,7 +152,7 @@ public List getElementsByName(@Nonnull String name, boolean ignoreCase) stream .filter(it -> Helpers.equals(name, it.getName(), ignoreCase)) .sorted() - .collect(Collectors.toList()) + .collect(Helpers.toUnmodifiableList()) ); } From df0c2b8de93ab13319c3baf1fc96a61116a0083b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 7 Jan 2024 15:19:19 +0100 Subject: [PATCH 12/15] Improve optimization and add UnifiedChannelCacheView --- src/main/java/net/dv8tion/jda/api/JDA.java | 3 +- .../net/dv8tion/jda/api/entities/Guild.java | 5 +- .../attribute/IGuildChannelContainer.java | 90 ++++------- .../entities/channel/concrete/Category.java | 2 +- .../jda/api/sharding/ShardManager.java | 20 ++- .../jda/api/utils/cache/ChannelCacheView.java | 35 +++++ .../net/dv8tion/jda/internal/JDAImpl.java | 22 +++ .../jda/internal/entities/EntityBuilder.java | 6 +- .../jda/internal/entities/GuildImpl.java | 16 +- .../utils/cache/ChannelCacheViewImpl.java | 19 +++ .../utils/cache/UnifiedChannelCacheView.java | 145 ++++++++++++++++++ 11 files changed, 277 insertions(+), 86 deletions(-) create mode 100644 src/main/java/net/dv8tion/jda/internal/utils/cache/UnifiedChannelCacheView.java diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index f8e8982445..48581126ae 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -19,6 +19,7 @@ import net.dv8tion.jda.annotations.ForRemoval; import net.dv8tion.jda.annotations.Incubating; import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.attribute.IGuildChannelContainer; import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; @@ -70,7 +71,7 @@ * * @see JDABuilder */ -public interface JDA extends IGuildChannelContainer +public interface JDA extends IGuildChannelContainer { /** * Represents the connection status of JDA and its Main WebSocket. diff --git a/src/main/java/net/dv8tion/jda/api/entities/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index fb98d77b19..52af4d95e2 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -90,7 +90,7 @@ * @see JDA#getGuildsByName(String, boolean) * @see JDA#getGuilds() */ -public interface Guild extends IGuildChannelContainer, ISnowflake +public interface Guild extends IGuildChannelContainer, ISnowflake { /** Template for {@link #getIconUrl()}. */ String ICON_URL = "https://cdn.discordapp.com/icons/%s/%s.%s"; @@ -1571,7 +1571,8 @@ default List getScheduledEvents() * @return {@link SortedChannelCacheView SortedChannelCacheView} */ @Nonnull - SortedChannelCacheView getGuildChannelCache(); + @Override + SortedChannelCacheView getChannelCache(); /** * Populated list of {@link GuildChannel channels} for this guild. diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java index ec5a15e0c8..8b1ffb7150 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.api.sharding.ShardManager; import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.cache.CacheView; +import net.dv8tion.jda.api.utils.cache.ChannelCacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; import net.dv8tion.jda.internal.utils.Checks; @@ -45,8 +46,11 @@ *

For the most efficient usage, it is recommended to use {@link CacheView} getters such as {@link #getTextChannelCache()}. * List getters usually require making a snapshot copy of the underlying cache view, which may introduce an undesirable performance hit. */ -public interface IGuildChannelContainer +public interface IGuildChannelContainer { + @Nonnull + ChannelCacheView getChannelCache(); + /** * Get a channel of the specified type by id. * @@ -67,7 +71,7 @@ public interface IGuildChannelContainer * @return The casted channel, if it exists and is assignable to the provided class, or null */ @Nullable - default T getChannelById(@Nonnull Class type, @Nonnull String id) + default T getChannelById(@Nonnull Class type, @Nonnull String id) { return getChannelById(type, MiscUtil.parseSnowflake(id)); } @@ -92,11 +96,10 @@ default T getChannelById(@Nonnull Class type, @Nonnull St * @return The casted channel, if it exists and is assignable to the provided class, or null */ @Nullable - default T getChannelById(@Nonnull Class type, long id) + default T getChannelById(@Nonnull Class type, long id) { Checks.notNull(type, "Class"); - GuildChannel channel = getGuildChannelById(id); - return type.isInstance(channel) ? type.cast(channel) : null; + return getChannelCache().ofType(type).getElementById(id); } /** @@ -164,24 +167,8 @@ default GuildChannel getGuildChannelById(@Nonnull String id) @Nullable default GuildChannel getGuildChannelById(long id) { - //TODO-v5-unified-channel-cache - GuildChannel channel = getTextChannelById(id); - if (channel == null) - channel = getNewsChannelById(id); - if (channel == null) - channel = getVoiceChannelById(id); - if (channel == null) - channel = getStageChannelById(id); - if (channel == null) - channel = getCategoryById(id); - if (channel == null) - channel = getThreadChannelById(id); - if (channel == null) - channel = getForumChannelById(id); - if (channel == null) - channel = getMediaChannelById(id); - - return channel; + C channel = getChannelCache().getElementById(id); + return channel instanceof GuildChannel ? (GuildChannel) channel : null; } /** @@ -260,29 +247,8 @@ default GuildChannel getGuildChannelById(@Nonnull ChannelType type, @Nonnull Str @Nullable default GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) { - Checks.notNull(type, "ChannelType"); - switch (type) - { - case NEWS: - return getNewsChannelById(id); - case TEXT: - return getTextChannelById(id); - case VOICE: - return getVoiceChannelById(id); - case STAGE: - return getStageChannelById(id); - case CATEGORY: - return getCategoryById(id); - case FORUM: - return getForumChannelById(id); - case MEDIA: - return getMediaChannelById(id); - } - - if (type.isThread()) - return getThreadChannelById(id); - - return null; + C channel = getChannelCache().getElementById(type, id); + return channel instanceof GuildChannel ? (GuildChannel) channel : null; } @@ -352,7 +318,7 @@ default List getStageChannelsByName(@Nonnull String name, boolean @Nullable default StageChannel getStageChannelById(@Nonnull String id) { - return getStageChannelCache().getElementById(id); + return (StageChannel) getChannelCache().getElementById(ChannelType.STAGE, id); } /** @@ -374,7 +340,7 @@ default StageChannel getStageChannelById(@Nonnull String id) @Nullable default StageChannel getStageChannelById(long id) { - return getStageChannelCache().getElementById(id); + return (StageChannel) getChannelCache().getElementById(ChannelType.STAGE, id); } /** @@ -473,7 +439,7 @@ default List getThreadChannelsByName(@Nonnull String name, boolea @Nullable default ThreadChannel getThreadChannelById(@Nonnull String id) { - return getThreadChannelCache().getElementById(id); + return (ThreadChannel) getChannelCache().getElementById(ChannelType.GUILD_PUBLIC_THREAD, id); } /** @@ -497,7 +463,7 @@ default ThreadChannel getThreadChannelById(@Nonnull String id) @Nullable default ThreadChannel getThreadChannelById(long id) { - return getThreadChannelCache().getElementById(id); + return (ThreadChannel) getChannelCache().getElementById(ChannelType.GUILD_PUBLIC_THREAD, id); } /** @@ -595,7 +561,7 @@ default List getCategoriesByName(@Nonnull String name, boolean ignoreC @Nullable default Category getCategoryById(@Nonnull String id) { - return getCategoryCache().getElementById(id); + return (Category) getChannelCache().getElementById(ChannelType.CATEGORY, id); } /** @@ -617,7 +583,7 @@ default Category getCategoryById(@Nonnull String id) @Nullable default Category getCategoryById(long id) { - return getCategoryCache().getElementById(id); + return (Category) getChannelCache().getElementById(ChannelType.CATEGORY, id); } /** @@ -711,7 +677,7 @@ default List getTextChannelsByName(@Nonnull String name, boolean ig @Nullable default TextChannel getTextChannelById(@Nonnull String id) { - return getTextChannelCache().getElementById(id); + return (TextChannel) getChannelCache().getElementById(ChannelType.TEXT, id); } /** @@ -733,7 +699,7 @@ default TextChannel getTextChannelById(@Nonnull String id) @Nullable default TextChannel getTextChannelById(long id) { - return getTextChannelCache().getElementById(id); + return (TextChannel) getChannelCache().getElementById(ChannelType.TEXT, id); } /** @@ -827,7 +793,7 @@ default List getNewsChannelsByName(@Nonnull String name, boolean ig @Nullable default NewsChannel getNewsChannelById(@Nonnull String id) { - return getNewsChannelCache().getElementById(id); + return (NewsChannel) getChannelCache().getElementById(ChannelType.NEWS, id); } /** @@ -849,7 +815,7 @@ default NewsChannel getNewsChannelById(@Nonnull String id) @Nullable default NewsChannel getNewsChannelById(long id) { - return getNewsChannelCache().getElementById(id); + return (NewsChannel) getChannelCache().getElementById(ChannelType.NEWS, id); } /** @@ -943,7 +909,7 @@ default List getVoiceChannelsByName(@Nonnull String name, boolean @Nullable default VoiceChannel getVoiceChannelById(@Nonnull String id) { - return getVoiceChannelCache().getElementById(id); + return (VoiceChannel) getChannelCache().getElementById(ChannelType.VOICE, id); } /** @@ -965,7 +931,7 @@ default VoiceChannel getVoiceChannelById(@Nonnull String id) @Nullable default VoiceChannel getVoiceChannelById(long id) { - return getVoiceChannelCache().getElementById(id); + return (VoiceChannel) getChannelCache().getElementById(ChannelType.VOICE, id); } /** @@ -1058,7 +1024,7 @@ default List getForumChannelsByName(@Nonnull String name, boolean @Nullable default ForumChannel getForumChannelById(@Nonnull String id) { - return getForumChannelCache().getElementById(id); + return (ForumChannel) getChannelCache().getElementById(ChannelType.FORUM, id); } /** @@ -1080,7 +1046,7 @@ default ForumChannel getForumChannelById(@Nonnull String id) @Nullable default ForumChannel getForumChannelById(long id) { - return getForumChannelCache().getElementById(id); + return (ForumChannel) getChannelCache().getElementById(ChannelType.FORUM, id); } /** @@ -1172,7 +1138,7 @@ default List getMediaChannelsByName(@Nonnull String name, boolean @Nullable default MediaChannel getMediaChannelById(@Nonnull String id) { - return getMediaChannelCache().getElementById(id); + return (MediaChannel) getChannelCache().getElementById(ChannelType.MEDIA, id); } /** @@ -1194,7 +1160,7 @@ default MediaChannel getMediaChannelById(@Nonnull String id) @Nullable default MediaChannel getMediaChannelById(long id) { - return getMediaChannelCache().getElementById(id); + return (MediaChannel) getChannelCache().getElementById(ChannelType.MEDIA, id); } /** diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java b/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java index dea7266c78..f10a7719a8 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/concrete/Category.java @@ -62,7 +62,7 @@ public interface Category extends GuildChannel, ICopyableChannel, IPositionableC default List getChannels() { return getGuild() - .getGuildChannelCache() + .getChannelCache() .ofType(ICategorizableChannel.class) .applyStream(stream -> stream .filter(it -> this.equals(it.getParentCategory())) diff --git a/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java b/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java index a611589ac0..8378d08758 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java @@ -32,12 +32,14 @@ import net.dv8tion.jda.api.requests.Route; import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.cache.CacheView; +import net.dv8tion.jda.api.utils.cache.ChannelCacheView; import net.dv8tion.jda.api.utils.cache.ShardCacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.requests.CompletedRestAction; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.cache.UnifiedChannelCacheView; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -685,17 +687,6 @@ default List getRolesByName(@Nonnull final String name, final boolean igno return this.getRoleCache().getElementsByName(name, ignoreCase); } - @Nullable - @Override - default T getChannelById(@Nonnull Class type, long id) - { - Checks.notNull(type, "Class"); - Channel channel = getPrivateChannelById(id); - if (channel != null) - return type.isInstance(channel) ? type.cast(channel) : null; - return IGuildChannelContainer.super.getChannelById(type, id); - } - /** * This returns the {@link net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel PrivateChannel} which has the same id as the one provided. *
If there is no known {@link net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel PrivateChannel} with an id that matches the provided @@ -819,6 +810,13 @@ default SnowflakeCacheView getMediaChannelCache() return CacheView.allSnowflakes(() -> this.getShardCache().stream().map(JDA::getMediaChannelCache)); } + @Nonnull + @Override + default ChannelCacheView getChannelCache() + { + return new UnifiedChannelCacheView<>(() -> this.getShardCache().stream().map(JDA::getChannelCache)); + } + /** * This returns the {@link net.dv8tion.jda.api.JDA JDA} instance which has the same id as the one provided. *
If there is no shard with an id that matches the provided one, this will return {@code null}. diff --git a/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java index a0f0f227e8..fd49d35d82 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java @@ -18,8 +18,10 @@ import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.utils.MiscUtil; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Specialized {@link SnowflakeCacheView} type used for handling channels. @@ -47,4 +49,37 @@ public interface ChannelCacheView extends SnowflakeCacheView< */ @Nonnull ChannelCacheView ofType(@Nonnull Class type); + + /** + * Retrieves the entity represented by the provided ID. + * + * @param type + * The expected {@link ChannelType} + * @param id + * The ID of the entity + * + * @return Possibly-null entity for the specified ID, null if the expected type is different from the actual type + */ + @Nullable + T getElementById(@Nonnull ChannelType type, long id); + + /** + * Retrieves the entity represented by the provided ID. + * + * @param type + * The expected {@link ChannelType} + * @param id + * The ID of the entity + * + * @throws java.lang.NumberFormatException + * If the provided String is {@code null} or + * cannot be resolved to an unsigned long id + * + * @return Possibly-null entity for the specified ID, null if the expected type is different from the actual type + */ + @Nullable + default T getElementById(@Nonnull ChannelType type, @Nonnull String id) + { + return getElementById(type, MiscUtil.parseSnowflake(id)); + } } diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index 20c8b29e23..30984d94cf 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -28,6 +28,7 @@ import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.*; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; import net.dv8tion.jda.api.entities.sticker.StickerPack; import net.dv8tion.jda.api.entities.sticker.StickerSnowflake; @@ -55,6 +56,7 @@ import net.dv8tion.jda.api.utils.*; import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.cache.CacheView; +import net.dv8tion.jda.api.utils.cache.ChannelCacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; @@ -722,6 +724,13 @@ public SnowflakeCacheView getScheduledEventCache() return CacheView.allSnowflakes(() -> guildCache.stream().map(Guild::getScheduledEventCache)); } + @Nonnull + @Override + public ChannelCacheView getChannelCache() + { + return channelCache; + } + @Nonnull @Override public SnowflakeCacheView getCategoryCache() @@ -806,6 +815,19 @@ public T getChannelById(@Nonnull Class type, long id) return channelCache.ofType(type).getElementById(id); } + @Override + public GuildChannel getGuildChannelById(long id) + { + return channelCache.ofType(GuildChannel.class).getElementById(id); + } + + @Override + public GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) + { + Channel channel = channelCache.getElementById(type, id); + return channel instanceof GuildChannel ? (GuildChannel) channel : null; + } + @Nonnull @Override public CacheRestAction openPrivateChannelById(long userId) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index 9854bb562a..e9fea20999 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -1076,7 +1076,7 @@ public Category createCategory(GuildImpl guild, DataObject json, long guildId) { boolean playbackCache = false; final long id = json.getLong("id"); - CategoryImpl channel = (CategoryImpl) getJDA().getChannelsView().getElementById(id); + CategoryImpl channel = (CategoryImpl) getJDA().getCategoryById(id); if (channel == null) { if (guild == null) @@ -1438,7 +1438,7 @@ public MediaChannel createMediaChannel(GuildImpl guild, DataObject json, long gu { boolean playbackCache = false; final long id = json.getLong("id"); - MediaChannelImpl channel = (MediaChannelImpl) getJDA().getChannelsView().getElementById(id); + MediaChannelImpl channel = (MediaChannelImpl) getJDA().getMediaChannelById(id); if (channel == null) { if (guild == null) @@ -1671,7 +1671,7 @@ public ReceivedMessage createMessageWithLookup(DataObject json, @Nullable Guild if (guild == null) return createMessage0(json, createPrivateChannelByMessage(json), null, modifyCache); //If we know that the message was sent in a guild, we can use the guild to resolve the channel directly - MessageChannel channel = guild.getChannelById(MessageChannel.class, json.getUnsignedLong("channel_id")); + MessageChannel channel = guild.getChannelById(GuildMessageChannel.class, json.getUnsignedLong("channel_id")); // if (channel == null) // throw new IllegalArgumentException(MISSING_CHANNEL); return createMessage0(json, channel, (GuildImpl) guild, modifyCache); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index f0ca644783..fd4e86d921 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -771,18 +771,22 @@ public SortedSnowflakeCacheView getThreadChannelCache() @Nonnull @Override - public SortedChannelCacheViewImpl getGuildChannelCache() + public SortedChannelCacheViewImpl getChannelCache() { return channelCache; } + @Nullable + @Override + public GuildChannel getGuildChannelById(long id) + { + return channelCache.getElementById(id); + } + @Override - @SuppressWarnings("unchecked") - public T getChannelById(@Nonnull Class type, long id) + public GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) { - return GuildChannel.class.isAssignableFrom(type) - ? (T) channelCache.ofType((Class) type).getElementById(id) - : null; + return channelCache.getElementById(type, id); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java index 11000ddb11..04bbb05e96 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -58,6 +58,7 @@ protected ChannelType normalizeKey(ChannelType type) return type.isThread() ? ChannelType.GUILD_PUBLIC_THREAD : type; } + @Nullable @SuppressWarnings("unchecked") protected TLongObjectMap getMap(@Nonnull ChannelType type) { @@ -226,6 +227,16 @@ public T getElementById(long id) } } + public T getElementById(@Nonnull ChannelType type, long id) + { + Checks.notNull(type, "ChannelType"); + try (UnlockHook hook = readLock()) + { + TLongObjectMap map = getMap(type); + return map == null ? null : map.get(id); + } + } + @Nonnull @Override public Iterator iterator() @@ -354,6 +365,14 @@ public ChannelCacheView ofType(@Nonnull Class type) return ChannelCacheViewImpl.this.ofType(type); } + @Nullable + @Override + public C getElementById(@Nonnull ChannelType type, long id) + { + T channel = ChannelCacheViewImpl.this.getElementById(type, id); + return this.type.isInstance(channel) ? this.type.cast(channel) : null; + } + @Nullable @Override public C getElementById(long id) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/UnifiedChannelCacheView.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/UnifiedChannelCacheView.java new file mode 100644 index 0000000000..ea7bdba6f2 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/UnifiedChannelCacheView.java @@ -0,0 +1,145 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.internal.utils.cache; + +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.utils.ClosableIterator; +import net.dv8tion.jda.api.utils.cache.ChannelCacheView; +import net.dv8tion.jda.internal.utils.ChainedClosableIterator; +import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.Helpers; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class UnifiedChannelCacheView implements ChannelCacheView +{ + private final Supplier>> supplier; + + public UnifiedChannelCacheView(Supplier>> supplier) + { + this.supplier = supplier; + } + + @Override + public void forEach(Consumer action) + { + Objects.requireNonNull(action, "Consumer"); + try (ClosableIterator iterator = lockedIterator()) + { + while (iterator.hasNext()) + action.accept(iterator.next()); + } + } + + @Nonnull + @Override + public List asList() + { + return stream().collect(Helpers.toUnmodifiableList()); + } + + @Nonnull + @Override + public Set asSet() + { + return stream().collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + } + + @Nonnull + @Override + public ClosableIterator lockedIterator() + { + return new ChainedClosableIterator<>(supplier.get().iterator()); + } + + @Override + public long size() + { + return supplier.get().mapToLong(ChannelCacheView::size).sum(); + } + + @Override + public boolean isEmpty() + { + return supplier.get().allMatch(ChannelCacheView::isEmpty); + } + + @Nonnull + @Override + public List getElementsByName(@Nonnull String name, boolean ignoreCase) + { + return supplier.get() + .flatMap(view -> view.getElementsByName(name, ignoreCase).stream()) + .collect(Helpers.toUnmodifiableList()); + } + + @Nonnull + @Override + public Stream stream() + { + return supplier.get().flatMap(ChannelCacheView::stream); + } + + @Nonnull + @Override + public Stream parallelStream() + { + return supplier.get().parallel().flatMap(ChannelCacheView::parallelStream); + } + + @Nonnull + @Override + public ChannelCacheView ofType(@Nonnull Class type) + { + Checks.notNull(type, "Type"); + return new UnifiedChannelCacheView<>(() -> supplier.get().map(view -> view.ofType(type))); + } + + @Nullable + @Override + public C getElementById(@Nonnull ChannelType type, long id) + { + return supplier.get().map(view -> view.getElementById(type, id)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + @Nullable + @Override + public C getElementById(long id) + { + return supplier.get().map(view -> view.getElementById(id)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + @Nonnull + @Override + public Iterator iterator() + { + return stream().iterator(); + } +} From dd2ddfb42852ca210e2cec3bc74176fc4b78f2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sun, 7 Jan 2024 15:32:54 +0100 Subject: [PATCH 13/15] Update docs --- src/main/java/net/dv8tion/jda/api/entities/Guild.java | 4 +++- .../channel/attribute/IGuildChannelContainer.java | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/api/entities/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index 52af4d95e2..f9dd51d479 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -27,6 +27,7 @@ import net.dv8tion.jda.api.entities.automod.AutoModTriggerType; import net.dv8tion.jda.api.entities.automod.build.AutoModRuleData; import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.attribute.ICopyableChannel; import net.dv8tion.jda.api.entities.channel.attribute.IGuildChannelContainer; import net.dv8tion.jda.api.entities.channel.attribute.IInviteContainer; @@ -1566,7 +1567,8 @@ default List getScheduledEvents() * Various methods like {@link SortedChannelCacheView#forEachUnordered(Consumer)} or {@link SortedChannelCacheView#lockedIterator()} * bypass sorting for optimization reasons. * - *

It is possible to filter the channels to more specific types using {@link SortedChannelCacheView#ofType(Class)}. + *

It is possible to filter the channels to more specific types using + * {@link ChannelCacheView#getElementById(ChannelType, long)} or {@link SortedChannelCacheView#ofType(Class)}. * * @return {@link SortedChannelCacheView SortedChannelCacheView} */ diff --git a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java index 8b1ffb7150..1e16fd7b73 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java +++ b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java @@ -48,6 +48,15 @@ */ public interface IGuildChannelContainer { + /** + * Unified cache of all channels associated with this shard or guild. + * + *

This {@link ChannelCacheView} stores all channels in individually typed maps based on {@link ChannelType}. + * You can use {@link ChannelCacheView#getElementById(ChannelType, long)} or {@link ChannelCacheView#ofType(Class)} to filter + * out more specific types. + * + * @return {@link ChannelCacheView} + */ @Nonnull ChannelCacheView getChannelCache(); From 53e30d89394943b4a6ca68b6c328aba56d2c2930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Jan 2024 17:39:38 +0100 Subject: [PATCH 14/15] Simplify Guild#getChannels --- .../jda/internal/entities/GuildImpl.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index fd4e86d921..0614f7b3ae 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -828,36 +828,30 @@ public List getChannels(boolean includeHidden) // 1. A channel is not visible if we don't have VIEW_CHANNEL permissions // 2. A category is not visible if we don't see any channels within it // - // For this reason we first resolve the category channel lists individually, and then filter out empty categories. + // In our implementation we iterate all applicable channels and only add categories, + // when a member of the category is added too. // // Note: We avoid using Category#getChannels because it would iterate the entire cache each time. // This is an optimization to avoid many unnecessary iterations. Member self = getSelfMember(); - // We group all channels by their categories first, so that we can later check which categories have no visible channels - Map> grouped = new HashMap<>(); - // Using sets here ensures deduplication - Function> newSet = k -> new HashSet<>(); + SortedSet channels = new TreeSet<>(); channelCache.ofType(ICategorizableChannel.class).forEachUnordered(channel -> { // Hide threads and inaccessible channels if (channel.getType().isThread() || !self.hasPermission(channel, Permission.VIEW_CHANNEL)) return; - // Group all categorized channels to their respective category (or null if no parent is set) + Category category = channel.getParentCategory(); - grouped.computeIfAbsent(category, newSet).add(channel); - // Categories are not nested, so their parent is also null, we just always add it to that group - // Empty categories will never show up here, since no categorizable channel will add them to this group - grouped.computeIfAbsent(null, newSet).add(category); + channels.add(channel); + + // Empty categories will never show up here, + // since no categorizable channel will add them to this group + if (category != null) + channels.add(category); }); - // Finally, sort them in the expected order - return grouped - .values() - .stream() - .flatMap(Set::stream) - .sorted() // See ChannelUtil.compare - .collect(Helpers.toUnmodifiableList()); + return Collections.unmodifiableList(new ArrayList<>(channels)); } @Nonnull From 1f99ff97c29607f50f706a953c76bbfbe4c34961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 13 Jan 2024 19:04:42 +0100 Subject: [PATCH 15/15] Add comment to document intentions of idempotency --- .../net/dv8tion/jda/internal/entities/GuildImpl.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index 0614f7b3ae..de5c76df09 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -192,8 +192,14 @@ public void invalidate() public void uncacheChannel(GuildChannel channel, boolean keepThreads) { long id = channel.getIdLong(); - if (channelCache.remove(channel.getType(), id) != null) - api.getChannelsView().remove(channel.getType(), id); + + // Enforce idempotence by checking the channel was in cache + // If the channel was not in cache, there is no reason to cleanup anything else. + // This idempotency makes sure that new cache is never affected by old cache + if (channelCache.remove(channel.getType(), id) == null) + return; + + api.getChannelsView().remove(channel.getType(), id); if (!keepThreads && channel instanceof IThreadContainer) {