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/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 30b18a5bca..48581126ae 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -71,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. @@ -1478,17 +1478,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/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index 10af96368b..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; @@ -59,10 +60,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; @@ -93,7 +91,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"; @@ -1561,6 +1559,23 @@ 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 ChannelCacheView#getElementById(ChannelType, long)} or {@link SortedChannelCacheView#ofType(Class)}. + * + * @return {@link SortedChannelCacheView SortedChannelCacheView} + */ + @Nonnull + @Override + SortedChannelCacheView getChannelCache(); + /** * 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/IGuildChannelContainer.java b/src/main/java/net/dv8tion/jda/api/entities/channel/attribute/IGuildChannelContainer.java index ec5a15e0c8..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 @@ -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,20 @@ *

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 { + /** + * 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(); + /** * Get a channel of the specified type by id. * @@ -67,7 +80,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 +105,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 +176,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 +256,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 +327,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 +349,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 +448,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 +472,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 +570,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 +592,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 +686,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 +708,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 +802,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 +824,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 +918,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 +940,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 +1033,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 +1055,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 +1147,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 +1169,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/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..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 @@ -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,14 @@ 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() + .getChannelCache() + .ofType(ICategorizableChannel.class) + .applyStream(stream -> stream + .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/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 new file mode 100644 index 0000000000..fd49d35d82 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/ChannelCacheView.java @@ -0,0 +1,85 @@ +/* + * 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 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. + *
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); + + /** + * 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/api/utils/cache/SortedChannelCacheView.java b/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java new file mode 100644 index 0000000000..7e46c4f3f2 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/cache/SortedChannelCacheView.java @@ -0,0 +1,40 @@ +/* + * 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.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 + SortedChannelCacheView 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 16e916943f..30984d94cf 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -26,7 +26,9 @@ 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.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; @@ -54,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; @@ -75,6 +78,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,16 +105,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 mediaChannelsCache = new SnowflakeCacheViewImpl<>(MediaChannel.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()); @@ -269,7 +265,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); } } } @@ -728,67 +724,74 @@ public SnowflakeCacheView getScheduledEventCache() return CacheView.allSnowflakes(() -> guildCache.stream().map(Guild::getScheduledEventCache)); } + @Nonnull + @Override + public ChannelCacheView getChannelCache() + { + return channelCache; + } + @Nonnull @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 getMediaChannelCache() { - return mediaChannelsCache; + return channelCache.ofType(MediaChannel.class); } @Nonnull @Override public SnowflakeCacheView getPrivateChannelCache() { - return privateChannelCache; + return channelCache.ofType(PrivateChannel.class); } @Override @@ -806,6 +809,25 @@ public PrivateChannel getPrivateChannelById(long id) return channel; } + @Override + 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) @@ -1252,49 +1274,9 @@ public SnowflakeCacheViewImpl getGuildsView() return guildCache; } - public SnowflakeCacheViewImpl getCategoriesView() - { - 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 getMediaChannelsView() - { - return mediaChannelsCache; - } - - public SnowflakeCacheViewImpl getPrivateChannelsView() + public ChannelCacheViewImpl getChannelsView() { - 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 cae8badde5..e9fea20999 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; @@ -69,6 +70,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; @@ -1074,21 +1076,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().getCategoryById(id); if (channel == null) { if (guild == null) guild = (GuildImpl) getJDA().getGuildsView().get(guildId); - SnowflakeCacheViewImpl - guildCategoryView = guild.getCategoriesView(), - categoryView = getJDA().getCategoriesView(); + 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.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1112,21 +1113,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(); + 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.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1156,21 +1156,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(); + 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.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1197,21 +1196,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(); + 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.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1243,21 +1241,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(); + 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.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1303,12 +1300,11 @@ 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(); + ChannelCacheViewImpl guildThreadView = guild.getChannelView(); + ChannelCacheViewImpl threadView = getJDA().getChannelsView(); try ( UnlockHook vlock = guildThreadView.writeLock(); UnlockHook jlock = threadView.writeLock()) @@ -1316,8 +1312,8 @@ public ThreadChannel createThreadChannel(GuildImpl guild, DataObject json, long channel = new ThreadChannelImpl(id, guild, type); if (modifyCache) { - guildThreadView.getMap().put(id, channel); - playbackCache = threadView.getMap().put(id, channel) == null; + guildThreadView.put(channel); + playbackCache = threadView.put(channel) == null; } } } @@ -1390,21 +1386,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(); + 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); - playbackCache = globalView.getMap().put(id, channel) == null; + guildView.put(channel); + playbackCache = globalView.put(channel) == null; } } @@ -1443,21 +1438,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().getMediaChannelById(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; } } @@ -1556,11 +1550,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()); } @@ -1680,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 11ecc5bc7f..de5c76df09 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; @@ -77,10 +78,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; @@ -94,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; @@ -103,15 +102,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 SortedSnowflakeCacheViewImpl mediaChannelCache = new SortedSnowflakeCacheViewImpl<>(MediaChannel.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); @@ -159,57 +151,12 @@ 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 mediaView = getJDA().getMediaChannelsView(); - 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 = mediaView.writeLock()) - { - getMediaChannelsView() - .forEachUnordered(chan -> mediaView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = voiceView.writeLock()) - { - getVoiceChannelCache() - .forEachUnordered(chan -> voiceView.getMap().remove(chan.getIdLong())); - } - try (UnlockHook hook = categoryView.writeLock()) + getJDA().getGuildsView().remove(id); + + 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 @@ -245,71 +192,28 @@ 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; - } + + // 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) { // 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 @@ -819,56 +723,76 @@ public SortedSnowflakeCacheView getScheduledEventCache() @Override public SortedSnowflakeCacheView getCategoryCache() { - return categoryCache; + return channelCache.ofType(Category.class); } @Nonnull @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 + @Override + public SortedChannelCacheViewImpl getChannelCache() + { + return channelCache; + } + + @Nullable + @Override + public GuildChannel getGuildChannelById(long id) + { + return channelCache.getElementById(id); + } + + @Override + public GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) + { + return channelCache.getElementById(type, id); } @Nonnull @@ -896,38 +820,44 @@ 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()) + ); + } + + // 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 + // + // 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(); - 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 -> + + SortedSet channels = new TreeSet<>(); + channelCache.ofType(ICategorizableChannel.class).forEachUnordered(channel -> { - if (!includeHidden && category.getChannels().stream().noneMatch(filterHidden)) - return; + // Hide threads and inaccessible channels + if (channel.getType().isThread() || !self.hasPermission(channel, Permission.VIEW_CHANNEL)) return; - channels.add(category); - }); + Category category = channel.getParentCategory(); + channels.add(channel); - // See AbstractGuildChannelImpl#compareTo for details on how this achieves the canonical order of the client - Collections.sort(channels); + // Empty categories will never show up here, + // since no categorizable channel will add them to this group + if (category != null) + channels.add(category); + }); - return Collections.unmodifiableList(channels); + return Collections.unmodifiableList(new ArrayList<>(channels)); } @Nonnull @@ -2309,44 +2239,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 b59ac14014..827fcfce28 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 @@ -243,12 +243,4 @@ public VoiceChannelImpl setStatus(String status) this.status = status; return this; } - - // -- Abstract Hooks -- - - @Override - protected void onPositionChange() - { - getGuild().getVoiceChannelsView().clearCachedLists(); - } } 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/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 a37aa145ca..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,13 +17,13 @@ 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; 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 { @@ -48,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().getTextChannelsView().remove(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.getTextChannelsView().remove(channel.getIdLong()); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case NEWS: - { - NewsChannel channel = getJDA().getNewsChannelView().remove(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.getNewsChannelView().remove(channel.getIdLong()); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case VOICE: - { - VoiceChannel channel = getJDA().getVoiceChannelsView().remove(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.getVoiceChannelsView().remove(channel.getIdLong()); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case STAGE: - { - StageChannel channel = getJDA().getStageChannelView().remove(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.getStageChannelsView().remove(channel.getIdLong()); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - - case CATEGORY: - { - 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.getCategoriesView().remove(channelId); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - category)); - break; - } - case FORUM: - { - ForumChannel channel = getJDA().getForumChannelsView().remove(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.getForumChannelsView().remove(channel.getIdLong()); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case MEDIA: - { - MediaChannel channel = getJDA().getMediaChannelsView().remove(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()); - getJDA().handleEvent( - new ChannelDeleteEvent( - getJDA(), responseNumber, - channel)); - break; - } - case PRIVATE: - { - SnowflakeCacheViewImpl privateView = getJDA().getPrivateChannelsView(); - PrivateChannel channel = privateView.remove(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/handle/ChannelUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java index e36ba17698..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; @@ -61,7 +62,7 @@ import net.dv8tion.jda.internal.entities.channel.mixin.middleman.MessageChannelMixin; import net.dv8tion.jda.internal.requests.WebSocketClient; import net.dv8tion.jda.internal.utils.UnlockHook; -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.ArrayList; @@ -350,17 +351,16 @@ private void handleHideChildThreads(IThreadContainer channel) for (ThreadChannel thread : threads) { GuildImpl guild = (GuildImpl) channel.getGuild(); - SnowflakeCacheViewImpl - guildThreadView = guild.getThreadChannelsView(), - threadView = getJDA().getThreadChannelsView(); + ChannelCacheViewImpl guildThreadView = guild.getChannelView(); + 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()); - guildThreadView.getMap().remove(thread.getIdLong()); + threadView.remove(thread.getType(), 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 57a83b034f..b9e055f883 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,14 +42,16 @@ 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 = 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); return null; } - guild.getThreadChannelsView().remove(threadId); + channelsView.remove(thread.getType(), 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 b0efe4071d..6f2ab353a7 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ThreadUpdateHandler.java @@ -17,8 +17,10 @@ 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.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; @@ -27,8 +29,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.SortedSnowflakeCacheViewImpl; +import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl; import java.util.List; import java.util.Objects; @@ -194,10 +195,10 @@ protected Long handleInternally(DataObject content) if (thread.isArchived()) { - SortedSnowflakeCacheViewImpl guildView = thread.getGuild().getThreadChannelsView(); - SnowflakeCacheViewImpl globalView = api.getThreadChannelsView(); - guildView.remove(threadId); - globalView.remove(threadId); + ChannelCacheViewImpl guildView = thread.getGuild().getChannelView(); + ChannelCacheViewImpl globalView = api.getChannelsView(); + guildView.remove(thread); + globalView.remove(thread); } return null; 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) { 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 6225bc0463..1a7c1fca7f 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -801,15 +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.getMediaChannelsView().clear(); + api.getChannelsView().clear(); api.getGuildsView().clear(); api.getUsersView().clear(); 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/main/java/net/dv8tion/jda/internal/utils/Helpers.java b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java index 1c9ca0a0f6..b8a7051213 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -223,6 +223,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..04bbb05e96 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/ChannelCacheViewImpl.java @@ -0,0 +1,398 @@ +/* + * 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.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ChannelCacheViewImpl extends ReadWriteLockCache implements ChannelCacheView +{ + protected final EnumMap> caches = new EnumMap<>(ChannelType.class); + + 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 + protected ChannelType normalizeKey(ChannelType type) + { + return type.isThread() ? ChannelType.GUILD_PUBLIC_THREAD : type; + } + + @Nullable + @SuppressWarnings("unchecked") + protected TLongObjectMap getMap(@Nonnull ChannelType type) + { + return (TLongObjectMap) caches.get(normalizeKey(type)); + } + + @Nullable + @SuppressWarnings("unchecked") + public C put(C element) + { + try (UnlockHook hook = writeLock()) + { + return (C) getMap(element.getType()).put(element.getIdLong(), element); + } + } + + @Nullable + @SuppressWarnings("unchecked") + public C remove(ChannelType type, long id) + { + try (UnlockHook hook = writeLock()) + { + T removed = getMap(type).remove(id); + return (C) removed; + } + } + + public C remove(C channel) + { + return remove(channel.getType(), channel.getIdLong()); + } + + public void removeIf(Class typeFilter, Predicate predicate) + { + try (UnlockHook hook = writeLock()) + { + ofType(typeFilter).removeIf(predicate); + } + } + + public void clear() + { + try (UnlockHook hook = writeLock()) + { + caches.values().forEach(TLongObjectMap::clear); + } + } + + @Nonnull + @Override + public FilteredCacheView 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() + { + List list = getCachedList(); + if (list == null) + list = cache((List) applyStream(stream -> stream.collect(Collectors.toList()))); + return list; + } + + @Nonnull + @Override + public Set asSet() + { + Set set = getCachedSet(); + if (set == null) + set = cache((Set) applyStream(stream -> stream.collect(Collectors.toSet()))); + return set; + } + + @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; + } + } + + 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() + { + return stream().iterator(); + } + + public class FilteredCacheView implements ChannelCacheView + { + protected final Class type; + protected final List> filteredMaps; + + @SuppressWarnings("unchecked") + protected FilteredCacheView(Class type) + { + Checks.notNull(type, "Type"); + this.type = type; + + this.filteredMaps = caches.entrySet() + .stream() + .filter(entry -> entry.getKey() != null && type.isAssignableFrom(entry.getKey().getInterface())) + .map(entry -> (TLongObjectMap) entry.getValue()) + .collect(Collectors.toList()); + } + + protected void removeIf(Predicate filter) + { + this.filteredMaps.forEach(map -> map.valueCollection().removeIf(filter)); + } + + + @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 =filteredMaps + .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 filteredMaps + .stream() + .mapToLong(TLongObjectMap::size) + .sum(); + } + } + + @Override + public boolean isEmpty() + { + try (UnlockHook hook = readLock()) + { + return filteredMaps + .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 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(@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) + { + try (UnlockHook hook = readLock()) + { + return filteredMaps + .stream() + .map(it -> it.get(id)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + } + + @Nonnull + @Override + public Iterator iterator() + { + return asList().iterator(); + } + } +} 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..f7cbc38693 --- /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 SortedFilteredCacheView 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(); + } + + public 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(Helpers.toUnmodifiableList()) + ); + } + + @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); + } + } +} 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(); + } +} 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"); + } +}