From 9e7a3d26e4992df88e9eb439fc2760c13ca2785e Mon Sep 17 00:00:00 2001 From: lucko Date: Sun, 16 Jun 2024 21:11:23 +0100 Subject: [PATCH] Improve efficiency of action log queries (#3917) --- .../java/net/luckperms/api/LuckPerms.java | 11 + .../net/luckperms/api/actionlog/Action.java | 8 +- .../luckperms/api/actionlog/ActionLog.java | 5 + .../luckperms/api/actionlog/ActionLogger.java | 67 +++- .../api/actionlog/filter/ActionFilter.java | 114 ++++++ .../actionlog/filter/ActionFilterFactory.java | 88 +++++ .../api/actionlog/filter/package-info.java | 29 ++ .../api/event/sync/PostNetworkSyncEvent.java | 1 - .../java/net/luckperms/api/util/Page.java | 53 +++ .../listeners/BukkitPlatformListener.java | 1 - common/build.gradle | 11 +- .../lucko/luckperms/common/actionlog/Log.java | 108 ------ .../common/actionlog/LogDispatcher.java | 68 ++-- .../Paginated.java => actionlog/LogPage.java} | 64 ++-- .../common/actionlog/LoggedAction.java | 23 +- .../common/actionlog/filter/ActionFields.java | 60 +++ .../filter/ActionFilterMongoBuilder.java | 70 ++++ .../filter/ActionFilterSqlBuilder.java | 69 ++++ .../actionlog/filter/ActionFilters.java | 110 ++++++ .../common/api/LuckPermsApiProvider.java | 6 + .../api/implementation/ApiActionFilter.java | 47 +++ .../ApiActionFilterFactory.java | 77 ++++ .../api/implementation/ApiActionLog.java | 35 +- .../api/implementation/ApiActionLogger.java | 61 ++- .../common/bulkupdate/BulkUpdate.java | 87 +---- .../common/bulkupdate/BulkUpdateBuilder.java | 25 +- .../common/bulkupdate/BulkUpdateField.java | 68 ++++ .../bulkupdate/BulkUpdateSqlBuilder.java | 76 ++++ .../{Action.java => BulkUpdateAction.java} | 14 +- .../bulkupdate/action/DeleteAction.java | 8 +- .../bulkupdate/action/UpdateAction.java | 39 +- .../comparison/StandardComparison.java | 112 ------ .../common/bulkupdate/query/Query.java | 83 ---- .../command/abstraction/ParentCommand.java | 69 ++-- .../common/commands/group/DeleteGroup.java | 8 +- .../commands/group/GroupParentCommand.java | 2 +- .../common/commands/group/GroupRename.java | 10 +- .../common/commands/log/LogGroupHistory.java | 33 +- .../common/commands/log/LogNotify.java | 5 +- .../common/commands/log/LogParentCommand.java | 47 +-- .../common/commands/log/LogRecent.java | 67 ++-- .../common/commands/log/LogSearch.java | 35 +- .../common/commands/log/LogTrackHistory.java | 34 +- .../common/commands/log/LogUserHistory.java | 29 +- .../commands/misc/BulkUpdateCommand.java | 22 +- .../common/commands/misc/SearchCommand.java | 12 +- .../commands/track/TrackParentCommand.java | 2 +- .../commands/user/UserParentCommand.java | 2 +- .../luckperms/common/config/ConfigKeys.java | 5 + .../comparison => filter}/Comparison.java | 66 ++-- .../comparison => filter}/Constraint.java | 42 +- .../common/filter/ConstraintFactory.java | 102 +++++ .../lucko/luckperms/common/filter/Filter.java | 59 +++ .../luckperms/common/filter/FilterField.java | 74 ++++ .../luckperms/common/filter/FilterList.java | 101 +++++ .../common/filter/PageParameters.java | 79 ++++ .../filter/mongo/ConstraintMongoBuilder.java | 87 +++++ .../filter/mongo/FilterMongoBuilder.java | 65 ++++ .../filter/sql/ConstraintSqlBuilder.java | 84 ++++ .../common/filter/sql/FilterSqlBuilder.java | 81 ++++ .../luckperms/common/locale/Message.java | 4 +- .../messaging/InternalMessagingService.java | 10 +- .../messaging/LuckPermsMessagingService.java | 23 +- .../node/matcher/ConstraintNodeMatcher.java | 14 +- .../node/matcher/StandardNodeMatchers.java | 39 +- .../common/sender/AbstractSender.java | 6 +- .../common/sender/SenderFactory.java | 4 +- .../luckperms/common/storage/Storage.java | 42 +- .../implementation/StorageImplementation.java | 6 +- .../file/AbstractConfigurateStorage.java | 9 +- .../implementation/file/FileActionLogger.java | 33 +- .../implementation/mongodb/MongoStorage.java | 191 ++++----- .../implementation/split/SplitStorage.java | 9 +- .../implementation/sql/SqlStorage.java | 98 +++-- .../sql/builder/AbstractSqlBuilder.java | 36 ++ .../builder}/PreparedStatementBuilder.java | 25 +- .../storage/misc/StorageCredentials.java | 6 + .../luckperms/common/util/AsyncInterface.java | 71 ++++ .../actionlog/ActionFilterMongoTest.java | 89 +++++ .../common/actionlog/ActionFilterSqlTest.java | 83 ++++ .../common/actionlog/ActionFilterTest.java | 226 +++++++++++ .../common/bulkupdate/BulkUpdateSqlTest.java | 143 +++++++ .../common/bulkupdate/BulkUpdateTest.java | 90 +---- .../ComparisonTest.java | 13 +- .../common/filter/FilterMongoTest.java | 118 ++++++ .../common/filter/FilterSqlTest.java | 115 ++++++ .../common/filter/PageParametersTest.java | 50 +++ .../common/storage/AbstractStorageTest.java | 361 ++++++++++++++++++ .../storage/ConfigurateStorageTest.java | 141 +++++++ .../common/storage/MongoStorageTest.java | 61 +++ .../common/storage/SqlStorageTest.java | 226 +---------- .../luckperms/common/util/PaginatedTest.java | 104 ----- .../listeners/FabricConnectionListener.java | 1 - .../messaging/PluginMessageMessenger.java | 3 - .../luckperms/fabric/model/MixinUser.java | 2 - .../app/integration/CommandExecutor.java | 12 +- .../app/integration/StandaloneSender.java | 36 +- ...ngletonPlayer.java => StandaloneUser.java} | 55 ++- .../standalone/LPStandalonePlugin.java | 4 +- .../standalone/StandaloneCommandManager.java | 10 +- .../standalone/StandaloneSenderFactory.java | 45 ++- .../stub/StandaloneContextManager.java | 24 +- .../standalone/CommandsIntegrationTest.java | 204 ++++++++-- .../standalone/StorageIntegrationTest.java | 6 +- .../standalone/WebEditorIntegrationTest.java | 4 +- .../standalone/utils/CommandTester.java | 48 ++- .../standalone/utils/TestPluginBootstrap.java | 70 +--- .../standalone/utils/TestSender.java | 113 ++++++ 108 files changed, 4363 insertions(+), 1640 deletions(-) create mode 100644 api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java create mode 100644 api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java create mode 100644 api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java create mode 100644 api/src/main/java/net/luckperms/api/util/Page.java delete mode 100644 common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java rename common/src/main/java/me/lucko/luckperms/common/{util/Paginated.java => actionlog/LogPage.java} (66%) create mode 100644 common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java rename common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/{Action.java => BulkUpdateAction.java} (80%) delete mode 100644 common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java delete mode 100644 common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java rename common/src/main/java/me/lucko/luckperms/common/{bulkupdate/comparison => filter}/Comparison.java (56%) rename common/src/main/java/me/lucko/luckperms/common/{bulkupdate/comparison => filter}/Constraint.java (60%) create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/Filter.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java rename common/src/main/java/me/lucko/luckperms/common/{bulkupdate => storage/implementation/sql/builder}/PreparedStatementBuilder.java (75%) create mode 100644 common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java rename common/src/test/java/me/lucko/luckperms/common/{bulkupdate => filter}/ComparisonTest.java (77%) create mode 100644 common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java delete mode 100644 common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java rename common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java => standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java (68%) rename standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/{SingletonPlayer.java => StandaloneUser.java} (56%) create mode 100644 standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java diff --git a/api/src/main/java/net/luckperms/api/LuckPerms.java b/api/src/main/java/net/luckperms/api/LuckPerms.java index 8e99fe610..6c851c45c 100644 --- a/api/src/main/java/net/luckperms/api/LuckPerms.java +++ b/api/src/main/java/net/luckperms/api/LuckPerms.java @@ -26,6 +26,7 @@ package net.luckperms.api; import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.actionlog.filter.ActionFilterFactory; import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextManager; import net.luckperms.api.event.EventBus; @@ -267,4 +268,14 @@ public interface LuckPerms { @Internal @NonNull NodeMatcherFactory getNodeMatcherFactory(); + /** + * Gets the {@link ActionFilterFactory}. + * + * @return the action filter factory + * @since 5.5 + */ + @Internal + @NonNull + ActionFilterFactory getActionFilterFactory(); + } diff --git a/api/src/main/java/net/luckperms/api/actionlog/Action.java b/api/src/main/java/net/luckperms/api/actionlog/Action.java index 82d3f2a5a..53260674c 100644 --- a/api/src/main/java/net/luckperms/api/actionlog/Action.java +++ b/api/src/main/java/net/luckperms/api/actionlog/Action.java @@ -28,6 +28,7 @@ package net.luckperms.api.actionlog; import net.luckperms.api.LuckPermsProvider; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.ApiStatus.NonExtendable; import java.time.Instant; import java.util.Optional; @@ -35,7 +36,10 @@ import java.util.UUID; /** * Represents a logged action. + * + *

API users should not implement this interface directly.

*/ +@NonExtendable public interface Action extends Comparable { /** @@ -81,6 +85,7 @@ public interface Action extends Comparable { /** * Represents the source of an action. */ + @NonExtendable interface Source { /** @@ -102,6 +107,7 @@ public interface Action extends Comparable { /** * Represents the target of an action. */ + @NonExtendable interface Target { /** @@ -126,7 +132,7 @@ public interface Action extends Comparable { @NonNull Type getType(); /** - * Represents the type of a {@link Target}. + * Represents the type of {@link Target}. */ enum Type { USER, GROUP, TRACK diff --git a/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java b/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java index bba6eb457..131ab3a5c 100644 --- a/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java +++ b/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java @@ -25,6 +25,7 @@ package net.luckperms.api.actionlog; +import net.luckperms.api.actionlog.filter.ActionFilter; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; @@ -40,7 +41,11 @@ import java.util.UUID; * You can add to the log using the {@link ActionLogger}, and then request an updated copy.

* *

All methods are thread safe, and return immutable and thread safe collections.

+ * + * @deprecated Use {@link ActionLogger#queryActions(ActionFilter)} or + * {@link ActionLogger#queryActions(ActionFilter, int, int)} instead. */ +@Deprecated public interface ActionLog { /** diff --git a/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java b/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java index 0f4bf7878..8b8eb6dca 100644 --- a/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java +++ b/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java @@ -25,9 +25,11 @@ package net.luckperms.api.actionlog; -import net.luckperms.api.messaging.MessagingService; +import net.luckperms.api.actionlog.filter.ActionFilter; +import net.luckperms.api.util.Page; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -46,43 +48,78 @@ public interface ActionLogger { * Gets a {@link ActionLog} instance from the plugin storage. * * @return a log instance + * @deprecated Use {@link #queryActions(ActionFilter)} or {@link #queryActions(ActionFilter, int, int)} instead. These methods + * are more efficient (they don't load the full action log into memory) and allow for pagination. */ + @Deprecated @NonNull CompletableFuture getLog(); /** - * Submits a log entry to the plugin to be handled. + * Gets all actions from the action log matching the given {@code filter}. * - *

This method submits the log to the storage provider and broadcasts - * it.

+ *

If the filter is {@code null}, all actions will be returned.

* - *

It is therefore roughly equivalent to calling - * {@link #submitToStorage(Action)} and {@link #broadcastAction(Action)}, - * however, using this method is preferred to making the calls individually.

+ *

Unlike {@link #queryActions(ActionFilter, int, int)}, this method does not implement any pagination and will return + * all entries at once.

* - *

If you want to submit a log entry but don't know which method to pick, + * @param filter the filter, optional + * @return the actions + * @since 5.5 + */ + @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter); + + /** + * Gets a page of actions from the action log matching the given {@code filter}. + * + *

If the filter is {@code null}, all actions will be returned.

+ * + * @param filter the filter, optional + * @param pageSize the size of the page + * @param pageNumber the page number + * @return the page of actions + * @since 5.5 + */ + @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter, int pageSize, int pageNumber); + + /** + * Submits a logged action to LuckPerms. + * + *

This method submits the action to the storage provider to be persisted in the action log. + * It also broadcasts it to administrator players on the current instance and to admins on other + * connected servers if a messaging service is configured.

+ * + *

It is roughly equivalent to calling + * {@link #submitToStorage(Action)} followed by {@link #broadcastAction(Action)}, + * however using this method is preferred to making the calls individually.

+ * + *

If you want to submit an action log entry but don't know which method to pick, * use this one.

* * @param entry the entry to submit - * @return a future which will complete when the action is done + * @return a future which will complete when the action is submitted */ @NonNull CompletableFuture submit(@NonNull Action entry); /** - * Submits a log entry to the plugins storage handler. + * Submits a logged action to LuckPerms and persists it in the storage backend. + * + *

This method does not broadcast the action or send it through the messaging service.

* * @param entry the entry to submit - * @return a future which will complete when the action is done + * @return a future which will complete when the action is submitted */ @NonNull CompletableFuture submitToStorage(@NonNull Action entry); /** - * Submits a log entry to the plugins log broadcasting handler. + * Submits a logged action to LuckPerms and broadcasts it to administrators. * - *

If enabled, this method will also dispatch the log entry via the - * plugins {@link MessagingService}.

+ *

The broadcast is made to administrator players on the current instance + * and to admins on other connected servers if a messaging service is configured.

+ * + *

This method does not save the action to the plugin storage backend.

* * @param entry the entry to submit - * @return a future which will complete when the action is done + * @return a future which will complete when the action is broadcasted */ @NonNull CompletableFuture broadcastAction(@NonNull Action entry); diff --git a/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java new file mode 100644 index 000000000..6614cbb34 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java @@ -0,0 +1,114 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.actionlog.filter; + +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.actionlog.Action; +import org.jetbrains.annotations.ApiStatus.NonExtendable; + +import java.util.UUID; +import java.util.function.Predicate; + +/** + * A predicate filter which matches certain {@link Action}s. + * + *

API users should not implement this interface directly.

+ * + * @since 5.5 + */ +@NonExtendable +public interface ActionFilter extends Predicate { + + /** + * Gets an {@link ActionFilter} which matches any action. + * + * @return the matcher + */ + static ActionFilter any() { + return LuckPermsProvider.get().getActionFilterFactory().any(); + } + + /** + * Gets an {@link ActionFilter} which matches actions with a specific source user. + * + * @param uniqueId the source user unique id + * @return the matcher + */ + static ActionFilter source(UUID uniqueId) { + return LuckPermsProvider.get().getActionFilterFactory().source(uniqueId); + } + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific user. + * + * @param uniqueId the target user unique id + * @return the matcher + */ + static ActionFilter user(UUID uniqueId) { + return LuckPermsProvider.get().getActionFilterFactory().user(uniqueId); + } + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific group. + * + * @param name the target group name + * @return the matcher + */ + static ActionFilter group(String name) { + return LuckPermsProvider.get().getActionFilterFactory().group(name); + } + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific track. + * + * @param name the target track name + * @return the matcher + */ + static ActionFilter track(String name) { + return LuckPermsProvider.get().getActionFilterFactory().track(name); + } + + /** + * Gets an {@link ActionFilter} which matches actions which contain a specific search query in the source name, + * target name or description. + * + * @param query the search query + * @return the matcher + */ + static ActionFilter search(String query) { + return LuckPermsProvider.get().getActionFilterFactory().search(query); + } + + /** + * Tests to see if the given {@link Action} matches the filter. + * + * @param action the action to test + * @return true if the action matched + */ + @Override + boolean test(Action action); + +} diff --git a/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java new file mode 100644 index 000000000..31e699ac5 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java @@ -0,0 +1,88 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.actionlog.filter; + +import org.jetbrains.annotations.ApiStatus.Internal; + +import java.util.UUID; + +/** + * A factory which creates {@link ActionFilter}s. + * + * @since 5.5 + */ +@Internal +public interface ActionFilterFactory { + + /** + * Gets an {@link ActionFilter} which matches any action. + * + * @return the matcher + */ + ActionFilter any(); + + /** + * Gets an {@link ActionFilter} which matches actions with a specific source user. + * + * @param uniqueId the source user unique id + * @return the matcher + */ + ActionFilter source(UUID uniqueId); + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific user. + * + * @param uniqueId the target user unique id + * @return the matcher + */ + ActionFilter user(UUID uniqueId); + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific group. + * + * @param name the target group name + * @return the matcher + */ + ActionFilter group(String name); + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific track. + * + * @param name the target track name + * @return the matcher + */ + ActionFilter track(String name); + + /** + * Gets an {@link ActionFilter} which matches actions which contain a specific search query in the source name, + * target name or description. + * + * @param query the search query + * @return the matcher + */ + ActionFilter search(String query); + +} diff --git a/api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java b/api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java new file mode 100644 index 000000000..2eebf3be8 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java @@ -0,0 +1,29 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * {@link net.luckperms.api.actionlog.Action} filters. + */ +package net.luckperms.api.actionlog.filter; \ No newline at end of file diff --git a/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java b/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java index 34dc3f66a..c156eb0ce 100644 --- a/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java +++ b/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.sync; import net.luckperms.api.event.LuckPermsEvent; -import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/util/Page.java b/api/src/main/java/net/luckperms/api/util/Page.java new file mode 100644 index 000000000..3ecf589c4 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/util/Page.java @@ -0,0 +1,53 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.util; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; + +/** + * Represents a page of entries. + * + * @since 5.5 + */ +public interface Page { + + /** + * Gets the entries on this page. + * + * @return the entries + */ + @NonNull List entries(); + + /** + * Gets the total/overall number of entries (not just the number of entries on this page). + * + * @return the total number of entries + */ + int overallSize(); + +} diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java index 977001b6c..9e599a304 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java @@ -37,7 +37,6 @@ import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.event.server.RemoteServerCommandEvent; import org.bukkit.event.server.ServerCommandEvent; -import java.util.Locale; import java.util.regex.Pattern; public class BukkitPlatformListener implements Listener { diff --git a/common/build.gradle b/common/build.gradle index d3e5e9966..9db5a047f 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -4,7 +4,11 @@ plugins { } test { - useJUnitPlatform {} + useJUnitPlatform { + if (!project.hasProperty('dockerTests')) { + excludeTags 'docker' + } + } } jacocoTestReport { @@ -15,9 +19,14 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1' + testImplementation "org.testcontainers:junit-jupiter:1.19.8" testImplementation 'org.mockito:mockito-core:5.11.0' testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' testImplementation 'com.h2database:h2:2.1.214' + testImplementation 'org.mongodb:mongodb-driver-legacy:4.5.0' + testImplementation 'org.spongepowered:configurate-yaml:3.7.2' + testImplementation 'org.spongepowered:configurate-hocon:3.7.2' + testImplementation 'me.lucko.configurate:configurate-toml:3.7' api project(':api') api 'org.checkerframework:checker-qual:3.12.0' diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java deleted file mode 100644 index f5c744988..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package me.lucko.luckperms.common.actionlog; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSortedSet; -import me.lucko.luckperms.common.util.ImmutableCollectors; -import net.luckperms.api.actionlog.Action; - -import java.util.ArrayList; -import java.util.List; -import java.util.SortedSet; -import java.util.UUID; - -public class Log { - private static final Log EMPTY = new Log(ImmutableList.of()); - - public static Builder builder() { - return new Builder(); - } - - public static Log empty() { - return EMPTY; - } - - private final SortedSet content; - - Log(List content) { - this.content = ImmutableSortedSet.copyOf(content); - } - - public SortedSet getContent() { - return this.content; - } - - public SortedSet getContent(UUID actor) { - return this.content.stream() - .filter(e -> e.getSource().getUniqueId().equals(actor)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getUserHistory(UUID uniqueId) { - return this.content.stream() - .filter(e -> e.getTarget().getType() == Action.Target.Type.USER) - .filter(e -> e.getTarget().getUniqueId().isPresent() && e.getTarget().getUniqueId().get().equals(uniqueId)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getGroupHistory(String name) { - return this.content.stream() - .filter(e -> e.getTarget().getType() == Action.Target.Type.GROUP) - .filter(e -> e.getTarget().getName().equals(name)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getTrackHistory(String name) { - return this.content.stream() - .filter(e -> e.getTarget().getType() == Action.Target.Type.TRACK) - .filter(e -> e.getTarget().getName().equals(name)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getSearch(String query) { - return this.content.stream() - .filter(e -> e.matchesSearch(query)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public static class Builder { - private final List content = new ArrayList<>(); - - public Builder add(LoggedAction e) { - this.content.add(e); - return this; - } - - public Log build() { - if (this.content.isEmpty()) { - return EMPTY; - } - return new Log(this.content); - } - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java index c4ffd5b45..befa7ab2a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java @@ -29,12 +29,14 @@ import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.commands.log.LogNotify; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import net.luckperms.api.event.log.LogBroadcastEvent; import net.luckperms.api.event.log.LogNotifyEvent; import java.util.Collection; +import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; public class LogDispatcher { @@ -64,7 +66,12 @@ public class LogDispatcher { return !this.plugin.getEventDispatcher().dispatchLogBroadcast(cancelled, entry, origin); } - private void broadcast(LoggedAction entry, LogNotifyEvent.Origin origin, Sender sender) { + // broadcast the entry to online players + private void broadcast(LoggedAction entry, LogBroadcastEvent.Origin broadcastOrigin, LogNotifyEvent.Origin origin, Sender sender) { + if (!shouldBroadcast(entry, broadcastOrigin)) { + return; + } + this.plugin.getOnlineSenders() .filter(CommandPermission.LOG_NOTIFY::isAuthorized) .filter(s -> { @@ -74,41 +81,46 @@ public class LogDispatcher { .forEach(s -> Message.LOG.send(s, entry)); } - public void dispatch(LoggedAction entry, Sender sender) { + // log the entry to storage + public CompletableFuture logToStorage(LoggedAction entry) { if (!this.plugin.getEventDispatcher().dispatchLogPublish(false, entry)) { - this.plugin.getStorage().logAction(entry); + return this.plugin.getStorage().logAction(entry); + } else { + return CompletableFuture.completedFuture(null); } + } - this.plugin.getMessagingService().ifPresent(service -> service.pushLog(entry)); - - if (shouldBroadcast(entry, LogBroadcastEvent.Origin.LOCAL)) { - broadcast(entry, LogNotifyEvent.Origin.LOCAL, sender); + // log the entry to messaging + public CompletableFuture logToMessaging(LoggedAction entry) { + InternalMessagingService messagingService = this.plugin.getMessagingService().orElse(null); + if (messagingService != null) { + return messagingService.pushLog(entry); + } else { + return CompletableFuture.completedFuture(null); } } + // log the entry to storage and messaging, and broadcast it to online players + private CompletableFuture dispatch(LoggedAction entry, Sender sender, LogBroadcastEvent.Origin broadcastOrigin, LogNotifyEvent.Origin origin) { + CompletableFuture storageFuture = logToStorage(entry); + CompletableFuture messagingFuture = logToMessaging(entry); + broadcast(entry, broadcastOrigin, origin, sender); + return CompletableFuture.allOf(storageFuture, messagingFuture); + } + + public CompletableFuture dispatch(LoggedAction entry, Sender sender) { + return dispatch(entry, sender, LogBroadcastEvent.Origin.LOCAL, LogNotifyEvent.Origin.LOCAL); + } + + public CompletableFuture dispatchFromApi(LoggedAction entry) { + return dispatch(entry, null, LogBroadcastEvent.Origin.LOCAL_API, LogNotifyEvent.Origin.LOCAL_API); + } + public void broadcastFromApi(LoggedAction entry) { - this.plugin.getMessagingService().ifPresent(extendedMessagingService -> extendedMessagingService.pushLog(entry)); - - if (shouldBroadcast(entry, LogBroadcastEvent.Origin.LOCAL_API)) { - broadcast(entry, LogNotifyEvent.Origin.LOCAL_API, null); - } + broadcast(entry, LogBroadcastEvent.Origin.LOCAL_API, LogNotifyEvent.Origin.LOCAL_API, null); } - public void dispatchFromApi(LoggedAction entry) { - if (!this.plugin.getEventDispatcher().dispatchLogPublish(false, entry)) { - try { - this.plugin.getStorage().logAction(entry).get(); - } catch (Exception e) { - this.plugin.getLogger().warn("Error whilst storing action", e); - } - } - - broadcastFromApi(entry); - } - - public void dispatchFromRemote(LoggedAction entry) { - if (shouldBroadcast(entry, LogBroadcastEvent.Origin.REMOTE)) { - broadcast(entry, LogNotifyEvent.Origin.REMOTE, null); - } + public void broadcastFromRemote(LoggedAction entry) { + broadcast(entry, LogBroadcastEvent.Origin.REMOTE, LogNotifyEvent.Origin.REMOTE, null); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/Paginated.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogPage.java similarity index 66% rename from common/src/main/java/me/lucko/luckperms/common/util/Paginated.java rename to common/src/main/java/me/lucko/luckperms/common/actionlog/LogPage.java index 7feb91712..b9ea7c008 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/Paginated.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogPage.java @@ -23,54 +23,50 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.util; +package me.lucko.luckperms.common.actionlog; import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.filter.PageParameters; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Objects; -/** - * A simple pagination utility - * - * @param the element type - */ -public class Paginated { - private final List content; - - public Paginated(Collection content) { - this.content = ImmutableList.copyOf(content); +public class LogPage { + public static LogPage of(List content, @Nullable PageParameters params, int totalEntries) { + return new LogPage(content, params, totalEntries); } - public List getContent() { + private final List content; + private final @Nullable PageParameters params; + private final int totalEntries; + + LogPage(List content, @Nullable PageParameters params, int totalEntries) { + this.content = ImmutableList.copyOf(content); + this.params = params; + this.totalEntries = totalEntries; + } + + public List getContent() { return this.content; } - public int getMaxPages(int entriesPerPage) { - return (int) Math.ceil((double) this.content.size() / (double) entriesPerPage); + public List> getNumberedContent() { + int startIndex = this.params != null + ? this.params.pageSize() * (this.params.pageNumber() - 1) + : 0; + + List> numberedContent = new ArrayList<>(); + for (int i = 0; i < this.content.size(); i++) { + int index = startIndex + i + 1; + numberedContent.add(new Entry<>(index, this.content.get(i))); + } + return numberedContent; } - public List> getPage(int pageNo, int pageSize) { - if (pageNo < 1) { - throw new IllegalArgumentException("pageNo cannot be less than 1: " + pageNo); - } - - int first = (pageNo - 1) * pageSize; - if (this.content.size() <= first) { - throw new IllegalStateException("Content does not contain that many elements. (requested page: " + pageNo + - ", page size: " + pageSize + ", page first index: " + first + ", content size: " + this.content.size() + ")"); - } - - int last = first + pageSize - 1; - List> out = new ArrayList<>(pageSize); - - for (int i = first; i <= last && i < this.content.size(); i++) { - out.add(new Entry<>(i + 1, this.content.get(i))); - } - - return out; + public int getTotalEntries() { + return this.totalEntries; } public static final class Entry { diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java index defeb575c..00ce2c64d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java @@ -26,6 +26,7 @@ package me.lucko.luckperms.common.actionlog; import com.google.common.base.Strings; +import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.PermissionHolder; @@ -44,10 +45,10 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; /** * An implementation of {@link Action} and {@link Action.Builder}, @@ -123,15 +124,11 @@ public class LoggedAction implements Action { return ActionComparator.INSTANCE.compare(this, other); } - public boolean matchesSearch(String query) { - query = Objects.requireNonNull(query, "query").toLowerCase(Locale.ROOT); - return this.source.name.toLowerCase(Locale.ROOT).contains(query) || - this.target.name.toLowerCase(Locale.ROOT).contains(query) || - this.description.toLowerCase(Locale.ROOT).contains(query); - } - public void submit(LuckPermsPlugin plugin, Sender sender) { - plugin.getLogDispatcher().dispatch(this, sender); + CompletableFuture future = plugin.getLogDispatcher().dispatch(this, sender); + if (plugin.getConfiguration().get(ConfigKeys.LOG_SYNCHRONOUSLY_IN_COMMANDS)) { + future.join(); + } } @Override @@ -393,14 +390,14 @@ public class LoggedAction implements Action { } } - public static char getTypeCharacter(Target.Type type) { + public static String getTypeString(Target.Type type) { switch (type) { case USER: - return 'U'; + return "U"; case GROUP: - return 'G'; + return "G"; case TRACK: - return 'T'; + return "T"; default: throw new AssertionError(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java new file mode 100644 index 000000000..b4887e701 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java @@ -0,0 +1,60 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog.filter; + +import me.lucko.luckperms.common.filter.FilterField; +import net.luckperms.api.actionlog.Action; + +import java.util.UUID; + +public final class ActionFields { + + public static final FilterField SOURCE_UNIQUE_ID = FilterField.named( + "SOURCE_UNIQUE_ID", + action -> action.getSource().getUniqueId() + ); + public static final FilterField SOURCE_NAME = FilterField.named( + "SOURCE_NAME", + action -> action.getSource().getName() + ); + public static final FilterField TARGET_TYPE = FilterField.named( + "TARGET_TYPE", + action -> action.getTarget().getType() + ); + public static final FilterField TARGET_UNIQUE_ID = FilterField.named( + "TARGET_UNIQUE_ID", + action -> action.getTarget().getUniqueId().orElse(null) + ); + public static final FilterField TARGET_NAME = FilterField.named( + "TARGET_NAME", + action -> action.getTarget().getName() + ); + public static final FilterField DESCRIPTION = FilterField.named( + "DESCRIPTION", + action -> action.getDescription() + ); + +} \ No newline at end of file diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java new file mode 100644 index 000000000..d82ec017e --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java @@ -0,0 +1,70 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog.filter; + +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.mongo.FilterMongoBuilder; +import net.luckperms.api.actionlog.Action; + +import java.util.UUID; + +public final class ActionFilterMongoBuilder extends FilterMongoBuilder { + public static final ActionFilterMongoBuilder INSTANCE = new ActionFilterMongoBuilder(); + + private ActionFilterMongoBuilder() { + + } + + @Override + public String mapFieldName(FilterField field) { + if (field == ActionFields.SOURCE_UNIQUE_ID) { + return "source.uniqueId"; + } else if (field == ActionFields.SOURCE_NAME) { + return "source.name"; + } else if (field == ActionFields.TARGET_TYPE) { + return "target.type"; + } else if (field == ActionFields.TARGET_UNIQUE_ID) { + return "target.uniqueId"; + } else if (field == ActionFields.TARGET_NAME) { + return "target.name"; + } else if (field == ActionFields.DESCRIPTION) { + return "description"; + } + throw new AssertionError(field); + } + + @Override + public Object mapConstraintValue(Object value) { + if (value instanceof String | value instanceof UUID) { + return value; + } else if (value instanceof Action.Target.Type) { + return ((Action.Target.Type) value).name(); + } else { + throw new IllegalArgumentException("Don't know how to map value with type: " + value.getClass().getName()); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java new file mode 100644 index 000000000..e7a71ce9a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java @@ -0,0 +1,69 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog.filter; + +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.sql.FilterSqlBuilder; +import net.luckperms.api.actionlog.Action; + +import java.util.UUID; + +public class ActionFilterSqlBuilder extends FilterSqlBuilder { + + @Override + public void visitFieldName(FilterField field) { + if (field == ActionFields.SOURCE_UNIQUE_ID) { + this.builder.append("actor_uuid"); + } else if (field == ActionFields.SOURCE_NAME) { + this.builder.append("actor_name"); + } else if (field == ActionFields.TARGET_TYPE) { + this.builder.append("type"); + } else if (field == ActionFields.TARGET_UNIQUE_ID) { + this.builder.append("acted_uuid"); + } else if (field == ActionFields.TARGET_NAME) { + this.builder.append("acted_name"); + } else if (field == ActionFields.DESCRIPTION) { + this.builder.append("action"); + } else { + throw new AssertionError(field); + } + } + + @Override + public void visitConstraintValue(Object value) { + if (value instanceof String) { + this.builder.variable(((String) value)); + } else if (value instanceof UUID) { + this.builder.variable(value.toString()); + } else if (value instanceof Action.Target.Type) { + this.builder.variable(LoggedAction.getTypeString((Action.Target.Type) value)); + } else { + throw new IllegalArgumentException("Don't know how to write value with type: " + value.getClass().getName()); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java new file mode 100644 index 000000000..f1513a2ca --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java @@ -0,0 +1,110 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog.filter; + +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.ConstraintFactory; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.actionlog.Action.Target; + +import java.util.UUID; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public final class ActionFilters { + private ActionFilters() {} + + // all actions + public static FilterList all() { + return FilterList.empty(); + } + + // all actions performed by a given source (actor) + public static FilterList source(UUID uniqueId) { + return FilterList.and( + ActionFields.SOURCE_UNIQUE_ID.isEqualTo(uniqueId, ConstraintFactory.UUIDS) + ); + } + + // all actions affecting a given user + public static FilterList user(UUID uniqueId) { + return FilterList.and( + ActionFields.TARGET_TYPE.isEqualTo(Target.Type.USER, TARGET_TYPE_CONSTRAINT_FACTORY), + ActionFields.TARGET_UNIQUE_ID.isEqualTo(uniqueId, ConstraintFactory.UUIDS) + ); + } + + // all actions affecting a given group + public static FilterList group(String name) { + return FilterList.and( + ActionFields.TARGET_TYPE.isEqualTo(Target.Type.GROUP, TARGET_TYPE_CONSTRAINT_FACTORY), + ActionFields.TARGET_NAME.isEqualTo(name, ConstraintFactory.STRINGS) + ); + } + + // all actions affecting a given track + public static FilterList track(String name) { + return FilterList.and( + ActionFields.TARGET_TYPE.isEqualTo(Target.Type.TRACK, TARGET_TYPE_CONSTRAINT_FACTORY), + ActionFields.TARGET_NAME.isEqualTo(name, ConstraintFactory.STRINGS) + ); + } + + // all actions matching the given search query + public static FilterList search(String query) { + return FilterList.or( + ActionFields.SOURCE_NAME.isSimilarTo("%" + query + "%", ConstraintFactory.STRINGS), + ActionFields.TARGET_NAME.isSimilarTo("%" + query + "%", ConstraintFactory.STRINGS), + ActionFields.DESCRIPTION.isSimilarTo("%" + query + "%", ConstraintFactory.STRINGS) + ); + } + + private static final ConstraintFactory TARGET_TYPE_CONSTRAINT_FACTORY = new ConstraintFactory() { + @Override + public Predicate equal(Target.Type value) { + return value::equals; + } + + @Override + public Predicate notEqual(Target.Type value) { + return string -> !value.equals(string); + } + + @Override + public Predicate similar(Target.Type value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return type -> pattern.matcher(type.toString()).matches(); + } + + @Override + public Predicate notSimilar(Target.Type value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return type -> !pattern.matcher(type.toString()).matches(); + } + }; + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java b/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java index 7da93b90e..a68ef671a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.common.api; +import me.lucko.luckperms.common.api.implementation.ApiActionFilterFactory; import me.lucko.luckperms.common.api.implementation.ApiActionLogger; import me.lucko.luckperms.common.api.implementation.ApiContextManager; import me.lucko.luckperms.common.api.implementation.ApiGroupManager; @@ -47,6 +48,7 @@ import me.lucko.luckperms.common.plugin.logging.PluginLogger; import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.actionlog.filter.ActionFilterFactory; import net.luckperms.api.context.ContextManager; import net.luckperms.api.messaging.MessagingService; import net.luckperms.api.messenger.MessengerProvider; @@ -225,4 +227,8 @@ public class LuckPermsApiProvider implements LuckPerms { return ApiNodeMatcherFactory.INSTANCE; } + @Override + public @NonNull ActionFilterFactory getActionFilterFactory() { + return ApiActionFilterFactory.INSTANCE; + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java new file mode 100644 index 000000000..65a4be9ea --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java @@ -0,0 +1,47 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.api.implementation; + +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.actionlog.filter.ActionFilter; + +public class ApiActionFilter implements ActionFilter { + private final FilterList filter; + + public ApiActionFilter(FilterList filter) { + this.filter = filter; + } + + @Override + public boolean test(Action action) { + return this.filter.evaluate(action); + } + + public FilterList getFilter() { + return this.filter; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java new file mode 100644 index 000000000..5bd9a60c6 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java @@ -0,0 +1,77 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.api.implementation; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import net.luckperms.api.actionlog.filter.ActionFilter; +import net.luckperms.api.actionlog.filter.ActionFilterFactory; + +import java.util.Objects; +import java.util.UUID; + +public final class ApiActionFilterFactory implements ActionFilterFactory { + public static final ApiActionFilterFactory INSTANCE = new ApiActionFilterFactory(); + + private ApiActionFilterFactory() { + + } + + @Override + public ActionFilter any() { + return new ApiActionFilter(ActionFilters.all()); + } + + @Override + public ActionFilter source(UUID uniqueId) { + Objects.requireNonNull(uniqueId, "uniqueId"); + return new ApiActionFilter(ActionFilters.source(uniqueId)); + } + + @Override + public ActionFilter user(UUID uniqueId) { + Objects.requireNonNull(uniqueId, "uniqueId"); + return new ApiActionFilter(ActionFilters.user(uniqueId)); + } + + @Override + public ActionFilter group(String name) { + Objects.requireNonNull(name, "name"); + return new ApiActionFilter(ActionFilters.group(name)); + } + + @Override + public ActionFilter track(String name) { + Objects.requireNonNull(name, "name"); + return new ApiActionFilter(ActionFilters.track(name)); + } + + @Override + public ActionFilter search(String query) { + Objects.requireNonNull(query, "query"); + return new ApiActionFilter(ActionFilters.search(query)); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java index 6bacec94e..ef8e7df79 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java @@ -25,50 +25,63 @@ package me.lucko.luckperms.common.api.implementation; -import me.lucko.luckperms.common.actionlog.Log; -import me.lucko.luckperms.common.api.ApiUtils; +import com.google.common.collect.ImmutableSortedSet; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.util.ImmutableCollectors; import net.luckperms.api.actionlog.Action; import net.luckperms.api.actionlog.ActionLog; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; import java.util.Objects; import java.util.SortedSet; import java.util.UUID; -@SuppressWarnings({"unchecked", "rawtypes"}) +@Deprecated public class ApiActionLog implements ActionLog { - private final Log handle; + private final SortedSet content; - public ApiActionLog(Log handle) { - this.handle = handle; + public ApiActionLog(List content) { + this.content = ImmutableSortedSet.copyOf(content); } @Override public @NonNull SortedSet getContent() { - return (SortedSet) this.handle.getContent(); + return this.content; } @Override public @NonNull SortedSet getContent(@NonNull UUID actor) { Objects.requireNonNull(actor, "actor"); - return (SortedSet) this.handle.getContent(actor); + return this.content.stream() + .filter(e -> e.getSource().getUniqueId().equals(actor)) + .collect(ImmutableCollectors.toSortedSet()); } @Override public @NonNull SortedSet getUserHistory(@NonNull UUID uniqueId) { Objects.requireNonNull(uniqueId, "uuid"); - return (SortedSet) this.handle.getUserHistory(uniqueId); + return this.content.stream() + .filter(e -> e.getTarget().getType() == Action.Target.Type.USER) + .filter(e -> e.getTarget().getUniqueId().isPresent() && e.getTarget().getUniqueId().get().equals(uniqueId)) + .collect(ImmutableCollectors.toSortedSet()); } @Override public @NonNull SortedSet getGroupHistory(@NonNull String name) { Objects.requireNonNull(name, "name"); - return (SortedSet) this.handle.getGroupHistory(ApiUtils.checkName(name)); + return this.content.stream() + .filter(e -> e.getTarget().getType() == Action.Target.Type.GROUP) + .filter(e -> e.getTarget().getName().equals(name)) + .collect(ImmutableCollectors.toSortedSet()); } @Override public @NonNull SortedSet getTrackHistory(@NonNull String name) { Objects.requireNonNull(name, "name"); - return (SortedSet) this.handle.getTrackHistory(ApiUtils.checkName(name)); + return this.content.stream() + .filter(e -> e.getTarget().getType() == Action.Target.Type.TRACK) + .filter(e -> e.getTarget().getName().equals(name)) + .collect(ImmutableCollectors.toSortedSet()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java index b596e1793..c571d5e04 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java @@ -25,13 +25,22 @@ package me.lucko.luckperms.common.api.implementation; +import me.lucko.luckperms.common.actionlog.LogDispatcher; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import net.luckperms.api.actionlog.Action; import net.luckperms.api.actionlog.ActionLog; import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.actionlog.filter.ActionFilter; +import net.luckperms.api.util.Page; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; public class ApiActionLogger implements ActionLogger { @@ -47,22 +56,66 @@ public class ApiActionLogger implements ActionLogger { } @Override + @Deprecated public @NonNull CompletableFuture getLog() { - return this.plugin.getStorage().getLog().thenApply(ApiActionLog::new); + return this.plugin.getStorage().getLogPage(ActionFilters.all(), null) + .thenApply(result -> new ApiActionLog(result.getContent())); + } + + @Override + public @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter) { + return this.plugin.getStorage().getLogPage(getFilterList(filter), null).thenApply(ActionPage::new).thenApply(Page::entries); + } + + @Override + public @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter, int pageSize, int pageNumber) { + return this.plugin.getStorage().getLogPage(getFilterList(filter), new PageParameters(pageSize, pageNumber)).thenApply(ActionPage::new); } @Override public @NonNull CompletableFuture submit(@NonNull Action entry) { - return CompletableFuture.runAsync(() -> this.plugin.getLogDispatcher().dispatchFromApi((LoggedAction) entry), this.plugin.getBootstrap().getScheduler().async()); + return this.plugin.getLogDispatcher().dispatchFromApi((LoggedAction) entry); } @Override public @NonNull CompletableFuture submitToStorage(@NonNull Action entry) { - return this.plugin.getStorage().logAction(entry); + return this.plugin.getLogDispatcher().logToStorage((LoggedAction) entry); } @Override public @NonNull CompletableFuture broadcastAction(@NonNull Action entry) { - return CompletableFuture.runAsync(() -> this.plugin.getLogDispatcher().broadcastFromApi((LoggedAction) entry), this.plugin.getBootstrap().getScheduler().async()); + LogDispatcher dispatcher = this.plugin.getLogDispatcher(); + + CompletableFuture messagingFuture = dispatcher.logToStorage(((LoggedAction) entry)); + dispatcher.broadcastFromApi(((LoggedAction) entry)); + return messagingFuture; + } + + private static FilterList getFilterList(ActionFilter filter) { + Objects.requireNonNull(filter, "filter"); + if (filter instanceof ApiActionFilter) { + return ((ApiActionFilter) filter).getFilter(); + } else { + throw new IllegalArgumentException("Unknown filter type: " + filter.getClass()); + } + } + + private static final class ActionPage implements Page { + private final LogPage page; + + private ActionPage(LogPage page) { + this.page = page; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public @NonNull List entries() { + return (List) this.page.getContent(); + } + + @Override + public int overallSize() { + return this.page.getTotalEntries(); + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java index 33375355d..5740ccde1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java @@ -25,14 +25,13 @@ package me.lucko.luckperms.common.bulkupdate; -import me.lucko.luckperms.common.bulkupdate.action.Action; -import me.lucko.luckperms.common.bulkupdate.query.Query; +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.filter.FilterList; import me.lucko.luckperms.common.model.HolderType; import net.luckperms.api.node.Node; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Set; @@ -46,35 +45,30 @@ public final class BulkUpdate { private final DataType dataType; // the action to apply to the data which matches the constraints - private final Action action; + private final BulkUpdateAction action; - // a set of constraints which data must match to be acted upon - private final List queries; + // a set of filters which data must match to be acted upon + private final FilterList filters; // update statistics of the operation (number of nodes, users and groups affected) private final BulkUpdateStatistics statistics = new BulkUpdateStatistics(); private final boolean trackStatistics; - public BulkUpdate(DataType dataType, Action action, List queries, boolean trackStatistics) { + public BulkUpdate(DataType dataType, BulkUpdateAction action, FilterList filters, boolean trackStatistics) { this.dataType = dataType; this.action = action; - this.queries = queries; + this.filters = filters; this.trackStatistics = trackStatistics; } /** - * Check to see if a Node instance satisfies the constrints of this query + * Check to see if a Node instance satisfies the constraints of this query * * @param node the node to check * @return true if satisfied */ - public boolean satisfiesConstraints(Node node) { - for (Query query : this.queries) { - if (!query.isSatisfiedBy(node)) { - return false; - } - } - return true; + public boolean satisfiesFilters(Node node) { + return this.filters.evaluate(node); } /** @@ -84,7 +78,7 @@ public final class BulkUpdate { * @return the transformed node, or null if the node should be deleted */ private Node apply(Node node) { - if (!satisfiesConstraints(node)) { + if (!satisfiesFilters(node)) { return node; // make no change } @@ -129,63 +123,16 @@ public final class BulkUpdate { return results; } - /** - * Converts this {@link BulkUpdate} to SQL syntax - * - * @return this query in SQL form - */ - public PreparedStatementBuilder buildAsSql() { - // DELETE FROM {table} WHERE ... - // UPDATE {table} SET ... WHERE ... - - PreparedStatementBuilder builder = new PreparedStatementBuilder(); - - // add the action - // (DELETE FROM or UPDATE) - this.action.appendSql(builder); - - return appendConstraintsAsSql(builder); - } - - /** - * Appends the constraints of this {@link BulkUpdate} to the provided statement builder in SQL syntax - * - * @param builder the statement builder to append the constraints to - * @return the same statement builder provided as input - */ - public PreparedStatementBuilder appendConstraintsAsSql(PreparedStatementBuilder builder) { - - // if there are no constraints, just return without a WHERE clause - if (this.queries.isEmpty()) { - return builder; - } - - // append constraints - builder.append(" WHERE"); - for (int i = 0; i < this.queries.size(); i++) { - Query query = this.queries.get(i); - - builder.append(" "); - if (i != 0) { - builder.append("AND "); - } - - query.appendSql(builder); - } - - return builder; - } - public DataType getDataType() { return this.dataType; } - public Action getAction() { + public BulkUpdateAction getAction() { return this.action; } - public List getQueries() { - return this.queries; + public FilterList getFilters() { + return this.filters; } public boolean isTrackingStatistics() { @@ -204,12 +151,12 @@ public final class BulkUpdate { return this.getDataType() == that.getDataType() && Objects.equals(this.getAction(), that.getAction()) && - Objects.equals(this.getQueries(), that.getQueries()); + Objects.equals(this.getFilters(), that.getFilters()); } @Override public int hashCode() { - return Objects.hash(getDataType(), getAction(), getQueries(), isTrackingStatistics()); + return Objects.hash(getDataType(), getAction(), getFilters(), isTrackingStatistics()); } @Override @@ -217,7 +164,7 @@ public final class BulkUpdate { return "BulkUpdate(" + "dataType=" + this.getDataType() + ", " + "action=" + this.getAction() + ", " + - "constraints=" + this.getQueries() + ", " + + "constraints=" + this.getFilters() + ", " + "trackStatistics=" + this.isTrackingStatistics() + ")"; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java index 3203e80ac..0e7109bd2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java @@ -26,8 +26,12 @@ package me.lucko.luckperms.common.bulkupdate; import com.google.common.collect.ImmutableList; -import me.lucko.luckperms.common.bulkupdate.action.Action; -import me.lucko.luckperms.common.bulkupdate.query.Query; +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.ConstraintFactory; +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.node.Node; import java.util.LinkedHashSet; import java.util.Set; @@ -45,18 +49,18 @@ public class BulkUpdateBuilder { private DataType dataType = DataType.ALL; // the action to apply to the data which matches the constraints - private Action action = null; + private BulkUpdateAction action = null; // should the operation count the number of affected nodes, users and groups private boolean trackStatistics = false; - // a set of constraints which data must match to be acted upon - private final Set queries = new LinkedHashSet<>(); + // a set of filters which data must match to be acted upon + private final Set> filters = new LinkedHashSet<>(); private BulkUpdateBuilder() { } - public BulkUpdateBuilder action(Action action) { + public BulkUpdateBuilder action(BulkUpdateAction action) { this.action = action; return this; } @@ -71,8 +75,8 @@ public class BulkUpdateBuilder { return this; } - public BulkUpdateBuilder query(Query query) { - this.queries.add(query); + public BulkUpdateBuilder filter(BulkUpdateField field, Comparison comparison, String value) { + this.filters.add(new Filter<>(field, ConstraintFactory.STRINGS.build(comparison, value))); return this; } @@ -81,7 +85,8 @@ public class BulkUpdateBuilder { throw new IllegalStateException("no action specified"); } - return new BulkUpdate(this.dataType, this.action, ImmutableList.copyOf(this.queries), this.trackStatistics); + FilterList filters = new FilterList<>(FilterList.LogicalOperator.AND, ImmutableList.copyOf(this.filters)); + return new BulkUpdate(this.dataType, this.action, filters, this.trackStatistics); } @Override @@ -89,7 +94,7 @@ public class BulkUpdateBuilder { return "BulkUpdateBuilder(" + "dataType=" + this.dataType + ", " + "action=" + this.action + ", " + - "constraints=" + this.queries + ", " + + "constraints=" + this.filters + ", " + "trackStatistics=" + this.trackStatistics + ")"; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java new file mode 100644 index 000000000..03d674e03 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java @@ -0,0 +1,68 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.bulkupdate; + +import me.lucko.luckperms.common.filter.FilterField; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.node.Node; + +import java.util.Locale; + +/** + * Represents a field being used in a bulk update + */ +public enum BulkUpdateField implements FilterField { + + PERMISSION { + @Override + public String getValue(Node node) { + return node.getKey(); + } + }, + + SERVER { + @Override + public String getValue(Node node) { + return node.getContexts().getAnyValue(DefaultContextKeys.SERVER_KEY).orElse("global"); + } + }, + + WORLD { + @Override + public String getValue(Node node) { + return node.getContexts().getAnyValue(DefaultContextKeys.WORLD_KEY).orElse("global"); + } + }; + + public static BulkUpdateField of(String s) { + try { + return valueOf(s.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java new file mode 100644 index 000000000..63b25c200 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java @@ -0,0 +1,76 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.bulkupdate; + +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.sql.FilterSqlBuilder; +import net.luckperms.api.node.Node; + +public class BulkUpdateSqlBuilder extends FilterSqlBuilder { + + public void visit(BulkUpdate update) { + visit(update.getAction()); + visit(update.getFilters()); + } + + public void visit(BulkUpdateAction action) { + if (action instanceof UpdateAction) { + visit(((UpdateAction) action)); + } else if (action instanceof DeleteAction) { + visit(((DeleteAction) action)); + } else { + throw new UnsupportedOperationException(action.getClass().getName()); + } + } + + public void visit(UpdateAction action) { + this.builder.append("UPDATE {table} SET "); + visitFieldName(action.getField()); + this.builder.append("="); + this.builder.variable(action.getNewValue()); + } + + public void visit(DeleteAction action) { + this.builder.append("DELETE FROM {table}"); + } + + @Override + public void visitFieldName(FilterField field) { + if (field == BulkUpdateField.PERMISSION) { + this.builder.append("permission"); + } else if (field == BulkUpdateField.SERVER) { + this.builder.append("server"); + } else if (field == BulkUpdateField.WORLD) { + this.builder.append("world"); + } else { + throw new AssertionError(field); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/Action.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/BulkUpdateAction.java similarity index 80% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/Action.java rename to common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/BulkUpdateAction.java index 25b9d681c..946ebccdc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/Action.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/BulkUpdateAction.java @@ -25,13 +25,12 @@ package me.lucko.luckperms.common.bulkupdate.action; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; import net.luckperms.api.node.Node; /** * Represents an action to be applied to a given node. */ -public interface Action { +public interface BulkUpdateAction { /** * Gets the name of this action @@ -44,17 +43,8 @@ public interface Action { * Applies this action to the given NodeModel, and returns the result. * * @param from the node to base changes from - * @return the new nodemodel instance, or null if the node should be deleted. + * @return the new node instance, or null if the node should be deleted. */ Node apply(Node from); - /** - * Gets this action in SQL form. - * - * Will include a placeholder for the table, as "{table}". - * - * @param builder the statement builder - */ - void appendSql(PreparedStatementBuilder builder); - } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java index 97319562c..d5c300c25 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java @@ -25,10 +25,9 @@ package me.lucko.luckperms.common.bulkupdate.action; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; import net.luckperms.api.node.Node; -public class DeleteAction implements Action { +public class DeleteAction implements BulkUpdateAction { public static DeleteAction create() { return new DeleteAction(); @@ -46,9 +45,4 @@ public class DeleteAction implements Action { public Node apply(Node from) { return null; // this action just deletes nodes, so return null } - - @Override - public void appendSql(PreparedStatementBuilder builder) { - builder.append("DELETE FROM {table}"); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java index 81639b3f4..89a27f658 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java @@ -25,28 +25,27 @@ package me.lucko.luckperms.common.bulkupdate.action; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; import me.lucko.luckperms.common.node.factory.NodeBuilders; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.node.Node; -public class UpdateAction implements Action { +public class UpdateAction implements BulkUpdateAction { - public static UpdateAction of(QueryField field, String value) { + public static UpdateAction of(BulkUpdateField field, String value) { return new UpdateAction(field, value); } // the field we're updating - private final QueryField field; + private final BulkUpdateField field; // the new value of the field - private final String value; + private final String newValue; - private UpdateAction(QueryField field, String value) { + private UpdateAction(BulkUpdateField field, String newValue) { this.field = field; - this.value = value; + this.newValue = newValue; } @Override @@ -54,11 +53,19 @@ public class UpdateAction implements Action { return "update"; } + public BulkUpdateField getField() { + return this.field; + } + + public String getNewValue() { + return this.newValue; + } + @Override public Node apply(Node from) { switch (this.field) { case PERMISSION: - return NodeBuilders.determineMostApplicable(this.value) + return NodeBuilders.determineMostApplicable(this.newValue) .value(from.getValue()) .expiry(from.getExpiry()) .context(from.getContexts()) @@ -66,8 +73,8 @@ public class UpdateAction implements Action { case SERVER: { MutableContextSet contexts = from.getContexts().mutableCopy(); contexts.removeAll(DefaultContextKeys.SERVER_KEY); - if (!this.value.equals("global")) { - contexts.add(DefaultContextKeys.SERVER_KEY, this.value); + if (!this.newValue.equals("global")) { + contexts.add(DefaultContextKeys.SERVER_KEY, this.newValue); } return from.toBuilder() @@ -77,8 +84,8 @@ public class UpdateAction implements Action { case WORLD: { MutableContextSet contexts = from.getContexts().mutableCopy(); contexts.removeAll(DefaultContextKeys.WORLD_KEY); - if (!this.value.equals("global")) { - contexts.add(DefaultContextKeys.WORLD_KEY, this.value); + if (!this.newValue.equals("global")) { + contexts.add(DefaultContextKeys.WORLD_KEY, this.newValue); } return from.toBuilder() @@ -89,10 +96,4 @@ public class UpdateAction implements Action { throw new RuntimeException(); } } - - @Override - public void appendSql(PreparedStatementBuilder builder) { - builder.append("UPDATE {table} SET " + this.field.getSqlName() + "="); - builder.variable(this.value); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java deleted file mode 100644 index 3152515e1..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package me.lucko.luckperms.common.bulkupdate.comparison; - -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; - -import java.util.regex.Pattern; - -/** - * An enumeration of standard {@link Comparison}s. - */ -public enum StandardComparison implements Comparison { - - EQUAL("==", "=") { - @Override - public CompiledExpression compile(String expression) { - return expression::equalsIgnoreCase; - } - }, - - NOT_EQUAL("!=", "!=") { - @Override - public CompiledExpression compile(String expression) { - return string -> !expression.equalsIgnoreCase(string); - } - }, - - SIMILAR("~~", "LIKE") { - @Override - public CompiledExpression compile(String expression) { - Pattern pattern = StandardComparison.compilePatternForLikeSyntax(expression); - return string -> pattern.matcher(string).matches(); - } - }, - - NOT_SIMILAR("!~", "NOT LIKE") { - @Override - public CompiledExpression compile(String expression) { - Pattern pattern = StandardComparison.compilePatternForLikeSyntax(expression); - return string -> !pattern.matcher(string).matches(); - } - }; - - public static final String WILDCARD = "%"; - public static final String WILDCARD_ONE = "_"; - - private final String symbol; - private final String asSql; - - StandardComparison(String symbol, String asSql) { - this.symbol = symbol; - this.asSql = asSql; - } - - @Override - public String getSymbol() { - return this.symbol; - } - - @Override - public void appendSql(PreparedStatementBuilder builder) { - builder.append(this.asSql); - } - - @Override - public String toString() { - return this.symbol; - } - - public static StandardComparison parseComparison(String s) { - for (StandardComparison t : values()) { - if (t.getSymbol().equals(s)) { - return t; - } - } - return null; - } - - static Pattern compilePatternForLikeSyntax(String expression) { - expression = expression.replace(".", "\\."); - - // convert from SQL LIKE syntax to regex - expression = expression.replace(WILDCARD_ONE, "."); - expression = expression.replace(WILDCARD, ".*"); - - return Pattern.compile(expression, Pattern.CASE_INSENSITIVE); - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java deleted file mode 100644 index 7cf3f6b03..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package me.lucko.luckperms.common.bulkupdate.query; - -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import net.luckperms.api.context.DefaultContextKeys; -import net.luckperms.api.node.Node; - -/** - * Represents a query component - */ -public class Query { - - public static Query of(QueryField field, Constraint constraint) { - return new Query(field, constraint); - } - - // the field this query is comparing against - private final QueryField field; - - // the constraint - private final Constraint constraint; - - private Query(QueryField field, Constraint constraint) { - this.field = field; - this.constraint = constraint; - } - - /** - * Returns if the given node satisfies this query - * - * @param node the node - * @return true if satisfied - */ - public boolean isSatisfiedBy(Node node) { - switch (this.field) { - case PERMISSION: - return this.constraint.eval(node.getKey()); - case SERVER: - return this.constraint.eval(node.getContexts().getAnyValue(DefaultContextKeys.SERVER_KEY).orElse("global")); - case WORLD: - return this.constraint.eval(node.getContexts().getAnyValue(DefaultContextKeys.WORLD_KEY).orElse("global")); - default: - throw new RuntimeException(); - } - } - - public void appendSql(PreparedStatementBuilder builder) { - this.constraint.appendSql(builder, this.field.getSqlName()); - } - - public QueryField getField() { - return this.field; - } - - public Constraint getConstraint() { - return this.constraint; - } -} diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java index d51bb325f..99d8b494f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java @@ -86,39 +86,44 @@ public abstract class ParentCommand extends Command { return; } - final String targetArgument = args.get(0); - I targetId = null; - if (this.type == Type.TAKES_ARGUMENT_FOR_TARGET) { - targetId = parseTarget(targetArgument, plugin, sender); + if (this.type == Type.TARGETED) { + final String targetArgument = args.get(0); + I targetId = parseTarget(targetArgument, plugin, sender); if (targetId == null) { return; } - } - - ReentrantLock lock = getLockForTarget(targetId); - lock.lock(); - try { - T target = getTarget(targetId, plugin, sender); - if (target == null) { - return; - } + ReentrantLock lock = getLockForTarget(targetId); + lock.lock(); try { - sub.execute(plugin, sender, target, args.subList(this.type.minArgs, args.size()), label); + T target = getTarget(targetId, plugin, sender); + if (target == null) { + return; + } + + try { + sub.execute(plugin, sender, target, args.subList(this.type.minArgs, args.size()), label); + } catch (CommandException e) { + e.handle(sender, label, sub); + } + + cleanup(target, plugin); + } finally { + lock.unlock(); + } + } else { + try { + sub.execute(plugin, sender, null, args.subList(this.type.minArgs, args.size()), label); } catch (CommandException e) { e.handle(sender, label, sub); } - - cleanup(target, plugin); - } finally { - lock.unlock(); } } @Override public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentList args) { switch (this.type) { - case TAKES_ARGUMENT_FOR_TARGET: + case TARGETED: return TabCompleter.create() .at(0, CompletionSupplier.startsWith(() -> getTargets(plugin).stream())) .at(1, CompletionSupplier.startsWith(() -> getChildren().stream() @@ -133,7 +138,7 @@ public abstract class ParentCommand extends Command { .orElse(Collections.emptyList()) ) .complete(args); - case NO_TARGET_ARGUMENT: + case NOT_TARGETED: return TabCompleter.create() .at(0, CompletionSupplier.startsWith(() -> getChildren().stream() .filter(s -> s.isAuthorized(sender)) @@ -178,21 +183,31 @@ public abstract class ParentCommand extends Command { return getChildren().stream().anyMatch(sc -> sc.isAuthorized(sender)); } - protected abstract List getTargets(LuckPermsPlugin plugin); + protected List getTargets(LuckPermsPlugin plugin) { + throw new UnsupportedOperationException(); + } - protected abstract I parseTarget(String target, LuckPermsPlugin plugin, Sender sender); + protected I parseTarget(String target, LuckPermsPlugin plugin, Sender sender) { + throw new UnsupportedOperationException(); + } - protected abstract ReentrantLock getLockForTarget(I target); + protected ReentrantLock getLockForTarget(I target) { + throw new UnsupportedOperationException(); + } - protected abstract T getTarget(I target, LuckPermsPlugin plugin, Sender sender); + protected T getTarget(I target, LuckPermsPlugin plugin, Sender sender) { + throw new UnsupportedOperationException(); + } - protected abstract void cleanup(T t, LuckPermsPlugin plugin); + protected void cleanup(T t, LuckPermsPlugin plugin) { + throw new UnsupportedOperationException(); + } public enum Type { // e.g. /lp log sub-command.... - NO_TARGET_ARGUMENT(0), + NOT_TARGETED(0), // e.g. /lp user sub-command.... - TAKES_ARGUMENT_FOR_TARGET(1); + TARGETED(1); private final int cmdIndex; private final int minArgs; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java index e3b5a546a..2de521a43 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java @@ -28,12 +28,9 @@ package me.lucko.luckperms.common.commands.group; import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.bulkupdate.BulkUpdateBuilder; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; import me.lucko.luckperms.common.bulkupdate.DataType; import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; -import me.lucko.luckperms.common.bulkupdate.query.Query; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -43,6 +40,7 @@ import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.model.Group; @@ -115,7 +113,7 @@ public class DeleteGroup extends SingleCommand { .trackStatistics(false) .dataType(DataType.ALL) .action(DeleteAction.create()) - .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.EQUAL, Inheritance.key(groupName)))) + .filter(BulkUpdateField.PERMISSION, Comparison.EQUAL, Inheritance.key(groupName)) .build(); plugin.getStorage().applyBulkUpdate(operation).whenCompleteAsync((v, ex) -> { if (ex != null) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java index 356ffb346..507d30395 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java @@ -60,7 +60,7 @@ public class GroupParentCommand extends ParentCommand { .build(key -> new ReentrantLock()); public GroupParentCommand() { - super(CommandSpec.GROUP, "Group", Type.TAKES_ARGUMENT_FOR_TARGET, ImmutableList.>builder() + super(CommandSpec.GROUP, "Group", Type.TARGETED, ImmutableList.>builder() .add(new GroupInfo()) .add(new CommandPermission<>(HolderType.GROUP)) .add(new CommandParent<>(HolderType.GROUP)) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java index cbef015dc..57cf03349 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java @@ -28,11 +28,8 @@ package me.lucko.luckperms.common.commands.group; import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.bulkupdate.BulkUpdateBuilder; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; -import me.lucko.luckperms.common.bulkupdate.query.Query; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -41,6 +38,7 @@ import me.lucko.luckperms.common.command.tabcomplete.CompletionSupplier; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.command.utils.StorageAssistant; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.node.types.Inheritance; @@ -112,8 +110,8 @@ public class GroupRename extends ChildCommand { BulkUpdate operation = BulkUpdateBuilder.create() .trackStatistics(false) .dataType(me.lucko.luckperms.common.bulkupdate.DataType.ALL) - .action(UpdateAction.of(QueryField.PERMISSION, Inheritance.key(newGroupName))) - .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.EQUAL, Inheritance.key(target.getName())))) + .action(UpdateAction.of(BulkUpdateField.PERMISSION, Inheritance.key(newGroupName))) + .filter(BulkUpdateField.PERMISSION, Comparison.EQUAL, Inheritance.key(target.getName())) .build(); return plugin.getStorage().applyBulkUpdate(operation); } else { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java index a5a58fa0d..9d8d56e14 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java @@ -25,25 +25,26 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; import java.util.Locale; -public class LogGroupHistory extends ChildCommand { +public class LogGroupHistory extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogGroupHistory() { @@ -51,44 +52,34 @@ public class LogGroupHistory extends ChildCommand { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { String group = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(group)) { Message.GROUP_INVALID_ENTRY.send(sender, group); return; } - Paginated content = new Paginated<>(log.getGroupHistory(group)); + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, args.getIntOrDefault(1, 1)); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.group(group), pageParams).join(); - int page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), sender, content); - } - } + int page = pageParams.pageNumber(); + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); - private static void showLog(int page, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page == Integer.MIN_VALUE) { - page = maxPage; - } - if (page < 1 || page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); String name = entries.stream().findAny().get().value().getTarget().getName(); Message.LOG_HISTORY_GROUP_HEADER.send(sender, name, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java index 678eb44cb..9361d3535 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java @@ -25,7 +25,6 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -43,7 +42,7 @@ import net.luckperms.api.node.Node; import java.util.Optional; import java.util.UUID; -public class LogNotify extends ChildCommand { +public class LogNotify extends ChildCommand { private static final String IGNORE_NODE = "luckperms.log.notify.ignoring"; public LogNotify() { @@ -83,7 +82,7 @@ public class LogNotify extends ChildCommand { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { if (sender.isConsole()) { Message.LOG_NOTIFY_CONSOLE.send(sender); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java index 0e368f755..a608ddc3a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java @@ -26,22 +26,13 @@ package me.lucko.luckperms.common.commands.log; import com.google.common.collect.ImmutableList; -import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.ParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; -import me.lucko.luckperms.common.locale.Message; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.sender.Sender; - -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -public class LogParentCommand extends ParentCommand { - private final ReentrantLock lock = new ReentrantLock(); +public class LogParentCommand extends ParentCommand { public LogParentCommand() { - super(CommandSpec.LOG, "Log", Type.NO_TARGET_ARGUMENT, ImmutableList.>builder() + super(CommandSpec.LOG, "Log", Type.NOT_TARGETED, ImmutableList.>builder() .add(new LogRecent()) .add(new LogSearch()) .add(new LogNotify()) @@ -51,38 +42,4 @@ public class LogParentCommand extends ParentCommand { .build() ); } - - @Override - protected ReentrantLock getLockForTarget(Void target) { - return this.lock; // all commands target the same log, so we share a lock between all "targets" - } - - @Override - protected Log getTarget(Void target, LuckPermsPlugin plugin, Sender sender) { - Log log = plugin.getStorage().getLog().join(); - - if (log == null) { - Message.LOG_LOAD_ERROR.send(sender); - } - - return log; - } - - @Override - protected void cleanup(Log log, LuckPermsPlugin plugin) { - - } - - @Override - protected List getTargets(LuckPermsPlugin plugin) { - // should never be called if we specify Type.NO_TARGET_ARGUMENT in the constructor - throw new UnsupportedOperationException(); - } - - @Override - protected Void parseTarget(String target, LuckPermsPlugin plugin, Sender sender) { - // should never be called if we specify Type.NO_TARGET_ARGUMENT in the constructor - throw new UnsupportedOperationException(); - } - } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java index 3e400cd8b..248c7acc1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java @@ -25,22 +25,23 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; import java.util.UUID; -public class LogRecent extends ChildCommand { +public class LogRecent extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogRecent() { @@ -48,39 +49,32 @@ public class LogRecent extends ChildCommand { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { - if (args.isEmpty()) { - // No page or user - Paginated content = new Paginated<>(log.getContent()); - showLog(content.getMaxPages(ENTRIES_PER_PAGE), false, sender, content); - return; + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { + int page = 1; + UUID uuid = null; + + if (!args.isEmpty()) { + int pageNo = args.getIntOrDefault(0, Integer.MIN_VALUE); + if (pageNo != Integer.MIN_VALUE) { + page = pageNo; + } else { + uuid = args.getUserTarget(0, plugin, sender); + if (uuid == null) { + return; + } + + pageNo = args.getIntOrDefault(1, Integer.MIN_VALUE); + if (pageNo != Integer.MIN_VALUE) { + page = pageNo; + } + } } - int page = args.getIntOrDefault(0, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - Paginated content = new Paginated<>(log.getContent()); - showLog(page, false, sender, content); - return; - } + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(uuid == null ? ActionFilters.all() : ActionFilters.source(uuid), pageParams).join(); - // User and possibly page - UUID uuid = args.getUserTarget(0, plugin, sender); - if (uuid == null) { - return; - } - - Paginated content = new Paginated<>(log.getContent(uuid)); - page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, true, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), true, sender, content); - } - } - - private static void showLog(int page, boolean specificUser, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } @@ -90,8 +84,8 @@ public class LogRecent extends ChildCommand { return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); - if (specificUser) { + List> entries = log.getNumberedContent(); + if (uuid != null) { String name = entries.stream().findAny().get().value().getSource().getName(); if (name.contains("@")) { name = name.split("@")[0]; @@ -101,8 +95,9 @@ public class LogRecent extends ChildCommand { Message.LOG_RECENT_HEADER.send(sender, page, maxPage); } - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java index 4d82ff01b..5af98e275 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java @@ -25,21 +25,22 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; -public class LogSearch extends ChildCommand { +public class LogSearch extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogSearch() { @@ -47,8 +48,8 @@ public class LogSearch extends ChildCommand { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { - int page = Integer.MIN_VALUE; + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { + int page = 1; if (args.size() > 1) { try { page = Integer.parseInt(args.get(args.size() - 1)); @@ -59,36 +60,26 @@ public class LogSearch extends ChildCommand { } final String query = String.join(" ", args); - Paginated content = new Paginated<>(log.getSearch(query)); + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.search(query), pageParams).join(); - if (page != Integer.MIN_VALUE) { - showLog(page, query, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), query, sender, content); - } - } - - private static void showLog(int page, String query, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page == Integer.MIN_VALUE) { - page = maxPage; - } - if (page < 1 || page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); Message.LOG_SEARCH_HEADER.send(sender, query, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java index 0a655a3c1..f88b2a94f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java @@ -25,25 +25,26 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; import java.util.Locale; -public class LogTrackHistory extends ChildCommand { +public class LogTrackHistory extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogTrackHistory() { @@ -51,44 +52,33 @@ public class LogTrackHistory extends ChildCommand { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { String track = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(track)) { Message.TRACK_INVALID_ENTRY.send(sender, track); return; } + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, args.getIntOrDefault(1, 1)); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.track(track), pageParams).join(); - Paginated content = new Paginated<>(log.getTrackHistory(track)); + int page = pageParams.pageNumber(); + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); - int page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), sender, content); - } - } - - private static void showLog(int page, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page == Integer.MIN_VALUE) { - page = maxPage; - } - if (page < 1 || page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); String name = entries.stream().findAny().get().value().getTarget().getName(); Message.LOG_HISTORY_TRACK_HEADER.send(sender, name, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java index cb04d89e9..f12fbe560 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java @@ -25,22 +25,23 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; import java.util.UUID; -public class LogUserHistory extends ChildCommand { +public class LogUserHistory extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogUserHistory() { @@ -48,25 +49,19 @@ public class LogUserHistory extends ChildCommand { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { UUID uuid = args.getUserTarget(0, plugin, sender); if (uuid == null) { return; } - Paginated content = new Paginated<>(log.getUserHistory(uuid)); + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, args.getIntOrDefault(1, 1)); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.user(uuid), pageParams).join(); - int page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), sender, content); - } - } + int page = pageParams.pageNumber(); + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); - private static void showLog(int page, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } @@ -76,11 +71,11 @@ public class LogUserHistory extends ChildCommand { return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); String name = entries.stream().findAny().get().value().getTarget().getName(); Message.LOG_HISTORY_USER_HEADER.send(sender, name, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java index d5e9ede5e..b957dc6f7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java @@ -28,15 +28,12 @@ package me.lucko.luckperms.common.commands.misc; import com.github.benmanes.caffeine.cache.Cache; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.bulkupdate.BulkUpdateBuilder; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateSqlBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdateStatistics; import me.lucko.luckperms.common.bulkupdate.DataType; import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; -import me.lucko.luckperms.common.bulkupdate.comparison.Comparison; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; -import me.lucko.luckperms.common.bulkupdate.query.Query; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; import me.lucko.luckperms.common.command.abstraction.CommandException; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -44,6 +41,7 @@ import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentException; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; @@ -112,7 +110,7 @@ public class BulkUpdateCommand extends SingleCommand { } String field = args.remove(0); - QueryField queryField = QueryField.of(field); + BulkUpdateField queryField = BulkUpdateField.of(field); if (queryField == null) { throw new ArgumentException.DetailedUsage(); } @@ -131,20 +129,20 @@ public class BulkUpdateCommand extends SingleCommand { return; } - QueryField field = QueryField.of(parts[0]); + BulkUpdateField field = BulkUpdateField.of(parts[0]); if (field == null) { Message.BULK_UPDATE_INVALID_CONSTRAINT.send(sender, constraint); return; } - Comparison comparison = StandardComparison.parseComparison(parts[1]); + Comparison comparison = Comparison.parse(parts[1]); if (comparison == null) { Message.BULK_UPDATE_INVALID_COMPARISON.send(sender, parts[1]); return; } String expr = parts[2]; - bulkUpdateBuilder.query(Query.of(field, Constraint.of(comparison, expr))); + bulkUpdateBuilder.filter(field, comparison, expr); } BulkUpdate bulkUpdate = bulkUpdateBuilder.build(); @@ -155,7 +153,11 @@ public class BulkUpdateCommand extends SingleCommand { String id = String.format("%04d", ThreadLocalRandom.current().nextInt(10000)); this.pendingOperations.put(id, bulkUpdate); - Message.BULK_UPDATE_QUEUED.send(sender, bulkUpdate.buildAsSql().toReadableString().replace("{table}", bulkUpdate.getDataType().getName())); + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(bulkUpdate); + String readableSql = sqlBuilder.builder().toReadableString().replace("{table}", bulkUpdate.getDataType().getName()); + + Message.BULK_UPDATE_QUEUED.send(sender, readableSql); Message.BULK_UPDATE_CONFIRM.send(sender, label, id); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java index c68f612ac..ee003abf6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java @@ -26,9 +26,6 @@ package me.lucko.luckperms.common.commands.misc; import com.google.common.collect.Maps; -import me.lucko.luckperms.common.bulkupdate.comparison.Comparison; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -36,6 +33,7 @@ import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.node.comparator.NodeEntryComparator; @@ -62,13 +60,13 @@ public class SearchCommand extends SingleCommand { @Override public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) { - Comparison comparison = StandardComparison.parseComparison(args.get(0)); + Comparison comparison = Comparison.parse(args.get(0)); if (comparison == null) { - comparison = StandardComparison.EQUAL; + comparison = Comparison.EQUAL; args.add(0, "=="); } - ConstraintNodeMatcher matcher = StandardNodeMatchers.of(Constraint.of(comparison, args.get(1))); + ConstraintNodeMatcher matcher = StandardNodeMatchers.key(args.get(1), comparison); int page = args.getIntOrDefault(2, 1); Message.SEARCH_SEARCHING.send(sender, matcher.toString()); @@ -120,7 +118,7 @@ public class SearchCommand extends SingleCommand { headerMessage.send(sender, page, pages.size(), results.size()); for (Map.Entry> ent : mappedContent) { - Message.SEARCH_NODE_ENTRY.send(sender, comparison != StandardComparison.EQUAL, ent.getValue().getNode(), ent.getKey(), holderType, label, sender.getPlugin()); + Message.SEARCH_NODE_ENTRY.send(sender, comparison != Comparison.EQUAL, ent.getValue().getNode(), ent.getKey(), holderType, label, sender.getPlugin()); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java index 659f49ad8..505747ce7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java @@ -53,7 +53,7 @@ public class TrackParentCommand extends ParentCommand { .build(key -> new ReentrantLock()); public TrackParentCommand() { - super(CommandSpec.TRACK, "Track", Type.TAKES_ARGUMENT_FOR_TARGET, ImmutableList.>builder() + super(CommandSpec.TRACK, "Track", Type.TARGETED, ImmutableList.>builder() .add(new TrackInfo()) .add(new TrackEditor()) .add(new TrackAppend()) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java index 623bbcfdc..eafa37a67 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java @@ -62,7 +62,7 @@ public class UserParentCommand extends ParentCommand { .build(key -> new ReentrantLock()); public UserParentCommand() { - super(CommandSpec.USER, "User", Type.TAKES_ARGUMENT_FOR_TARGET, ImmutableList.>builder() + super(CommandSpec.USER, "User", Type.TARGETED, ImmutableList.>builder() .add(new UserInfo()) .add(new CommandPermission<>(HolderType.USER)) .add(new CommandParent<>(HolderType.USER)) diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index 120d9fd2a..495f3296f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -443,6 +443,11 @@ public final class ConfigKeys { .collect(ImmutableCollectors.toList()); }); + /** + * If log should be posted synchronously to storage/messaging in commands + */ + public static final ConfigKey LOG_SYNCHRONOUSLY_IN_COMMANDS = booleanKey("log-synchronously-in-commands", false); + /** * If LuckPerms should automatically install translation bundles and periodically update them. */ diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Comparison.java b/common/src/main/java/me/lucko/luckperms/common/filter/Comparison.java similarity index 56% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Comparison.java rename to common/src/main/java/me/lucko/luckperms/common/filter/Comparison.java index c05fed3d1..91456dfe4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Comparison.java +++ b/common/src/main/java/me/lucko/luckperms/common/filter/Comparison.java @@ -23,48 +23,60 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate.comparison; +package me.lucko.luckperms.common.filter; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; +import java.util.regex.Pattern; /** * A method of comparing two strings */ -public interface Comparison { +public enum Comparison { + + EQUAL("=="), + NOT_EQUAL("!="), + SIMILAR("~~"), + NOT_SIMILAR("!~"); + + public static final String WILDCARD = "%"; + public static final String WILDCARD_ONE = "_"; + + private final String symbol; + + Comparison(String symbol) { + this.symbol = symbol; + } /** * Gets the symbol which represents this comparison * * @return the comparison symbol */ - String getSymbol(); + public String getSymbol() { + return this.symbol; + } - /** - * Creates a {@link CompiledExpression} for the given expression - * - * @param expression the expression - * @return the compiled expression - */ - CompiledExpression compile(String expression); + @Override + public String toString() { + return this.symbol; + } - /** - * Returns the comparison operator in SQL form - */ - void appendSql(PreparedStatementBuilder builder); + public static Comparison parse(String s) { + for (Comparison t : values()) { + if (t.getSymbol().equals(s)) { + return t; + } + } + return null; + } - /** - * An instance of {@link Comparison} which is bound to an expression. - */ - interface CompiledExpression { + public static Pattern compilePatternForLikeSyntax(String expression) { + expression = expression.replace(".", "\\."); - /** - * Tests the expression against a given string, according to the - * rules of the parent {@link Comparison}. - * - * @param string the string - * @return if there was a match - */ - boolean test(String string); + // convert from SQL LIKE syntax to regex + expression = expression.replace(WILDCARD_ONE, "."); + expression = expression.replace(WILDCARD, ".*"); + + return Pattern.compile(expression, Pattern.CASE_INSENSITIVE); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Constraint.java b/common/src/main/java/me/lucko/luckperms/common/filter/Constraint.java similarity index 60% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Constraint.java rename to common/src/main/java/me/lucko/luckperms/common/filter/Constraint.java index d08b1e252..8386509c2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Constraint.java +++ b/common/src/main/java/me/lucko/luckperms/common/filter/Constraint.java @@ -23,24 +23,28 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate.comparison; +package me.lucko.luckperms.common.filter; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; +import java.util.function.Predicate; -public class Constraint { - - public static Constraint of(Comparison comparison, String expression) { - return new Constraint(comparison, expression); - } +public class Constraint { private final Comparison comparison; - private final String expressionValue; - private final Comparison.CompiledExpression compiledExpression; + private final T value; + private final Predicate predicate; - private Constraint(Comparison comparison, String expression) { + Constraint(Comparison comparison, T value, Predicate predicate) { this.comparison = comparison; - this.expressionValue = expression; - this.compiledExpression = this.comparison.compile(this.expressionValue); + this.value = value; + this.predicate = predicate; + } + + public Comparison comparison() { + return this.comparison; + } + + public T value() { + return this.value; } /** @@ -49,20 +53,12 @@ public class Constraint { * @param value the value * @return true if satisfied */ - public boolean eval(String value) { - return this.compiledExpression.test(value); - } - - public void appendSql(PreparedStatementBuilder builder, String field) { - // e.g. field LIKE ? - builder.append(field).append(' '); - this.comparison.appendSql(builder); - builder.append(' '); - builder.variable(this.expressionValue); + public boolean evaluate(T value) { + return this.predicate.test(value); } @Override public String toString() { - return this.comparison + " " + this.expressionValue; + return this.comparison + " " + this.value; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java b/common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java new file mode 100644 index 000000000..a7897384a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java @@ -0,0 +1,102 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import java.util.UUID; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public interface ConstraintFactory { + + Predicate equal(T value); + Predicate notEqual(T value); + Predicate similar(T value); + Predicate notSimilar(T value); + + default Constraint build(Comparison comparison, T value) { + switch (comparison) { + case EQUAL: + return new Constraint<>(comparison, value, equal(value)); + case NOT_EQUAL: + return new Constraint<>(comparison, value, notEqual(value)); + case SIMILAR: + return new Constraint<>(comparison, value, similar(value)); + case NOT_SIMILAR: + return new Constraint<>(comparison, value, notSimilar(value)); + default: + throw new AssertionError(comparison); + } + } + + ConstraintFactory STRINGS = new ConstraintFactory() { + @Override + public Predicate equal(String value) { + return value::equalsIgnoreCase; + } + + @Override + public Predicate notEqual(String value) { + return string -> !value.equalsIgnoreCase(string); + } + + @Override + public Predicate similar(String value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value); + return string -> pattern.matcher(string).matches(); + } + + @Override + public Predicate notSimilar(String value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value); + return string -> !pattern.matcher(string).matches(); + } + }; + + ConstraintFactory UUIDS = new ConstraintFactory() { + @Override + public Predicate equal(UUID value) { + return value::equals; + } + + @Override + public Predicate notEqual(UUID value) { + return string -> !value.equals(string); + } + + @Override + public Predicate similar(UUID value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return uuid -> pattern.matcher(uuid.toString()).matches(); + } + + @Override + public Predicate notSimilar(UUID value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return uuid -> !pattern.matcher(uuid.toString()).matches(); + } + }; + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/Filter.java b/common/src/main/java/me/lucko/luckperms/common/filter/Filter.java new file mode 100644 index 000000000..8270ff0d0 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/Filter.java @@ -0,0 +1,59 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +public class Filter { + private final FilterField field; + private final Constraint constraint; + + public Filter(FilterField field, Constraint constraint) { + this.field = field; + this.constraint = constraint; + } + + public final FilterField field() { + return this.field; + } + + public final Constraint constraint() { + return this.constraint; + } + + /** + * Returns if the given value satisfies this filter + * + * @param value the value + * @return true if satisfied + */ + public boolean evaluate(T value) { + return this.constraint.evaluate(this.field.getValue(value)); + } + + @Override + public String toString() { + return this.field + " " + this.constraint; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java b/common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java new file mode 100644 index 000000000..a0dd64b3f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java @@ -0,0 +1,74 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import java.util.function.Function; + +/** + * Represents a field that can be filtered on. + * + * @param the type the field is on + */ +public interface FilterField { + + static FilterField named(String name, Function func) { + return new FilterField() { + @Override + public FT getValue(T object) { + return func.apply(object); + } + + @Override + public String toString() { + return name; + } + }; + } + + /** + * Gets the value of this field on the given object. + * + * @param object the object + * @return the field value as a string + */ + FT getValue(T object); + + default Filter isEqualTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.EQUAL, value)); + } + + default Filter isNotEqualTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.NOT_EQUAL, value)); + } + + default Filter isSimilarTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.SIMILAR, value)); + } + + default Filter isNotSimilarTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.NOT_SIMILAR, value)); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java b/common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java new file mode 100644 index 000000000..c70d4d32c --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java @@ -0,0 +1,101 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import com.google.common.collect.ForwardingList; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class FilterList extends ForwardingList> { + + public static FilterList empty() { + return new FilterList<>(LogicalOperator.AND, ImmutableList.of()); + } + + @SafeVarargs + public static FilterList and(Filter... filters) { + return new FilterList<>(LogicalOperator.AND, ImmutableList.copyOf(filters)); + } + + @SafeVarargs + public static FilterList or(Filter... filters) { + return new FilterList<>(LogicalOperator.OR, ImmutableList.copyOf(filters)); + } + + private final LogicalOperator operator; + private final List> filters; + + public FilterList(LogicalOperator operator, List> filters) { + this.operator = operator; + this.filters = filters; + } + + public LogicalOperator operator() { + return this.operator; + } + + @Override + protected List> delegate() { + return this.filters; + } + + /** + * Check to see if a value satisfies all (AND) or any (OR) filters in the list + * + * @param value the value to check + * @return true if satisfied + */ + public boolean evaluate(T value) { + return this.operator.match(this.filters, value); + } + + @Override + public String toString() { + String operator = this.operator.name().toLowerCase(Locale.ROOT); + return this.filters.stream().map(Filter::toString).collect(Collectors.joining(" " + operator + " ")); + } + + public enum LogicalOperator { + AND { + @Override + public boolean match(List> filters, T value) { + return filters.stream().allMatch(filter -> filter.evaluate(value)); // true if empty + } + }, + OR { + @Override + public boolean match(List> filters, T value) { + return filters.stream().anyMatch(filter -> filter.evaluate(value)); // false if empty + } + }; + + public abstract boolean match(List> filters, T value); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java b/common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java new file mode 100644 index 000000000..ce61c3df2 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java @@ -0,0 +1,79 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class PageParameters { + + private final int pageSize; + private final int pageNumber; + + public PageParameters(int pageSize, int pageNumber) { + if (pageSize < 1) { + throw new IllegalArgumentException("pageSize cannot be less than 1: " + pageSize); + } + if (pageNumber < 1) { + throw new IllegalArgumentException("pageNumber cannot be less than 1: " + pageNumber); + } + + this.pageSize = pageSize; + this.pageNumber = pageNumber; + } + + public int pageSize() { + return this.pageSize; + } + + public int pageNumber() { + return this.pageNumber; + } + + public List paginate(List input) { + int fromIndex = this.pageSize * (this.pageNumber - 1); + if (fromIndex >= input.size()) { + return Collections.emptyList(); + } + + int toIndex = Math.min(fromIndex + this.pageSize, input.size()); + return input.subList(fromIndex, toIndex); + } + + public Stream paginate(Stream input) { + return input.skip((long) this.pageSize * (this.pageNumber - 1)).limit(this.pageSize); + } + + public int getMaxPage(int totalEntries) { + if (totalEntries == 0) { + return 0; + } + + return (totalEntries + this.pageSize - 1) / this.pageSize; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java new file mode 100644 index 000000000..5d37061bd --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java @@ -0,0 +1,87 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter.mongo; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.model.Filters; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.PageParameters; +import org.bson.conversions.Bson; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.regex.Pattern; + +public class ConstraintMongoBuilder { + public static final ConstraintMongoBuilder INSTANCE = new ConstraintMongoBuilder(); + + protected ConstraintMongoBuilder() { + + } + + public Object mapConstraintValue(Object value) { + return value; + } + + public Bson make(Constraint constraint, String fieldName) { + Comparison comparison = constraint.comparison(); + Object value = mapConstraintValue(constraint.value()); + + switch (comparison) { + case EQUAL: + return Filters.eq(fieldName, value); + case NOT_EQUAL: + return Filters.ne(fieldName, value); + case SIMILAR: { + if (!(value instanceof String)) { + throw new IllegalArgumentException("Unable to create SIMILAR comparison for non-string type: " + value.getClass().getName()); + } + Pattern pattern = Comparison.compilePatternForLikeSyntax((String) value); + return Filters.regex(fieldName, pattern); + } + case NOT_SIMILAR: { + if (!(value instanceof String)) { + throw new IllegalArgumentException("Unable to create NOT_SIMILAR comparison for non-string type: " + value.getClass().getName()); + } + Pattern pattern = Comparison.compilePatternForLikeSyntax((String) value); + return Filters.not(Filters.regex(fieldName, pattern)); + } + default: + throw new AssertionError(comparison); + } + } + + public static FindIterable page(@Nullable PageParameters params, FindIterable iterable) { + if (params == null) { + return iterable; + } + + int pageSize = params.pageSize(); + int pageNumber = params.pageNumber(); + return iterable.limit(pageSize).skip((pageNumber - 1) * pageSize); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java new file mode 100644 index 000000000..93cd57267 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java @@ -0,0 +1,65 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter.mongo; + +import com.mongodb.client.model.Filters; +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.FilterList; +import org.bson.conversions.Bson; + +import java.util.List; +import java.util.stream.Collectors; + +public abstract class FilterMongoBuilder extends ConstraintMongoBuilder { + + public abstract String mapFieldName(FilterField field); + + public Bson make(Filter filter) { + return make(filter.constraint(), mapFieldName(filter.field())); + } + + public Bson make(FilterList.LogicalOperator combineOperator, List> filters) { + if (filters.isEmpty()) { + return Filters.empty(); + } + + List bsonFilters = filters.stream().map(this::make).collect(Collectors.toList()); + switch (combineOperator) { + case AND: + return Filters.and(bsonFilters); + case OR: + return Filters.or(bsonFilters); + default: + throw new AssertionError(combineOperator); + } + } + + public Bson make(FilterList filters) { + return make(filters.operator(), filters); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java new file mode 100644 index 000000000..04e5d7fc5 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java @@ -0,0 +1,84 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter.sql; + +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.storage.implementation.sql.builder.AbstractSqlBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ConstraintSqlBuilder extends AbstractSqlBuilder { + + public void visitConstraintValue(Object value) { + if (value instanceof String) { + this.builder.variable(((String) value)); + } else { + throw new IllegalArgumentException("Don't know how to write value with type: " + value.getClass().getName()); + } + } + + public void visit(Constraint constraint) { + // '= value' + // '!= value' + // 'LIKE value' + // 'NOT LIKE value' + + visit(constraint.comparison()); + this.builder.append(' '); + visitConstraintValue(constraint.value()); + } + + public void visit(Comparison comparison) { + switch (comparison) { + case EQUAL: + this.builder.append("="); + break; + case NOT_EQUAL: + this.builder.append("!="); + break; + case SIMILAR: + this.builder.append("LIKE"); + break; + case NOT_SIMILAR: + this.builder.append("NOT LIKE"); + break; + default: + throw new AssertionError(comparison); + } + } + + public void visit(@Nullable PageParameters params) { + if (params == null) { + return; + } + + int pageSize = params.pageSize(); + int pageNumber = params.pageNumber(); + this.builder.append(" LIMIT " + pageSize + " OFFSET " + (pageNumber - 1) * pageSize); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java new file mode 100644 index 000000000..e5c643698 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java @@ -0,0 +1,81 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter.sql; + +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.FilterList; + +import java.util.List; + +public abstract class FilterSqlBuilder extends ConstraintSqlBuilder { + + public abstract void visitFieldName(FilterField field); + + public void visit(Filter filter) { + // 'field = value' + // 'field != value' + // 'field LIKE value' + // 'field NOT LIKE value' + + visitFieldName(filter.field()); + this.builder.append(' '); + visit(filter.constraint()); + } + + public void visit(FilterList.LogicalOperator combineOperator, List> filters) { + if (filters.isEmpty()) { + return; + } + + String combineString; + switch (combineOperator) { + case AND: + combineString = "AND "; + break; + case OR: + combineString = "OR "; + break; + default: + throw new AssertionError(combineOperator); + } + + this.builder.append(" WHERE"); + for (int i = 0; i < filters.size(); i++) { + Filter filter = filters.get(i); + this.builder.append(" "); + if (i != 0) { + this.builder.append(combineString); + } + visit(filter); + } + } + + public void visit(FilterList filters) { + visit(filters.operator(), filters); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java index ee530dd56..bd527b793 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java @@ -300,7 +300,7 @@ public interface Message { .append(text() .color(DARK_GRAY) .append(text('[')) - .append(text(LoggedAction.getTypeCharacter(action.getTarget().getType()), GREEN)) + .append(text(LoggedAction.getTypeString(action.getTarget().getType()), GREEN)) .append(text(']')) ) .append(space()) @@ -3778,7 +3778,7 @@ public interface Message { .append(text() .color(DARK_GRAY) .append(text('[')) - .append(text(LoggedAction.getTypeCharacter(action.getTarget().getType()), GREEN)) + .append(text(LoggedAction.getTypeString(action.getTarget().getType()), GREEN)) .append(text(']')) ) .append(space()) diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java index 26b26ec62..a54a2cc14 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java @@ -31,6 +31,8 @@ import net.luckperms.api.actionlog.Action; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; +import java.util.concurrent.CompletableFuture; + public interface InternalMessagingService { /** @@ -60,21 +62,21 @@ public interface InternalMessagingService { * Uses the messaging service to inform other servers about a general * change. */ - void pushUpdate(); + CompletableFuture pushUpdate(); /** * Pushes an update for a specific user. * * @param user the user */ - void pushUserUpdate(User user); + CompletableFuture pushUserUpdate(User user); /** * Pushes a log entry to connected servers. * * @param logEntry the log entry */ - void pushLog(Action logEntry); + CompletableFuture pushLog(Action logEntry); /** * Pushes a custom payload to connected servers. @@ -82,6 +84,6 @@ public interface InternalMessagingService { * @param channelId the channel id * @param payload the payload */ - void pushCustomPayload(String channelId, String payload); + CompletableFuture pushCustomPayload(String channelId, String payload); } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java index e828d032b..508fb8906 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java @@ -36,6 +36,7 @@ import me.lucko.luckperms.common.messaging.message.UpdateMessageImpl; import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.AsyncInterface; import me.lucko.luckperms.common.util.ExpiringSet; import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.JObject; @@ -54,9 +55,10 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -public class LuckPermsMessagingService implements InternalMessagingService, IncomingMessageConsumer { +public class LuckPermsMessagingService extends AsyncInterface implements InternalMessagingService, IncomingMessageConsumer { private final LuckPermsPlugin plugin; private final ExpiringSet receivedMessages; private final PushUpdateBuffer updateBuffer; @@ -65,6 +67,7 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco private final Messenger messenger; public LuckPermsMessagingService(LuckPermsPlugin plugin, MessengerProvider messengerProvider) { + super(plugin); this.plugin = plugin; this.messengerProvider = messengerProvider; @@ -107,8 +110,8 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco } @Override - public void pushUpdate() { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushUpdate() { + return future(() -> { UUID requestId = generatePingId(); this.plugin.getLogger().info("[Messaging] Sending ping with id: " + requestId); this.messenger.sendOutgoingMessage(new UpdateMessageImpl(requestId)); @@ -116,8 +119,8 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco } @Override - public void pushUserUpdate(User user) { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushUserUpdate(User user) { + return future(() -> { UUID requestId = generatePingId(); this.plugin.getLogger().info("[Messaging] Sending user ping for '" + user.getPlainDisplayName() + "' with id: " + requestId); this.messenger.sendOutgoingMessage(new UserUpdateMessageImpl(requestId, user.getUniqueId())); @@ -125,8 +128,8 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco } @Override - public void pushLog(Action logEntry) { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushLog(Action logEntry) { + return future(() -> { UUID requestId = generatePingId(); if (this.plugin.getEventDispatcher().dispatchLogNetworkPublish(!this.plugin.getConfiguration().get(ConfigKeys.PUSH_LOG_ENTRIES), requestId, logEntry)) { @@ -139,8 +142,8 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco } @Override - public void pushCustomPayload(String channelId, String payload) { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushCustomPayload(String channelId, String payload) { + return future(() -> { UUID requestId = generatePingId(); this.messenger.sendOutgoingMessage(new CustomMessageImpl(requestId, channelId, payload)); }); @@ -283,7 +286,7 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco ActionLogMessage msg = (ActionLogMessage) message; this.plugin.getEventDispatcher().dispatchLogReceive(msg.getId(), msg.getAction()); - this.plugin.getLogDispatcher().dispatchFromRemote((LoggedAction) msg.getAction()); + this.plugin.getLogDispatcher().broadcastFromRemote((LoggedAction) msg.getAction()); } else if (message instanceof CustomMessage) { CustomMessage msg = (CustomMessage) message; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java b/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java index b5c7d361b..d07f780e6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java @@ -25,7 +25,9 @@ package me.lucko.luckperms.common.node.matcher; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.ConstraintFactory; import net.luckperms.api.node.Node; import net.luckperms.api.node.matcher.NodeMatcher; import org.checkerframework.checker.nullness.qual.NonNull; @@ -35,20 +37,20 @@ import org.checkerframework.checker.nullness.qual.Nullable; * Abstract implementation of {@link NodeMatcher} backed by a {@link Constraint}. */ public abstract class ConstraintNodeMatcher implements NodeMatcher { - private final Constraint constraint; + private final Constraint constraint; - protected ConstraintNodeMatcher(Constraint constraint) { - this.constraint = constraint; + protected ConstraintNodeMatcher(Comparison comparison, String value) { + this.constraint = ConstraintFactory.STRINGS.build(comparison, value); } - public Constraint getConstraint() { + public Constraint getConstraint() { return this.constraint; } public abstract @Nullable T filterConstraintMatch(@NonNull Node node); public @Nullable T match(Node node) { - return getConstraint().eval(node.getKey()) ? filterConstraintMatch(node) : null; + return getConstraint().evaluate(node.getKey()) ? filterConstraintMatch(node) : null; } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java b/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java index 552d437bd..fe8321b3c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java @@ -25,8 +25,7 @@ package me.lucko.luckperms.common.node.matcher; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.types.DisplayName; import me.lucko.luckperms.common.node.types.Inheritance; @@ -45,12 +44,12 @@ import org.checkerframework.checker.nullness.qual.Nullable; public final class StandardNodeMatchers { private StandardNodeMatchers() {} - public static ConstraintNodeMatcher of(Constraint constraint) { - return new Generic(constraint); + public static ConstraintNodeMatcher key(String value, Comparison comparison) { + return new Generic(comparison, value); } public static ConstraintNodeMatcher key(String key) { - return new Generic(Constraint.of(StandardComparison.EQUAL, key)); + return new Generic(Comparison.EQUAL, key); } public static ConstraintNodeMatcher key(T node) { @@ -58,7 +57,7 @@ public final class StandardNodeMatchers { } public static ConstraintNodeMatcher keyStartsWith(String startsWith) { - return new Generic(Constraint.of(StandardComparison.SIMILAR, startsWith + StandardComparison.WILDCARD)); + return new Generic(Comparison.SIMILAR, startsWith + Comparison.WILDCARD); } public static ConstraintNodeMatcher equals(T other, NodeEqualityPredicate equalityPredicate) { @@ -74,8 +73,8 @@ public final class StandardNodeMatchers { } private static class Generic extends ConstraintNodeMatcher { - Generic(Constraint constraint) { - super(constraint); + Generic(Comparison comparison, String value) { + super(comparison, value); } @Override @@ -89,7 +88,7 @@ public final class StandardNodeMatchers { private final NodeEqualityPredicate equalityPredicate; NodeEquals(T node, NodeEqualityPredicate equalityPredicate) { - super(Constraint.of(StandardComparison.EQUAL, node.getKey())); + super(Comparison.EQUAL, node.getKey()); this.node = node; this.equalityPredicate = equalityPredicate; } @@ -106,7 +105,7 @@ public final class StandardNodeMatchers { private static final class MetaKeyEquals extends ConstraintNodeMatcher { MetaKeyEquals(String metaKey) { - super(Constraint.of(StandardComparison.SIMILAR, Meta.key(metaKey, StandardComparison.WILDCARD))); + super(Comparison.SIMILAR, Meta.key(metaKey, Comparison.WILDCARD)); } @Override @@ -118,8 +117,8 @@ public final class StandardNodeMatchers { private static final class TypeEquals extends ConstraintNodeMatcher { private final NodeType type; - protected TypeEquals(NodeType type) { - super(getConstraintForType(type)); + TypeEquals(NodeType type) { + super(Comparison.SIMILAR, getSimilarToComparisonValue(type)); this.type = type; } @@ -128,21 +127,21 @@ public final class StandardNodeMatchers { return this.type.tryCast(node).orElse(null); } - private static Constraint getConstraintForType(NodeType type) { + private static String getSimilarToComparisonValue(NodeType type) { if (type == NodeType.REGEX_PERMISSION) { - return Constraint.of(StandardComparison.SIMILAR, RegexPermission.key(StandardComparison.WILDCARD)); + return RegexPermission.key(Comparison.WILDCARD); } else if (type == NodeType.INHERITANCE) { - return Constraint.of(StandardComparison.SIMILAR, Inheritance.key(StandardComparison.WILDCARD)); + return Inheritance.key(Comparison.WILDCARD); } else if (type == NodeType.PREFIX) { - return Constraint.of(StandardComparison.SIMILAR, Prefix.NODE_MARKER + StandardComparison.WILDCARD + AbstractNode.NODE_SEPARATOR + StandardComparison.WILDCARD); + return Prefix.NODE_MARKER + Comparison.WILDCARD + AbstractNode.NODE_SEPARATOR + Comparison.WILDCARD; } else if (type == NodeType.SUFFIX) { - return Constraint.of(StandardComparison.SIMILAR, Suffix.NODE_MARKER + StandardComparison.WILDCARD + AbstractNode.NODE_SEPARATOR + StandardComparison.WILDCARD); + return Suffix.NODE_MARKER + Comparison.WILDCARD + AbstractNode.NODE_SEPARATOR + Comparison.WILDCARD; } else if (type == NodeType.META) { - return Constraint.of(StandardComparison.SIMILAR, Meta.key(StandardComparison.WILDCARD, StandardComparison.WILDCARD)); + return Meta.key(Comparison.WILDCARD, Comparison.WILDCARD); } else if (type == NodeType.WEIGHT) { - return Constraint.of(StandardComparison.SIMILAR, Weight.NODE_MARKER + StandardComparison.WILDCARD); + return Weight.NODE_MARKER + Comparison.WILDCARD; } else if (type == NodeType.DISPLAY_NAME) { - return Constraint.of(StandardComparison.SIMILAR, DisplayName.key(StandardComparison.WILDCARD)); + return DisplayName.key(Comparison.WILDCARD); } throw new IllegalArgumentException("Unable to create a NodeMatcher for NodeType " + type.name()); diff --git a/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java b/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java index 9d12ca53d..97b072493 100644 --- a/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java +++ b/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java @@ -78,7 +78,7 @@ public final class AbstractSender implements Sender { @Override public void sendMessage(Component message) { - if (isConsole()) { + if (this.factory.shouldSplitNewlines(this.sender)) { for (Component line : splitNewlines(message)) { this.factory.sendMessage(this.sender, line); } @@ -89,12 +89,12 @@ public final class AbstractSender implements Sender { @Override public Tristate getPermissionValue(String permission) { - return (isConsole() && this.factory.consoleHasAllPermissions()) ? Tristate.TRUE : this.factory.getPermissionValue(this.sender, permission); + return isConsole() ? Tristate.TRUE : this.factory.getPermissionValue(this.sender, permission); } @Override public boolean hasPermission(String permission) { - return (isConsole() && this.factory.consoleHasAllPermissions()) || this.factory.hasPermission(this.sender, permission); + return isConsole() || this.factory.hasPermission(this.sender, permission); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java b/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java index 0ec94ac84..5c020afdd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java @@ -63,8 +63,8 @@ public abstract class SenderFactory

implements Aut protected abstract boolean isConsole(T sender); - protected boolean consoleHasAllPermissions() { - return true; + protected boolean shouldSplitNewlines(T sender) { + return isConsole(sender); } public final Sender wrap(T sender) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java b/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java index 064c86000..0f89eecb7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java @@ -26,8 +26,10 @@ package me.lucko.luckperms.common.storage; import com.google.common.collect.ImmutableList; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; @@ -36,12 +38,13 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.split.SplitStorage; import me.lucko.luckperms.common.storage.misc.NodeEntry; -import me.lucko.luckperms.common.util.Throwing; +import me.lucko.luckperms.common.util.AsyncInterface; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collection; import java.util.Collections; @@ -51,18 +54,17 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; /** * Provides a {@link CompletableFuture} based API for interacting with a {@link StorageImplementation}. */ -public class Storage { +public class Storage extends AsyncInterface { private final LuckPermsPlugin plugin; private final StorageImplementation implementation; public Storage(LuckPermsPlugin plugin, StorageImplementation implementation) { + super(plugin); this.plugin = plugin; this.implementation = implementation; } @@ -79,32 +81,6 @@ public class Storage { } } - private CompletableFuture future(Callable supplier) { - return CompletableFuture.supplyAsync(() -> { - try { - return supplier.call(); - } catch (Exception e) { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - throw new CompletionException(e); - } - }, this.plugin.getBootstrap().getScheduler().async()); - } - - private CompletableFuture future(Throwing.Runnable runnable) { - return CompletableFuture.runAsync(() -> { - try { - runnable.run(); - } catch (Exception e) { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - throw new CompletionException(e); - } - }, this.plugin.getBootstrap().getScheduler().async()); - } - public String getName() { return this.implementation.getImplementationName(); } @@ -133,8 +109,8 @@ public class Storage { return future(() -> this.implementation.logAction(entry)); } - public CompletableFuture getLog() { - return future(this.implementation::getLog); + public CompletableFuture getLogPage(FilterList filters, @Nullable PageParameters page) { + return future(() -> this.implementation.getLogPage(filters, page)); } public CompletableFuture applyBulkUpdate(BulkUpdate bulkUpdate) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java index 1b53936aa..7e9f1e315 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java @@ -25,8 +25,10 @@ package me.lucko.luckperms.common.storage.implementation; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; @@ -58,7 +60,7 @@ public interface StorageImplementation { void logAction(Action entry) throws Exception; - Log getLog() throws Exception; + LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception; void applyBulkUpdate(BulkUpdate bulkUpdate) throws Exception; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java index 15bf0f2e1..2ab38b26f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java @@ -26,10 +26,12 @@ package me.lucko.luckperms.common.storage.implementation.file; import com.google.common.collect.Iterables; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.context.serializer.ContextSetConfigurateSerializer; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.Track; @@ -59,6 +61,7 @@ import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.node.types.MetaNode; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.Types; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.IOException; import java.nio.file.Path; @@ -168,8 +171,8 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio } @Override - public Log getLog() throws IOException { - return this.actionLogger.getLog(); + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + return this.actionLogger.getLogPage(filters, page); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java index 263034507..e657e7bbc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java @@ -29,11 +29,15 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.stream.JsonReader; import me.lucko.luckperms.common.actionlog.ActionJsonSerializer; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; +import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.gson.GsonProvider; import net.luckperms.api.actionlog.Action; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.BufferedReader; import java.io.IOException; @@ -42,11 +46,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class FileActionLogger { @@ -130,31 +137,43 @@ public class FileActionLogger { } } - public Log getLog() throws IOException { + private Stream loadLog(FilterList filters) throws IOException { // if there is log content waiting to be written, flush immediately before trying to read if (this.saveBuffer.isEnqueued()) { this.saveBuffer.requestDirectly(); } if (!Files.exists(this.contentFile)) { - return Log.empty(); + return Stream.empty(); } - Log.Builder log = Log.builder(); - + Stream.Builder builder = Stream.builder(); try (BufferedReader reader = Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { try { JsonElement parsed = GsonProvider.parser().parse(line); - log.add(ActionJsonSerializer.deserialize(parsed)); + LoggedAction action = ActionJsonSerializer.deserialize(parsed); + if (filters.evaluate(action)) { + builder.add(action); + } } catch (Exception e) { e.printStackTrace(); } } } + return builder.build(); + } - return log.build(); + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws IOException { + List filtered = loadLog(filters) + .sorted(Comparator.comparing(LoggedAction::getTimestamp)) + .collect(Collectors.toList()) + .reversed(); + + int size = filtered.size(); + List paginated = page != null ? page.paginate(filtered) : filtered; + return LogPage.of(paginated, page, size); } private final class SaveBuffer extends BufferedRequest { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java index cb21c050e..922e096bb 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java @@ -36,10 +36,15 @@ import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; import com.mongodb.client.model.ReplaceOptions; -import me.lucko.luckperms.common.actionlog.Log; +import com.mongodb.client.model.Sorts; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilterMongoBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.context.MutableContextSetImpl; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.filter.mongo.ConstraintMongoBuilder; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.Track; @@ -64,6 +69,8 @@ import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeBuilder; import org.bson.Document; import org.bson.UuidRepresentation; +import org.bson.conversions.Bson; +import org.checkerframework.checker.nullness.qual.Nullable; import java.time.Instant; import java.util.ArrayList; @@ -168,79 +175,24 @@ public class MongoStorage implements StorageImplementation { @Override public void logAction(Action entry) { MongoCollection c = this.database.getCollection(this.prefix + "action"); - - Document doc = new Document() - .append("timestamp", entry.getTimestamp().getEpochSecond()) - .append("source", new Document() - .append("uniqueId", entry.getSource().getUniqueId()) - .append("name", entry.getSource().getName()) - ); - - Document target = new Document() - .append("type", entry.getTarget().getType().name()) - .append("name", entry.getTarget().getName()); - - if (entry.getTarget().getUniqueId().isPresent()) { - target.append("uniqueId", entry.getTarget().getUniqueId().get()); - } - - doc.append("target", target); - doc.append("description", entry.getDescription()); - - c.insertOne(doc); + c.insertOne(actionToDoc(entry)); } @Override - public Log getLog() { - Log.Builder log = Log.builder(); + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + Bson filter = ActionFilterMongoBuilder.INSTANCE.make(filters); + MongoCollection c = this.database.getCollection(this.prefix + "action"); - try (MongoCursor cursor = c.find().iterator()) { + long count = c.countDocuments(filter); + + List content = new ArrayList<>(); + try (MongoCursor cursor = ConstraintMongoBuilder.page(page, c.find(filter).sort(Sorts.descending("timestamp", "_id"))).iterator()) { while (cursor.hasNext()) { - Document d = cursor.next(); - - if (d.containsKey("source")) { - // new format - Document source = d.get("source", Document.class); - Document target = d.get("target", Document.class); - - UUID targetUniqueId = null; - if (target.containsKey("uniqueId")) { - targetUniqueId = target.get("uniqueId", UUID.class); - } - - LoggedAction e = LoggedAction.build() - .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) - .source(source.get("uniqueId", UUID.class)) - .sourceName(source.getString("name")) - .targetType(LoggedAction.parseType(target.getString("type"))) - .target(targetUniqueId) - .targetName(target.getString("name")) - .description(d.getString("description")) - .build(); - - log.add(e); - } else { - // old format - UUID actedUuid = null; - if (d.containsKey("acted")) { - actedUuid = d.get("acted", UUID.class); - } - - LoggedAction e = LoggedAction.build() - .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) - .source(d.get("actor", UUID.class)) - .sourceName(d.getString("actorName")) - .targetType(LoggedAction.parseTypeCharacter(d.getString("type").charAt(0))) - .target(actedUuid) - .targetName(d.getString("actedName")) - .description(d.getString("action")) - .build(); - - log.add(e); - } + content.add(actionFromDoc(cursor.next())); } } - return log.build(); + + return LogPage.of(content, page, (int) count); } @Override @@ -253,7 +205,7 @@ public class MongoStorage implements StorageImplementation { UUID uuid = getDocumentId(d); Document results = processBulkUpdate(d, bulkUpdate, HolderType.USER); if (results != null) { - c.replaceOne(new Document("_id", uuid), results); + c.replaceOne(Filters.eq("_id", uuid), results); } } } @@ -267,7 +219,7 @@ public class MongoStorage implements StorageImplementation { String holder = d.getString("_id"); Document results = processBulkUpdate(d, bulkUpdate, HolderType.GROUP); if (results != null) { - c.replaceOne(new Document("_id", holder), results); + c.replaceOne(Filters.eq("_id", holder), results); } } } @@ -294,7 +246,7 @@ public class MongoStorage implements StorageImplementation { public User loadUser(UUID uniqueId, String username) { User user = this.plugin.getUserManager().getOrMake(uniqueId, username); MongoCollection c = this.database.getCollection(this.prefix + "users"); - try (MongoCursor cursor = c.find(new Document("_id", user.getUniqueId())).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", user.getUniqueId())).iterator()) { if (cursor.hasNext()) { // User exists, let's load. Document d = cursor.next(); @@ -309,7 +261,7 @@ public class MongoStorage implements StorageImplementation { boolean updatedUsername = user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name)); if (updatedUsername | user.auditTemporaryNodes()) { - c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user)); + c.replaceOne(Filters.eq("_id", user.getUniqueId()), userToDoc(user)); } } else { if (this.plugin.getUserManager().isNonDefaultUser(user)) { @@ -337,9 +289,9 @@ public class MongoStorage implements StorageImplementation { MongoCollection c = this.database.getCollection(this.prefix + "users"); user.normalData().discardChanges(); if (!this.plugin.getUserManager().isNonDefaultUser(user)) { - c.deleteOne(new Document("_id", user.getUniqueId())); + c.deleteOne(Filters.eq("_id", user.getUniqueId())); } else { - c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user), new ReplaceOptions().upsert(true)); + c.replaceOne(Filters.eq("_id", user.getUniqueId()), userToDoc(user), new ReplaceOptions().upsert(true)); } } @@ -363,7 +315,7 @@ public class MongoStorage implements StorageImplementation { public List> searchUserNodes(ConstraintNodeMatcher constraint) throws Exception { List> held = new ArrayList<>(); MongoCollection c = this.database.getCollection(this.prefix + "users"); - try (MongoCursor cursor = c.find().iterator()) { + try (MongoCursor cursor = c.find(Filters.elemMatch("permissions", ConstraintMongoBuilder.INSTANCE.make(constraint.getConstraint(), "key"))).iterator()) { while (cursor.hasNext()) { Document d = cursor.next(); UUID holder = getDocumentId(d); @@ -384,7 +336,7 @@ public class MongoStorage implements StorageImplementation { public Group createAndLoadGroup(String name) { Group group = this.plugin.getGroupManager().getOrMake(name); MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find(new Document("_id", group.getName())).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", group.getName())).iterator()) { if (cursor.hasNext()) { Document d = cursor.next(); group.loadNodesFromStorage(nodesFromDoc(d)); @@ -398,7 +350,7 @@ public class MongoStorage implements StorageImplementation { @Override public Optional loadGroup(String name) { MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", name)).iterator()) { if (!cursor.hasNext()) { return Optional.empty(); } @@ -432,20 +384,20 @@ public class MongoStorage implements StorageImplementation { public void saveGroup(Group group) { MongoCollection c = this.database.getCollection(this.prefix + "groups"); group.normalData().discardChanges(); - c.replaceOne(new Document("_id", group.getName()), groupToDoc(group), new ReplaceOptions().upsert(true)); + c.replaceOne(Filters.eq("_id", group.getName()), groupToDoc(group), new ReplaceOptions().upsert(true)); } @Override public void deleteGroup(Group group) { MongoCollection c = this.database.getCollection(this.prefix + "groups"); - c.deleteOne(new Document("_id", group.getName())); + c.deleteOne(Filters.eq("_id", group.getName())); } @Override public List> searchGroupNodes(ConstraintNodeMatcher constraint) throws Exception { List> held = new ArrayList<>(); MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find().iterator()) { + try (MongoCursor cursor = c.find(Filters.elemMatch("permissions", ConstraintMongoBuilder.INSTANCE.make(constraint.getConstraint(), "key"))).iterator()) { while (cursor.hasNext()) { Document d = cursor.next(); String holder = d.getString("_id"); @@ -466,7 +418,7 @@ public class MongoStorage implements StorageImplementation { public Track createAndLoadTrack(String name) { Track track = this.plugin.getTrackManager().getOrMake(name); MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - try (MongoCursor cursor = c.find(new Document("_id", track.getName())).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", track.getName())).iterator()) { if (!cursor.hasNext()) { c.insertOne(trackToDoc(track)); } else { @@ -481,7 +433,7 @@ public class MongoStorage implements StorageImplementation { @Override public Optional loadTrack(String name) { MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", name)).iterator()) { if (!cursor.hasNext()) { return Optional.empty(); } @@ -515,13 +467,13 @@ public class MongoStorage implements StorageImplementation { @Override public void saveTrack(Track track) { MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - c.replaceOne(new Document("_id", track.getName()), trackToDoc(track)); + c.replaceOne(Filters.eq("_id", track.getName()), trackToDoc(track)); } @Override public void deleteTrack(Track track) { MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - c.deleteOne(new Document("_id", track.getName())); + c.deleteOne(Filters.eq("_id", track.getName())); } @Override @@ -534,13 +486,13 @@ public class MongoStorage implements StorageImplementation { // do the insert if (!username.equalsIgnoreCase(oldUsername)) { - c.replaceOne(new Document("_id", uniqueId), new Document("_id", uniqueId).append("name", username), new ReplaceOptions().upsert(true)); + c.replaceOne(Filters.eq("_id", uniqueId), new Document("_id", uniqueId).append("name", username), new ReplaceOptions().upsert(true)); } PlayerSaveResultImpl result = PlayerSaveResultImpl.determineBaseResult(username, oldUsername); Set conflicting = new HashSet<>(); - try (MongoCursor cursor = c.find(new Document("name", username)).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("name", username)).iterator()) { while (cursor.hasNext()) { conflicting.add(getDocumentId(cursor.next())); } @@ -565,7 +517,7 @@ public class MongoStorage implements StorageImplementation { @Override public UUID getPlayerUniqueId(String username) { MongoCollection c = this.database.getCollection(this.prefix + "uuid"); - Document doc = c.find(new Document("name", username.toLowerCase(Locale.ROOT))).first(); + Document doc = c.find(Filters.eq("name", username.toLowerCase(Locale.ROOT))).first(); if (doc != null) { return getDocumentId(doc); } @@ -575,7 +527,7 @@ public class MongoStorage implements StorageImplementation { @Override public String getPlayerName(UUID uniqueId) { MongoCollection c = this.database.getCollection(this.prefix + "uuid"); - Document doc = c.find(new Document("_id", uniqueId)).first(); + Document doc = c.find(Filters.eq("_id", uniqueId)).first(); if (doc != null) { String username = doc.get("name", String.class); if (username != null && !username.equals("null")) { @@ -635,10 +587,9 @@ public class MongoStorage implements StorageImplementation { } private static Document nodeToDoc(Node node) { - Document document = new Document(); - - document.append("key", node.getKey()); - document.append("value", node.getValue()); + Document document = new Document() + .append("key", node.getKey()) + .append("value", node.getValue()); Instant expiry = node.getExpiry(); if (expiry != null) { @@ -699,4 +650,62 @@ public class MongoStorage implements StorageImplementation { return map; } + private static Document actionToDoc(Action action) { + Document source = new Document() + .append("uniqueId", action.getSource().getUniqueId()) + .append("name", action.getSource().getName()); + + Document target = new Document() + .append("type", action.getTarget().getType().name()) + .append("name", action.getTarget().getName()); + if (action.getTarget().getUniqueId().isPresent()) { + target.append("uniqueId", action.getTarget().getUniqueId().get()); + } + + return new Document() + .append("timestamp", action.getTimestamp().getEpochSecond()) + .append("source", source) + .append("target", target) + .append("description", action.getDescription()); + } + + private static LoggedAction actionFromDoc(Document d) { + if (d.containsKey("source")) { + // new format + Document source = d.get("source", Document.class); + Document target = d.get("target", Document.class); + + UUID targetUniqueId = null; + if (target.containsKey("uniqueId")) { + targetUniqueId = target.get("uniqueId", UUID.class); + } + + return LoggedAction.build() + .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) + .source(source.get("uniqueId", UUID.class)) + .sourceName(source.getString("name")) + .targetType(LoggedAction.parseType(target.getString("type"))) + .target(targetUniqueId) + .targetName(target.getString("name")) + .description(d.getString("description")) + .build(); + } else { + // old format + UUID actedUuid = null; + if (d.containsKey("acted")) { + actedUuid = d.get("acted", UUID.class); + } + + return LoggedAction.build() + .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) + .source(d.get("actor", UUID.class)) + .sourceName(d.getString("actorName")) + .targetType(LoggedAction.parseTypeCharacter(d.getString("type").charAt(0))) + .target(actedUuid) + .targetName(d.getString("actedName")) + .description(d.getString("action")) + .build(); + } + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java index 970e97068..e0a1c8397 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java @@ -26,8 +26,10 @@ package me.lucko.luckperms.common.storage.implementation.split; import com.google.common.collect.ImmutableMap; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; @@ -40,6 +42,7 @@ import me.lucko.luckperms.common.storage.misc.NodeEntry; import net.luckperms.api.actionlog.Action; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.List; import java.util.Map; @@ -118,8 +121,8 @@ public class SplitStorage implements StorageImplementation { } @Override - public Log getLog() throws Exception { - return implFor(SplitStorageType.LOG).getLog(); + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + return implFor(SplitStorageType.LOG).getLogPage(filters, page); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java index 229e75991..321b814ea 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java @@ -28,12 +28,16 @@ package me.lucko.luckperms.common.storage.implementation.sql; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.gson.reflect.TypeToken; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilterSqlBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateSqlBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdateStatistics; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.filter.sql.ConstraintSqlBuilder; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; @@ -54,6 +58,7 @@ import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.IOException; import java.io.InputStream; @@ -127,6 +132,7 @@ public class SqlStorage implements StorageImplementation { private static final String ACTION_INSERT = "INSERT INTO '{prefix}actions' (time, actor_uuid, actor_name, type, acted_uuid, acted_name, action) VALUES(?, ?, ?, ?, ?, ?, ?)"; private static final String ACTION_SELECT_ALL = "SELECT * FROM '{prefix}actions'"; + private static final String ACTION_COUNT = "SELECT COUNT(*) FROM '{prefix}actions'"; private final LuckPermsPlugin plugin; @@ -242,18 +248,38 @@ public class SqlStorage implements StorageImplementation { } @Override - public Log getLog() throws SQLException { - final Log.Builder log = Log.builder(); + public LogPage getLogPage(FilterList filter, @Nullable PageParameters page) throws SQLException { + int count = 0; + List content = new ArrayList<>(); + try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(ACTION_SELECT_ALL))) { + ActionFilterSqlBuilder countSqlBuilder = new ActionFilterSqlBuilder(); + countSqlBuilder.builder().append(ACTION_COUNT); + countSqlBuilder.visit(filter); + + try (PreparedStatement ps = countSqlBuilder.builder().build(c, this.statementProcessor)) { + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + count = rs.getInt(1); + } + } + } + + ActionFilterSqlBuilder sqlBuilder = new ActionFilterSqlBuilder(); + sqlBuilder.builder().append(ACTION_SELECT_ALL); + sqlBuilder.visit(filter); + sqlBuilder.builder().append(" ORDER BY time DESC, id DESC"); + sqlBuilder.visit(page); + + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - log.add(readAction(rs)); + content.add(readAction(rs)); } } } } - return log.build(); + return LogPage.of(content, page, count); } @Override @@ -262,18 +288,20 @@ public class SqlStorage implements StorageImplementation { try (Connection c = this.connectionFactory.getConnection()) { if (bulkUpdate.getDataType().isIncludingUsers()) { - String table = this.statementProcessor.apply("{prefix}user_permissions"); - try (PreparedStatement ps = bulkUpdate.buildAsSql().build(c, q -> q.replace("{table}", table))) { + Function tableReplacement = s -> s.replace("{table}", "{prefix}user_permissions"); + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(bulkUpdate); + + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor.compose(tableReplacement))) { if (bulkUpdate.isTrackingStatistics()) { - PreparedStatementBuilder builder = new PreparedStatementBuilder(); - builder.append(USER_PERMISSIONS_SELECT_DISTINCT); - bulkUpdate.appendConstraintsAsSql(builder); + BulkUpdateSqlBuilder statsSqlBuilder = new BulkUpdateSqlBuilder(); + statsSqlBuilder.builder().append(USER_PERMISSIONS_SELECT_DISTINCT); + statsSqlBuilder.visit(bulkUpdate.getFilters()); - try (PreparedStatement lookup = builder.build(c, this.statementProcessor)) { + try (PreparedStatement lookup = statsSqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = lookup.executeQuery()) { Set uuids = new HashSet<>(); - while (rs.next()) { uuids.add(Uuids.fromString(rs.getString("uuid"))); } @@ -281,7 +309,9 @@ public class SqlStorage implements StorageImplementation { stats.incrementAffectedUsers(uuids.size()); } } - stats.incrementAffectedNodes(ps.executeUpdate()); + + int rowsAffected = ps.executeUpdate(); + stats.incrementAffectedNodes(rowsAffected); } else { ps.execute(); } @@ -289,18 +319,20 @@ public class SqlStorage implements StorageImplementation { } if (bulkUpdate.getDataType().isIncludingGroups()) { - String table = this.statementProcessor.apply("{prefix}group_permissions"); - try (PreparedStatement ps = bulkUpdate.buildAsSql().build(c, q -> q.replace("{table}", table))) { + Function tableReplacement = s -> s.replace("{table}", "{prefix}group_permissions"); + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(bulkUpdate); + + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor.compose(tableReplacement))) { if (bulkUpdate.isTrackingStatistics()) { - PreparedStatementBuilder builder = new PreparedStatementBuilder(); - builder.append(GROUP_PERMISSIONS_SELECT_ALL); - bulkUpdate.appendConstraintsAsSql(builder); + BulkUpdateSqlBuilder statsSqlBuilder = new BulkUpdateSqlBuilder(); + statsSqlBuilder.builder().append(GROUP_PERMISSIONS_SELECT_ALL); + statsSqlBuilder.visit(bulkUpdate.getFilters()); - try (PreparedStatement lookup = builder.build(c, this.statementProcessor)) { + try (PreparedStatement lookup = statsSqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = lookup.executeQuery()) { Set groups = new HashSet<>(); - while (rs.next()) { groups.add(rs.getString("name")); } @@ -308,7 +340,9 @@ public class SqlStorage implements StorageImplementation { stats.incrementAffectedGroups(groups.size()); } } - stats.incrementAffectedNodes(ps.executeUpdate()); + + int rowsAffected = ps.executeUpdate(); + stats.incrementAffectedNodes(rowsAffected); } else { ps.execute(); } @@ -430,12 +464,14 @@ public class SqlStorage implements StorageImplementation { @Override public List> searchUserNodes(ConstraintNodeMatcher constraint) throws SQLException { - PreparedStatementBuilder builder = new PreparedStatementBuilder().append(USER_PERMISSIONS_SELECT_PERMISSION); - constraint.getConstraint().appendSql(builder, "permission"); + ConstraintSqlBuilder sqlBuilder = new ConstraintSqlBuilder(); + sqlBuilder.builder().append(USER_PERMISSIONS_SELECT_PERMISSION); + sqlBuilder.builder().append("permission "); + sqlBuilder.visit(constraint.getConstraint()); List> held = new ArrayList<>(); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = builder.build(c, this.statementProcessor)) { + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { UUID holder = UUID.fromString(rs.getString("uuid")); @@ -534,12 +570,14 @@ public class SqlStorage implements StorageImplementation { @Override public List> searchGroupNodes(ConstraintNodeMatcher constraint) throws SQLException { - PreparedStatementBuilder builder = new PreparedStatementBuilder().append(GROUP_PERMISSIONS_SELECT_PERMISSION); - constraint.getConstraint().appendSql(builder, "permission"); + ConstraintSqlBuilder sqlBuilder = new ConstraintSqlBuilder(); + sqlBuilder.builder().append(GROUP_PERMISSIONS_SELECT_PERMISSION); + sqlBuilder.builder().append("permission "); + sqlBuilder.visit(constraint.getConstraint()); List> held = new ArrayList<>(); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = builder.build(c, this.statementProcessor)) { + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String holder = rs.getString("name"); @@ -737,7 +775,7 @@ public class SqlStorage implements StorageImplementation { ps.setLong(1, action.getTimestamp().getEpochSecond()); ps.setString(2, action.getSource().getUniqueId().toString()); ps.setString(3, action.getSource().getName()); - ps.setString(4, Character.toString(LoggedAction.getTypeCharacter(action.getTarget().getType()))); + ps.setString(4, LoggedAction.getTypeString(action.getTarget().getType())); ps.setString(5, action.getTarget().getUniqueId().map(UUID::toString).orElse("null")); ps.setString(6, action.getTarget().getName()); ps.setString(7, action.getDescription()); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java new file mode 100644 index 000000000..a42d27cd4 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java @@ -0,0 +1,36 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.storage.implementation.sql.builder; + +public class AbstractSqlBuilder { + + protected final PreparedStatementBuilder builder = new PreparedStatementBuilder(); + + public PreparedStatementBuilder builder() { + return this.builder; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/PreparedStatementBuilder.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/PreparedStatementBuilder.java similarity index 75% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/PreparedStatementBuilder.java rename to common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/PreparedStatementBuilder.java index 0e076c0a7..28a92242e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/PreparedStatementBuilder.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/PreparedStatementBuilder.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate; +package me.lucko.luckperms.common.storage.implementation.sql.builder; import java.sql.Connection; import java.sql.PreparedStatement; @@ -57,12 +57,21 @@ public class PreparedStatementBuilder { } public PreparedStatement build(Connection connection, Function mapping) throws SQLException { - PreparedStatement statement = connection.prepareStatement(mapping.apply(this.sb.toString())); - for (int i = 0; i < this.variables.size(); i++) { - String var = this.variables.get(i); - statement.setString(i + 1, var); + PreparedStatement statement = null; + try { + statement = connection.prepareStatement(mapping.apply(this.sb.toString())); + for (int i = 0; i < this.variables.size(); i++) { + String var = this.variables.get(i); + statement.setString(i + 1, var); + } + return statement; + } catch (SQLException e) { + // if an exception is thrown, any try-with-resources block above this call won't be able to close the statement + if (statement != null) { + statement.close(); + } + throw e; } - return statement; } public String toReadableString() { @@ -72,4 +81,8 @@ public class PreparedStatementBuilder { } return s; } + + public String toQueryString() { + return this.sb.toString(); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java b/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java index 731d0d4f6..1545754c3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java @@ -25,6 +25,8 @@ package me.lucko.luckperms.common.storage.misc; +import com.google.common.collect.ImmutableMap; + import java.util.Map; import java.util.Objects; @@ -54,6 +56,10 @@ public class StorageCredentials { this.properties = properties; } + public StorageCredentials(String address, String database, String username, String password) { + this(address, database, username, password, 10, 10, 1800000, 0, 5000, ImmutableMap.of()); + } + public String getAddress() { return Objects.requireNonNull(this.address, "address"); } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java b/common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java new file mode 100644 index 000000000..d1996226e --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java @@ -0,0 +1,71 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.util; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * Base class for an interface which can perform operations asynchronously and return {@link CompletableFuture}s + */ +public abstract class AsyncInterface { + + private final LuckPermsPlugin plugin; + + protected AsyncInterface(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + protected CompletableFuture future(Callable supplier) { + return CompletableFuture.supplyAsync(() -> { + try { + return supplier.call(); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new CompletionException(e); + } + }, this.plugin.getBootstrap().getScheduler().async()); + } + + protected CompletableFuture future(Throwing.Runnable runnable) { + return CompletableFuture.runAsync(() -> { + try { + runnable.run(); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new CompletionException(e); + } + }, this.plugin.getBootstrap().getScheduler().async()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java new file mode 100644 index 000000000..f3920b67d --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java @@ -0,0 +1,89 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilterMongoBuilder; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import org.bson.BsonDocument; +import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.conversions.Bson; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ActionFilterMongoTest { + + private static Stream testQueries() { + return Stream.of( + Arguments.of( + ActionFilters.source(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + // {"$and": [{"source.uniqueId": {"$binary": {"base64": "cl1YXk/xTxisymrFODZAgA==", "subType": "04"}}}]} + "{\"$and\": [{\"source.uniqueId\": {\"$binary\": {\"base64\": \"cl1YXk/xTxisymrFODZAgA==\", \"subType\": \"04\"}}}]}" + ), + Arguments.of( + ActionFilters.user(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + // {"$and": [{"target.type": "USER"}, {"target.uniqueId": {"$binary": {"base64": "cl1YXk/xTxisymrFODZAgA==", "subType": "04"}}}]} + "{\"$and\": [{\"target.type\": \"USER\"}, {\"target.uniqueId\": {\"$binary\": {\"base64\": \"cl1YXk/xTxisymrFODZAgA==\", \"subType\": \"04\"}}}]}" + ), + Arguments.of( + ActionFilters.group("test"), + // {"$and": [{"target.type": "GROUP"}, {"target.name": "test"}]} + "{\"$and\": [{\"target.type\": \"GROUP\"}, {\"target.name\": \"test\"}]}" + ), + Arguments.of( + ActionFilters.track("test"), + // {"$and": [{"target.type": "TRACK"}, {"target.name": "test"}]} + "{\"$and\": [{\"target.type\": \"TRACK\"}, {\"target.name\": \"test\"}]}" + ), + Arguments.of( + ActionFilters.search("test"), + // {"$or": [{"source.name": {"$regularExpression": {"pattern": ".*test.*", "options": "i"}}}, {"target.name": {"$regularExpression": {"pattern": ".*test.*", "options": "i"}}}, {"description": {"$regularExpression": {"pattern": ".*test.*", "options": "i"}}}]} + "{\"$or\": [{\"source.name\": {\"$regularExpression\": {\"pattern\": \".*test.*\", \"options\": \"i\"}}}, {\"target.name\": {\"$regularExpression\": {\"pattern\": \".*test.*\", \"options\": \"i\"}}}, {\"description\": {\"$regularExpression\": {\"pattern\": \".*test.*\", \"options\": \"i\"}}}]}" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueries(FilterList filters, String expectedQuery) { + Bson bson = ActionFilterMongoBuilder.INSTANCE.make(filters); + + CodecRegistry codec = CodecRegistries.withUuidRepresentation(Bson.DEFAULT_CODEC_REGISTRY, UuidRepresentation.STANDARD); + String json = bson.toBsonDocument(BsonDocument.class, codec).toJson(); + + assertEquals(expectedQuery, json); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java new file mode 100644 index 000000000..d5de2e342 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java @@ -0,0 +1,83 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilterSqlBuilder; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ActionFilterSqlTest { + + private static Stream testFiltersSql() { + return Stream.of( + Arguments.of( + ActionFilters.source(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + "WHERE actor_uuid = 725d585e-4ff1-4f18-acca-6ac538364080", + "WHERE actor_uuid = ?" + ), + Arguments.of( + ActionFilters.user(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + "WHERE type = U AND acted_uuid = 725d585e-4ff1-4f18-acca-6ac538364080", + "WHERE type = ? AND acted_uuid = ?" + ), + Arguments.of( + ActionFilters.group("test"), + "WHERE type = G AND acted_name = test", + "WHERE type = ? AND acted_name = ?" + ), + Arguments.of( + ActionFilters.track("test"), + "WHERE type = T AND acted_name = test", + "WHERE type = ? AND acted_name = ?" + ), + Arguments.of( + ActionFilters.search("test"), + "WHERE actor_name LIKE %test% OR acted_name LIKE %test% OR action LIKE %test%", + "WHERE actor_name LIKE ? OR acted_name LIKE ? OR action LIKE ?" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource + public void testFiltersSql(FilterList filters, String expectedSql, String expectedSqlParams) { + ActionFilterSqlBuilder sqlBuilder = new ActionFilterSqlBuilder(); + sqlBuilder.visit(filters); + + assertEquals(" " + expectedSql, sqlBuilder.builder().toReadableString()); + assertEquals(" " + expectedSqlParams, sqlBuilder.builder().toQueryString()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java new file mode 100644 index 000000000..3fec1d6be --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java @@ -0,0 +1,226 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.actionlog; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ActionFilterTest { + + @Test + public void testSource() { + UUID uuid = UUID.randomUUID(); + FilterList filter = ActionFilters.source(uuid); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(uuid) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + } + + @Test + public void testUser() { + UUID uuid = UUID.randomUUID(); + FilterList filter = ActionFilters.user(uuid); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .target(uuid) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(uuid) + .targetName("Test Target") + .description("test 123") + .build()) + ); + } + + @Test + public void testGroup() { + String name = "test"; + FilterList filter = ActionFilters.group(name); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("aaaaa") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName(name) + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName(name) + .description("test 123") + .build()) + ); + } + + @Test + public void testTrack() { + String name = "test"; + FilterList filter = ActionFilters.track(name); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName("aaaaa") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName(name) + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName(name) + .description("test 123") + .build()) + ); + } + + @Test + public void testSearch() { + FilterList filter = ActionFilters.search("bar"); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("Test Target") + .description("test 123") + .build()) + ); + + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("foobarbaz") + .targetType(Action.Target.Type.GROUP) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("foobarbaz") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("Test Target") + .description("foo bar baz") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("bar") + .targetType(Action.Target.Type.GROUP) + .targetName("bar") + .description("bar") + .build()) + ); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java new file mode 100644 index 000000000..062c93d42 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java @@ -0,0 +1,143 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.bulkupdate; + +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; +import me.lucko.luckperms.common.filter.Comparison; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BulkUpdateSqlTest { + + private static Stream testSimpleActionSql() { + return Stream.of( + Arguments.of("DELETE FROM {table}", DeleteAction.create()), + Arguments.of("UPDATE {table} SET permission=foo", UpdateAction.of(BulkUpdateField.PERMISSION, "foo")), + Arguments.of("UPDATE {table} SET server=foo", UpdateAction.of(BulkUpdateField.SERVER, "foo")), + Arguments.of("UPDATE {table} SET world=foo", UpdateAction.of(BulkUpdateField.WORLD, "foo")) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testSimpleActionSql(String expectedSql, BulkUpdateAction action) { + BulkUpdate update = BulkUpdateBuilder.create() + .action(action) + .build(); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(update); + String sql = sqlBuilder.builder().toReadableString(); + + assertEquals(expectedSql, sql); + } + + private static Stream testQueryFilterSql() { + return Stream.of( + Arguments.of( + "DELETE FROM {table} WHERE permission = foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.EQUAL, + "foo" + ), + Arguments.of( + "DELETE FROM {table} WHERE permission != foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.NOT_EQUAL, + "foo" + ), + Arguments.of( + "DELETE FROM {table} WHERE permission LIKE foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.SIMILAR, + "foo" + ), + Arguments.of( + "DELETE FROM {table} WHERE permission NOT LIKE foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.NOT_SIMILAR, + "foo" + ), + Arguments.of( + "UPDATE {table} SET server=foo WHERE world = bar", + UpdateAction.of(BulkUpdateField.SERVER, "foo"), + BulkUpdateField.WORLD, + Comparison.EQUAL, + "bar" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueryFilterSql(String expectedSql, BulkUpdateAction action, BulkUpdateField field, Comparison comparison, String value) { + BulkUpdate update = BulkUpdateBuilder.create() + .action(action) + .filter(field, comparison, value) + .build(); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(update); + String sql = sqlBuilder.builder().toReadableString(); + + assertEquals(expectedSql, sql); + } + + @Test + public void testQueryFilterMultipleSql() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(UpdateAction.of(BulkUpdateField.SERVER, "foo")) + .filter(BulkUpdateField.WORLD, Comparison.EQUAL, "bar") + .filter(BulkUpdateField.PERMISSION, Comparison.SIMILAR, "baz") + .filter(BulkUpdateField.SERVER, Comparison.NOT_EQUAL, "aaa") + .filter(BulkUpdateField.WORLD, Comparison.NOT_SIMILAR, "bbb") + .build(); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(update); + assertEquals( + "UPDATE {table} SET server=? WHERE world = ? AND permission LIKE ? AND server != ? AND world NOT LIKE ?", + sqlBuilder.builder().toQueryString() + ); + assertEquals( + "UPDATE {table} SET server=foo WHERE world = bar AND permission LIKE baz AND server != aaa AND world NOT LIKE bbb", + sqlBuilder.builder().toReadableString() + ); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java index 9520dfc12..ad8bc001c 100644 --- a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java @@ -26,25 +26,17 @@ package me.lucko.luckperms.common.bulkupdate; import com.google.common.collect.ImmutableSet; -import me.lucko.luckperms.common.bulkupdate.action.Action; import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; -import me.lucko.luckperms.common.bulkupdate.query.Query; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.node.types.Permission; import net.luckperms.api.node.Node; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Set; -import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -53,9 +45,9 @@ public class BulkUpdateTest { @Test public void testUpdate() { BulkUpdate update = BulkUpdateBuilder.create() - .action(UpdateAction.of(QueryField.SERVER, "foo")) - .query(Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar"))) - .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "hello%"))) + .action(UpdateAction.of(BulkUpdateField.SERVER, "foo")) + .filter(BulkUpdateField.WORLD, Comparison.EQUAL, "bar") + .filter(BulkUpdateField.PERMISSION, Comparison.SIMILAR, "hello%") .trackStatistics(true) .build(); @@ -88,8 +80,8 @@ public class BulkUpdateTest { public void testDelete() { BulkUpdate update = BulkUpdateBuilder.create() .action(DeleteAction.create()) - .query(Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar"))) - .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "hello%"))) + .filter(BulkUpdateField.WORLD, Comparison.EQUAL, "bar") + .filter(BulkUpdateField.PERMISSION, Comparison.SIMILAR, "hello%") .trackStatistics(true) .build(); @@ -115,74 +107,4 @@ public class BulkUpdateTest { assertEquals(0, statistics.getAffectedGroups()); } - private static Stream testSimpleActionSql() { - return Stream.of( - Arguments.of("DELETE FROM {table}", DeleteAction.create()), - Arguments.of("UPDATE {table} SET permission=foo", UpdateAction.of(QueryField.PERMISSION, "foo")), - Arguments.of("UPDATE {table} SET server=foo", UpdateAction.of(QueryField.SERVER, "foo")), - Arguments.of("UPDATE {table} SET world=foo", UpdateAction.of(QueryField.WORLD, "foo")) - ); - } - - @ParameterizedTest(name = "[{index}] {0}") - @MethodSource - public void testSimpleActionSql(String expectedSql, Action action) { - BulkUpdate update = BulkUpdateBuilder.create() - .action(action) - .build(); - assertEquals(expectedSql, update.buildAsSql().toReadableString()); - } - - private static Stream testQueryFilterSql() { - return Stream.of( - Arguments.of( - "DELETE FROM {table} WHERE permission = foo", - DeleteAction.create(), - Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.EQUAL, "foo")) - ), - Arguments.of( - "DELETE FROM {table} WHERE permission != foo", - DeleteAction.create(), - Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.NOT_EQUAL, "foo")) - ), - Arguments.of( - "DELETE FROM {table} WHERE permission LIKE foo", - DeleteAction.create(), - Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "foo")) - ), - Arguments.of( - "DELETE FROM {table} WHERE permission NOT LIKE foo", - DeleteAction.create(), - Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.NOT_SIMILAR, "foo")) - ), - Arguments.of( - "UPDATE {table} SET server=foo WHERE world = bar", - UpdateAction.of(QueryField.SERVER, "foo"), - Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar")) - ) - ); - } - - @ParameterizedTest(name = "[{index}] {0}") - @MethodSource - public void testQueryFilterSql(String expectedSql, Action action, Query query) { - BulkUpdate update = BulkUpdateBuilder.create() - .action(action) - .query(query) - .build(); - assertEquals(expectedSql, update.buildAsSql().toReadableString()); - } - - @Test - public void testQueryFilterMultipleSql() { - BulkUpdate update = BulkUpdateBuilder.create() - .action(UpdateAction.of(QueryField.SERVER, "foo")) - .query(Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar"))) - .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "baz"))) - .build(); - - String expected = "UPDATE {table} SET server=foo WHERE world = bar AND permission LIKE baz"; - assertEquals(expected, update.buildAsSql().toReadableString()); - } - } diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/ComparisonTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/ComparisonTest.java similarity index 77% rename from common/src/test/java/me/lucko/luckperms/common/bulkupdate/ComparisonTest.java rename to common/src/test/java/me/lucko/luckperms/common/filter/ComparisonTest.java index eee355de7..dc75dacb2 100644 --- a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/ComparisonTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/filter/ComparisonTest.java @@ -23,9 +23,8 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate; +package me.lucko.luckperms.common.filter; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -43,8 +42,10 @@ public class ComparisonTest { "'', foo, false", }) public void testEquals(String expression, String test, boolean expected) { - assertEquals(expected, StandardComparison.EQUAL.compile(expression).test(test)); - assertEquals(!expected, StandardComparison.NOT_EQUAL.compile(expression).test(test)); + assertEquals(expected, ConstraintFactory.STRINGS.build(Comparison.EQUAL, expression).evaluate(test)); + assertEquals(expected, ConstraintFactory.STRINGS.build(Comparison.EQUAL, test).evaluate(expression)); + assertEquals(!expected, ConstraintFactory.STRINGS.build(Comparison.NOT_EQUAL, expression).evaluate(test)); + assertEquals(!expected, ConstraintFactory.STRINGS.build(Comparison.NOT_EQUAL, test).evaluate(expression)); } @ParameterizedTest(name = "[{index}] {0} {1}") @@ -72,8 +73,8 @@ public class ComparisonTest { "_ar, bbar, false", }) public void testSimilar(String expression, String test, boolean expected) { - assertEquals(expected, StandardComparison.SIMILAR.compile(expression).test(test)); - assertEquals(!expected, StandardComparison.NOT_SIMILAR.compile(expression).test(test)); + assertEquals(expected, ConstraintFactory.STRINGS.build(Comparison.SIMILAR, expression).evaluate(test)); + assertEquals(!expected, ConstraintFactory.STRINGS.build(Comparison.NOT_SIMILAR, expression).evaluate(test)); } } diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java new file mode 100644 index 000000000..bb68bb20f --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java @@ -0,0 +1,118 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import me.lucko.luckperms.common.filter.mongo.FilterMongoBuilder; +import org.bson.BsonDocument; +import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.conversions.Bson; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FilterMongoTest { + + private static Stream testQueries() { + return Stream.of( + Arguments.of( + FilterList.empty(), + // {} + "{}" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + // {"$and": [{"foo": "hello"}]} + "{\"$and\": [{\"foo\": \"hello\"}]}" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + // {"$and": [{"foo": "hello"}, {"bar": "world"}]} + "{\"$and\": [{\"foo\": \"hello\"}, {\"bar\": \"world\"}]}" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + // {"$or": [{"foo": "hello"}]} + "{\"$or\": [{\"foo\": \"hello\"}]}" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + // {"$or": [{"foo": "hello"}, {"bar": "world"}]} + "{\"$or\": [{\"foo\": \"hello\"}, {\"bar\": \"world\"}]}" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueries(FilterList filters, String expectedQuery) { + Bson bson = new TestFilterMongoBuilder().make(filters); + + CodecRegistry codec = CodecRegistries.withUuidRepresentation(Bson.DEFAULT_CODEC_REGISTRY, UuidRepresentation.STANDARD); + String json = bson.toBsonDocument(BsonDocument.class, codec).toJson(); + + assertEquals(expectedQuery, json); + } + + private enum TestField implements FilterField { + FOO, BAR; + + @Override + public String getValue(Object object) { + return "null"; + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + } + + private static final class TestFilterMongoBuilder extends FilterMongoBuilder { + + @Override + public String mapFieldName(FilterField field) { + return field.toString(); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java new file mode 100644 index 000000000..2870128b3 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java @@ -0,0 +1,115 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import me.lucko.luckperms.common.filter.sql.FilterSqlBuilder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FilterSqlTest { + + private static Stream testQueries() { + return Stream.of( + Arguments.of( + FilterList.empty(), + "", + "" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello", + " WHERE foo = ?" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello AND bar = world", + " WHERE foo = ? AND bar = ?" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello", + " WHERE foo = ?" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello OR bar = world", + " WHERE foo = ? OR bar = ?" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueries(FilterList filters, String expectedSql, String expectedSqlParams) { + TestFilterMongoBuilder sqlBuilder = new TestFilterMongoBuilder(); + sqlBuilder.visit(filters); + + System.out.println(sqlBuilder.builder().toReadableString()); + System.out.println(sqlBuilder.builder().toQueryString()); + + assertEquals(expectedSql, sqlBuilder.builder().toReadableString()); + assertEquals(expectedSqlParams, sqlBuilder.builder().toQueryString()); + } + + private enum TestField implements FilterField { + FOO, BAR; + + @Override + public String getValue(Object object) { + return "null"; + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + } + + private static final class TestFilterMongoBuilder extends FilterSqlBuilder { + + @Override + public void visitFieldName(FilterField field) { + this.builder.append(field.toString()); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java new file mode 100644 index 000000000..d7fe4d6fb --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java @@ -0,0 +1,50 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.filter; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PageParametersTest { + + @ParameterizedTest(name = "[{index}] {0} {1}") + @CsvSource({ + "5, 150, 30", + "151, 150, 1", + "150, 150, 1", + "149, 150, 2", + "1, 1, 1", + "1, 0, 0", + "10, 0, 0", + }) + public void testMaxPage(int pageSize, int total, int expectedMaxPage) { + int maxPage = new PageParameters(pageSize, 1).getMaxPage(total); + assertEquals(expectedMaxPage, maxPage); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java new file mode 100644 index 000000000..cf5025a6d --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java @@ -0,0 +1,361 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.storage; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.actionlog.LogPage; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.LuckPermsConfiguration; +import me.lucko.luckperms.common.event.EventDispatcher; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.PrimaryGroupHolder; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.model.manager.user.UserManager; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.model.PlayerSaveResult; +import net.luckperms.api.model.PlayerSaveResult.Outcome; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.PermissionNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalAnswers.answer; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public abstract class AbstractStorageTest { + + @Mock protected LuckPermsPlugin plugin; + @Mock protected LuckPermsBootstrap bootstrap; + @Mock protected LuckPermsConfiguration configuration; + + private StorageImplementation storage; + + @BeforeEach + public final void setupMocksAndStorage() throws Exception { + lenient().when(this.plugin.getBootstrap()).thenReturn(this.bootstrap); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.bootstrap.getScheduler()).thenReturn(mock(SchedulerAdapter.class)); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(PrimaryGroupHolder.AllParentsByWeight::new); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION_METHOD)).thenReturn("parents-by-weight"); + lenient().when(this.bootstrap.getResourceStream(anyString())) + .then(answer((String path) -> AbstractStorageTest.class.getClassLoader().getResourceAsStream(path))); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + + this.storage = makeStorage(this.plugin); + this.storage.init(); + } + + protected abstract StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception; + + protected void cleanupResources() { + // do nothing + } + + @AfterEach + public final void shutdownStorage() { + this.storage.shutdown(); + cleanupResources(); + } + + @Test + public void testActionLog() throws Exception { + UUID sourceUuid = UUID.randomUUID(); + UUID targetUuid = UUID.randomUUID(); + + Instant baseTime = Instant.now(); + + Function mockAction = i -> LoggedAction.build() + .source(i % 2 == 0 ? sourceUuid : UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(targetUuid) + .targetName("Test Target") + .description("hello " + i) + .timestamp(baseTime.plusSeconds(i)) + .build(); + + for (int i = 0; i < 100; i++) { + this.storage.logAction(mockAction.apply(-i)); + } + for (int i = 0; i < 100; i++) { + this.storage.logAction(mockAction.apply(i)); + } + for (int i = 100; i < 200; i++) { + this.storage.logAction(mockAction.apply(-i)); + } + + for (int i = 0; i < 10; i++) { + this.storage.logAction(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName(i % 2 == 0 ? "test_group" : "dummy") + .description("group test " + i) + .timestamp(baseTime) + .build()); + } + + for (int i = 0; i < 10; i++) { + this.storage.logAction(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName(i % 2 == 0 ? "test_track" : "dummy") + .description("track test " + i) + .timestamp(baseTime) + .build()); + } + + LogPage page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 1)); + assertEquals(ImmutableList.of( + mockAction.apply(98), + mockAction.apply(96), + mockAction.apply(94), + mockAction.apply(92), + mockAction.apply(90) + ), page.getContent()); + List positions = page.getNumberedContent().stream().map(LogPage.Entry::position).collect(Collectors.toList()); + assertEquals(ImmutableList.of(1, 2, 3, 4, 5), positions); + assertEquals(150, page.getTotalEntries()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 3)); + assertEquals(ImmutableList.of( + mockAction.apply(78), + mockAction.apply(76), + mockAction.apply(74), + mockAction.apply(72), + mockAction.apply(70) + ), page.getContent()); + positions = page.getNumberedContent().stream().map(LogPage.Entry::position).collect(Collectors.toList()); + assertEquals(ImmutableList.of(11, 12, 13, 14, 15), positions); + assertEquals(150, page.getTotalEntries()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 31)); + assertEquals(150, page.getTotalEntries()); + assertEquals(0, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(500, 1)); + assertEquals(150, page.getTotalEntries()); + assertEquals(150, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), null); + assertEquals(150, page.getTotalEntries()); + assertEquals(150, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.all(), null); + assertEquals(320, page.getTotalEntries()); + assertEquals(320, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.user(targetUuid), new PageParameters(5, 1)); + assertEquals(300, page.getTotalEntries()); + + page = this.storage.getLogPage(ActionFilters.group("test_group"), new PageParameters(10, 1)); + assertEquals(5, page.getContent().size()); + assertEquals( + ImmutableList.of("group test 8", "group test 6", "group test 4", "group test 2", "group test 0"), + page.getContent().stream().map(LoggedAction::getDescription).collect(Collectors.toList()) + ); + + page = this.storage.getLogPage(ActionFilters.track("test_track"), new PageParameters(10, 1)); + assertEquals(5, page.getContent().size()); + assertEquals( + ImmutableList.of("track test 8", "track test 6", "track test 4", "track test 2", "track test 0"), + page.getContent().stream().map(LoggedAction::getDescription).collect(Collectors.toList()) + ); + + page = this.storage.getLogPage(ActionFilters.search("hello"), new PageParameters(500, 1)); + assertEquals(300, page.getContent().size()); + } + + @Test + public void testSavePlayerData() throws Exception { + UUID uniqueId = UUID.randomUUID(); + + // clean insert + PlayerSaveResult r1 = this.storage.savePlayerData(uniqueId, "Player1"); + assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT), r1.getOutcomes()); + assertNull(r1.getOtherUniqueIds()); + assertNull(r1.getPreviousUsername()); + + // no change expected + PlayerSaveResult r2 = this.storage.savePlayerData(uniqueId, "Player1"); + assertEquals(ImmutableSet.of(Outcome.NO_CHANGE), r2.getOutcomes()); + assertNull(r2.getOtherUniqueIds()); + assertNull(r2.getPreviousUsername()); + + // changed username + PlayerSaveResult r3 = this.storage.savePlayerData(uniqueId, "Player2"); + assertEquals(ImmutableSet.of(Outcome.USERNAME_UPDATED), r3.getOutcomes()); + assertNull(r3.getOtherUniqueIds()); + assertTrue("Player1".equalsIgnoreCase(r3.getPreviousUsername())); + + // changed uuid + UUID newUniqueId = UUID.randomUUID(); + PlayerSaveResult r4 = this.storage.savePlayerData(newUniqueId, "Player2"); + assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT, Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME), r4.getOutcomes()); + assertNotNull(r4.getOtherUniqueIds()); + assertEquals(ImmutableSet.of(uniqueId), r4.getOtherUniqueIds()); + assertNull(r2.getPreviousUsername()); + } + + @Test + public void testGetPlayerUniqueIdAndName() throws Exception { + UUID uniqueId = UUID.randomUUID(); + String username = "Player1"; + + this.storage.savePlayerData(uniqueId, username); + + assertEquals(uniqueId, this.storage.getPlayerUniqueId("Player1")); + assertTrue(username.equalsIgnoreCase(this.storage.getPlayerName(uniqueId))); + } + + @Test + public void testGetPlayerUniqueIdAndNameNull() throws Exception { + assertNull(this.storage.getPlayerUniqueId("Player1")); + assertNull(this.storage.getPlayerName(UUID.randomUUID())); + } + + @Test + public void testSaveAndLoadGroup() throws Exception { + StandardGroupManager groupManager = new StandardGroupManager(this.plugin); + + //noinspection unchecked,rawtypes + lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) groupManager); + + Group group = this.storage.createAndLoadGroup("test"); + + group.normalData().add(Permission.builder() + .permission("test.1") + .withContext("server", "test") + .build() + ); + group.normalData().add(Permission.builder() + .permission("test.2") + .withContext("world", "test") + .build() + ); + group.normalData().add(Permission.builder() + .permission("test.3") + .expiry(1, TimeUnit.HOURS) + .withContext("server", "test") + .withContext("world", "test") + .withContext("hello", "test") + .build() + ); + + Set nodes = group.normalData().asSet(); + assertEquals(3, nodes.size()); + + this.storage.saveGroup(group); + groupManager.unload("test"); + + Group loaded = this.storage.loadGroup("test").orElse(null); + assertNotNull(loaded); + assertNotSame(group, loaded); + assertEquals(nodes, loaded.normalData().asSet()); + } + + @Test + public void testSaveAndDeleteUser() throws Exception { + StandardUserManager userManager = new StandardUserManager(this.plugin); + + //noinspection unchecked,rawtypes + when(this.plugin.getUserManager()).thenReturn((UserManager) userManager); + + UUID exampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"); + String exampleUsername = "Notch"; + PermissionNode examplePermission = Permission.builder() + .permission("test.1") + .withContext("server", "test") + .build(); + InheritanceNode defaultGroupNode = Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build(); + + // create a default user, assert that is doesn't appear in unique users list + this.storage.savePlayerData(exampleUniqueId, exampleUsername); + assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId)); + + // give the user a node, assert that it does appear in unique users list + User user = this.storage.loadUser(exampleUniqueId, exampleUsername); + user.setNode(DataType.NORMAL, examplePermission, true); + this.storage.saveUser(user); + assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); + + // clear all nodes (reset to default) and assert that it does not appear in unique users list + user.clearNodes(DataType.NORMAL, null, true); + this.storage.saveUser(user); + assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId)); + assertEquals(ImmutableSet.of(defaultGroupNode), user.normalData().asSet()); + + // give it a node again, assert that it shows as a unique user + user.setNode(DataType.NORMAL, examplePermission, true); + this.storage.saveUser(user); + assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); + assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet()); + + // reload user data from the db and assert that it is unchanged + user = this.storage.loadUser(exampleUniqueId, exampleUsername); + assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java new file mode 100644 index 000000000..22d1db444 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java @@ -0,0 +1,141 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.storage; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.implementation.file.CombinedConfigurateStorage; +import me.lucko.luckperms.common.storage.implementation.file.SeparatedConfigurateStorage; +import me.lucko.luckperms.common.storage.implementation.file.loader.HoconLoader; +import me.lucko.luckperms.common.storage.implementation.file.loader.JsonLoader; +import me.lucko.luckperms.common.storage.implementation.file.loader.TomlLoader; +import me.lucko.luckperms.common.storage.implementation.file.loader.YamlLoader; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.mockito.Mockito.lenient; + +public class ConfigurateStorageTest { + + @Nested + class SeparatedYaml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage"); + } + } + + @Nested + class SeparatedJson extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "JSON", new JsonLoader(), ".json", "json-storage"); + } + } + + @Nested + class SeparatedHocon extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "HOCON", new HoconLoader(), ".conf", "hocon-storage"); + } + } + + @Nested + class SeparatedToml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "TOML", new TomlLoader(), ".toml", "toml-storage"); + } + } + + @Nested + class CombinedYaml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage"); + } + } + + @Nested + class CombinedJson extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "JSON", new JsonLoader(), ".json", "json-storage"); + } + } + + @Nested + class CombinedHocon extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "HOCON", new HoconLoader(), ".conf", "hocon-storage"); + } + } + + @Nested + class CombinedToml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "TOML", new TomlLoader(), ".toml", "toml-storage"); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java new file mode 100644 index 000000000..cc28698da --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java @@ -0,0 +1,61 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.storage; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.implementation.mongodb.MongoStorage; +import me.lucko.luckperms.common.storage.misc.StorageCredentials; +import org.junit.jupiter.api.Tag; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +@Tag("docker") +public class MongoStorageTest extends AbstractStorageTest { + + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mongo")) + .withExposedPorts(27017); + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + this.container.start(); + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + StorageCredentials credentials = new StorageCredentials( + host + ":" + port, + "minecraft", + "", + "" + ); + return new MongoStorage(plugin, credentials, "", ""); + } + + @Override + protected void cleanupResources() { + this.container.stop(); + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java index e6fb76a0a..f098c75ab 100644 --- a/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java @@ -25,237 +25,22 @@ package me.lucko.luckperms.common.storage; -import com.google.common.collect.ImmutableSet; -import me.lucko.luckperms.common.actionlog.Log; -import me.lucko.luckperms.common.actionlog.LoggedAction; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.config.LuckPermsConfiguration; -import me.lucko.luckperms.common.event.EventDispatcher; -import me.lucko.luckperms.common.model.Group; -import me.lucko.luckperms.common.model.PrimaryGroupHolder; -import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; -import me.lucko.luckperms.common.model.manager.user.StandardUserManager; -import me.lucko.luckperms.common.model.manager.user.UserManager; -import me.lucko.luckperms.common.node.types.Inheritance; -import me.lucko.luckperms.common.node.types.Permission; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; -import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.file.NonClosableConnection; -import net.luckperms.api.actionlog.Action; -import net.luckperms.api.model.PlayerSaveResult; -import net.luckperms.api.model.PlayerSaveResult.Outcome; -import net.luckperms.api.model.data.DataType; -import net.luckperms.api.node.Node; -import net.luckperms.api.node.types.InheritanceNode; -import net.luckperms.api.node.types.PermissionNode; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.function.Function; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.AdditionalAnswers.answer; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +public class SqlStorageTest extends AbstractStorageTest { -@ExtendWith(MockitoExtension.class) -public class SqlStorageTest { - - @Mock private LuckPermsPlugin plugin; - @Mock private LuckPermsBootstrap bootstrap; - @Mock private LuckPermsConfiguration configuration; - - private SqlStorage storage; - - @BeforeEach - public void setupMocksAndDatabase() throws Exception { - lenient().when(this.plugin.getBootstrap()).thenReturn(this.bootstrap); - lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); - lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); - lenient().when(this.bootstrap.getScheduler()).thenReturn(mock(SchedulerAdapter.class)); - lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(PrimaryGroupHolder.AllParentsByWeight::new); - lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION_METHOD)).thenReturn("parents-by-weight"); - lenient().when(this.bootstrap.getResourceStream(anyString())) - .then(answer((String path) -> SqlStorageTest.class.getClassLoader().getResourceAsStream(path))); - lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); - - this.storage = new SqlStorage(this.plugin, new TestH2ConnectionFactory(), "luckperms_"); - this.storage.init(); - } - - @AfterEach - public void shutdownDatabase() { - this.storage.shutdown(); - } - - @Test - public void testActionLog() throws Exception { - LoggedAction action = LoggedAction.build() - .source(UUID.randomUUID()) - .sourceName("Test Source") - .targetType(Action.Target.Type.USER) - .target(UUID.randomUUID()) - .targetName("Test Target") - .description("hello 123 hello 123") - .build(); - - this.storage.logAction(action); - - Log log = this.storage.getLog(); - assertEquals(1, log.getContent().size()); - assertEquals(action, log.getContent().first()); - } - - @Test - public void testSavePlayerData() throws Exception { - UUID uniqueId = UUID.randomUUID(); - - // clean insert - PlayerSaveResult r1 = this.storage.savePlayerData(uniqueId, "Player1"); - assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT), r1.getOutcomes()); - assertNull(r1.getOtherUniqueIds()); - assertNull(r1.getPreviousUsername()); - - // no change expected - PlayerSaveResult r2 = this.storage.savePlayerData(uniqueId, "Player1"); - assertEquals(ImmutableSet.of(Outcome.NO_CHANGE), r2.getOutcomes()); - assertNull(r2.getOtherUniqueIds()); - assertNull(r2.getPreviousUsername()); - - // changed username - PlayerSaveResult r3 = this.storage.savePlayerData(uniqueId, "Player2"); - assertEquals(ImmutableSet.of(Outcome.USERNAME_UPDATED), r3.getOutcomes()); - assertNull(r3.getOtherUniqueIds()); - assertTrue("Player1".equalsIgnoreCase(r3.getPreviousUsername())); - - // changed uuid - UUID newUniqueId = UUID.randomUUID(); - PlayerSaveResult r4 = this.storage.savePlayerData(newUniqueId, "Player2"); - assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT, Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME), r4.getOutcomes()); - assertNotNull(r4.getOtherUniqueIds()); - assertEquals(ImmutableSet.of(uniqueId), r4.getOtherUniqueIds()); - assertNull(r2.getPreviousUsername()); - } - - @Test - public void testGetPlayerUniqueIdAndName() throws Exception { - UUID uniqueId = UUID.randomUUID(); - String username = "Player1"; - - this.storage.savePlayerData(uniqueId, username); - - assertEquals(uniqueId, this.storage.getPlayerUniqueId("Player1")); - assertTrue(username.equalsIgnoreCase(this.storage.getPlayerName(uniqueId))); - } - - @Test - public void testGetPlayerUniqueIdAndNameNull() throws Exception { - assertNull(this.storage.getPlayerUniqueId("Player1")); - assertNull(this.storage.getPlayerName(UUID.randomUUID())); - } - - @Test - public void testSaveAndLoadGroup() throws Exception { - StandardGroupManager groupManager = new StandardGroupManager(this.plugin); - - //noinspection unchecked,rawtypes - lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) groupManager); - - Group group = this.storage.createAndLoadGroup("test"); - - group.normalData().add(Permission.builder() - .permission("test.1") - .withContext("server", "test") - .build() - ); - group.normalData().add(Permission.builder() - .permission("test.2") - .withContext("world", "test") - .build() - ); - group.normalData().add(Permission.builder() - .permission("test.3") - .expiry(1, TimeUnit.HOURS) - .withContext("server", "test") - .withContext("world", "test") - .withContext("hello", "test") - .build() - ); - - Set nodes = group.normalData().asSet(); - assertEquals(3, nodes.size()); - - this.storage.saveGroup(group); - groupManager.unload("test"); - - Group loaded = this.storage.loadGroup("test").orElse(null); - assertNotNull(loaded); - assertNotSame(group, loaded); - assertEquals(nodes, loaded.normalData().asSet()); - } - - @Test - public void testSaveAndDeleteUser() throws SQLException { - StandardUserManager userManager = new StandardUserManager(this.plugin); - - //noinspection unchecked,rawtypes - when(this.plugin.getUserManager()).thenReturn((UserManager) userManager); - - UUID exampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"); - String exampleUsername = "Notch"; - PermissionNode examplePermission = Permission.builder() - .permission("test.1") - .withContext("server", "test") - .build(); - InheritanceNode defaultGroupNode = Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build(); - - // create a default user, assert that is doesn't appear in unique users list - this.storage.savePlayerData(exampleUniqueId, exampleUsername); - assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId)); - - // give the user a node, assert that it does appear in unique users list - User user = this.storage.loadUser(exampleUniqueId, exampleUsername); - user.setNode(DataType.NORMAL, examplePermission, true); - this.storage.saveUser(user); - assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); - - // clear all nodes (reset to default) and assert that it does not appear in unique users list - user.clearNodes(DataType.NORMAL, null, true); - this.storage.saveUser(user); - assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId)); - assertEquals(ImmutableSet.of(defaultGroupNode), user.normalData().asSet()); - - // give it a node again, assert that it shows as a unique user - user.setNode(DataType.NORMAL, examplePermission, true); - this.storage.saveUser(user); - assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); - assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet()); - - // reload user data from the db and assert that it is unchanged - user = this.storage.loadUser(exampleUniqueId, exampleUsername); - assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet()); + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + return new SqlStorage(plugin, new TestH2ConnectionFactory(), "luckperms_"); } private static class TestH2ConnectionFactory implements ConnectionFactory { @@ -300,5 +85,4 @@ public class SqlStorageTest { this.connection.shutdown(); } } - } diff --git a/common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java b/common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java deleted file mode 100644 index 7a0ed3795..000000000 --- a/common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package me.lucko.luckperms.common.util; - -import com.google.common.collect.ImmutableList; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.List; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class PaginatedTest { - - private static final Paginated EXAMPLE_PAGE = new Paginated<>(ImmutableList.of("one", "two", "three", "four", "five")); - - @ParameterizedTest - @CsvSource({ - "3, 2", - "1, 5", - "1, 6" - }) - public void testMaxPages(int expected, int entriesPerPage) { - assertEquals(expected, EXAMPLE_PAGE.getMaxPages(entriesPerPage)); - } - - @ParameterizedTest - @CsvSource({ - "1, 2", - "2, 2", - "3, 1" - }) - public void testPageSize(int pageNo, int expectedSize) { - List> page = EXAMPLE_PAGE.getPage(pageNo, 2); - assertEquals(expectedSize, page.size()); - } - - private static Stream testPageContent() { - return Stream.of( - Arguments.of(1, ImmutableList.of( - new Paginated.Entry<>(1, "one"), - new Paginated.Entry<>(2, "two") - )), - Arguments.of(2, ImmutableList.of( - new Paginated.Entry<>(3, "three"), - new Paginated.Entry<>(4, "four") - )), - Arguments.of(3, ImmutableList.of( - new Paginated.Entry<>(5, "five") - )) - ); - } - - @ParameterizedTest - @MethodSource - public void testPageContent(int pageNo, List> expectedContent) { - assertEquals(expectedContent, EXAMPLE_PAGE.getPage(pageNo, 2)); - } - - @ParameterizedTest - @CsvSource({ - "4, 2", - }) - public void testFailState(int pageNo, int pageSize) { - assertThrows(IllegalStateException.class, () -> EXAMPLE_PAGE.getPage(pageNo, pageSize)); - } - - @ParameterizedTest - @CsvSource({ - "0, 2", - "-1, 2" - }) - public void testFailArgument(int pageNo, int pageSize) { - assertThrows(IllegalArgumentException.class, () -> EXAMPLE_PAGE.getPage(pageNo, pageSize)); - } - -} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java index 679a555ff..2489e48b7 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java @@ -44,7 +44,6 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerLoginNetworkHandler; import net.minecraft.server.network.ServerPlayNetworkHandler; import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.util.Uuids; import java.util.UUID; import java.util.concurrent.CompletableFuture; diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java b/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java index bcc96802e..963f6e22c 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java @@ -29,8 +29,6 @@ import com.google.common.collect.Iterables; import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.fabric.LPFabricPlugin; -import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; -import net.fabricmc.fabric.api.networking.v1.PacketSender; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.luckperms.api.messenger.IncomingMessageConsumer; @@ -39,7 +37,6 @@ import net.minecraft.network.RegistryByteBuf; import net.minecraft.network.codec.PacketCodec; import net.minecraft.network.packet.CustomPayload; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerPlayNetworkHandler; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.Identifier; diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java b/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java index 37b309b2b..2753d67e7 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java @@ -32,8 +32,6 @@ import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; import net.minecraft.server.network.ServerPlayerEntity; -import java.util.Locale; - /** * Mixin interface for {@link ServerPlayerEntity} implementing {@link User} related * caches and functions. diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java index 8cdec14c9..c5d92a1e5 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java @@ -33,8 +33,16 @@ import java.util.concurrent.CompletableFuture; */ public interface CommandExecutor { - CompletableFuture execute(String command); + CompletableFuture execute(StandaloneSender player, String command); - List tabComplete(String command); + List tabComplete(StandaloneSender player, String command); + + default CompletableFuture execute(String command) { + return execute(StandaloneUser.INSTANCE, command); + } + + default List tabComplete(String command) { + return tabComplete(StandaloneUser.INSTANCE, command); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java similarity index 68% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java rename to standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java index a8d8f9051..eea1ad817 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java @@ -23,34 +23,26 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate.query; +package me.lucko.luckperms.standalone.app.integration; + +import net.kyori.adventure.text.Component; +import net.luckperms.api.util.Tristate; import java.util.Locale; +import java.util.UUID; -/** - * Represents a field being used in an update - */ -public enum QueryField { +public interface StandaloneSender { + String getName(); - PERMISSION("permission"), - SERVER("server"), - WORLD("world"); + UUID getUniqueId(); - private final String sqlName; + void sendMessage(Component component); - public static QueryField of(String s) { - try { - return valueOf(s.toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException e) { - return null; - } - } + Tristate getPermissionValue(String permission); - QueryField(String sqlName) { - this.sqlName = sqlName; - } + boolean hasPermission(String permission); - public String getSqlName() { - return this.sqlName; - } + boolean isConsole(); + + Locale getLocale(); } diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneUser.java similarity index 56% rename from standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java rename to standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneUser.java index 0df81372b..4d98b009f 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneUser.java @@ -28,58 +28,57 @@ package me.lucko.luckperms.standalone.app.integration; import me.lucko.luckperms.standalone.app.LuckPermsApplication; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; -import net.kyori.ansi.ColorLevel; +import net.luckperms.api.util.Tristate; -import java.util.Set; +import java.util.Locale; import java.util.UUID; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.function.Consumer; /** - * Dummy/singleton player class used by the standalone plugin. - * - *

In various places (ContextManager, SenderFactory, ..) the platform "player" type is used - * as a generic parameter. This class acts as this type for the standalone plugin.

+ * The sender instance used for the console / users executing commands + * on a standalone instance of LuckPerms */ -public class SingletonPlayer { +public class StandaloneUser implements StandaloneSender { - /** Empty UUID used by the singleton player. */ private static final UUID UUID = new UUID(0, 0); - /** A message sink that prints the component to stdout */ - private static final Consumer PRINT_TO_STDOUT = component -> LuckPermsApplication.LOGGER.info(ANSIComponentSerializer.ansi().serialize(component)); + public static final StandaloneUser INSTANCE = new StandaloneUser(); - /** Singleton instance */ - public static final SingletonPlayer INSTANCE = new SingletonPlayer(); - - /** A set of message sinks that messages are delivered to */ - private final Set> messageSinks; - - private SingletonPlayer() { - this.messageSinks = new CopyOnWriteArraySet<>(); - this.messageSinks.add(PRINT_TO_STDOUT); + private StandaloneUser() { } + @Override public String getName() { return "StandaloneUser"; } + @Override public UUID getUniqueId() { return UUID; } + @Override public void sendMessage(Component component) { - for (Consumer sink : this.messageSinks) { - sink.accept(component); - } + LuckPermsApplication.LOGGER.info(ANSIComponentSerializer.ansi().serialize(component)); } - public void addMessageSink(Consumer sink) { - this.messageSinks.add(sink); + @Override + public Tristate getPermissionValue(String permission) { + return Tristate.TRUE; } - public void removeMessageSink(Consumer sink) { - this.messageSinks.remove(sink); + @Override + public boolean hasPermission(String permission) { + return true; + } + + @Override + public boolean isConsole() { + return true; + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); } } diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java index 609b785e3..3970faa0b 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java @@ -39,7 +39,7 @@ import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.standalone.app.LuckPermsApplication; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; import me.lucko.luckperms.standalone.stub.StandaloneContextManager; import me.lucko.luckperms.standalone.stub.StandaloneDummyConnectionListener; import me.lucko.luckperms.standalone.stub.StandaloneEventBus; @@ -162,7 +162,7 @@ public class LPStandalonePlugin extends AbstractLuckPermsPlugin { @Override public Sender getConsoleSender() { - return getSenderFactory().wrap(SingletonPlayer.INSTANCE); + return getSenderFactory().wrap(StandaloneUser.INSTANCE); } public StandaloneSenderFactory getSenderFactory() { diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java index 4e2c2d0a5..e1b67bce1 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java @@ -29,7 +29,7 @@ import me.lucko.luckperms.common.command.CommandManager; import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.standalone.app.integration.CommandExecutor; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -43,15 +43,15 @@ public class StandaloneCommandManager extends CommandManager implements CommandE } @Override - public CompletableFuture execute(String command) { - Sender wrapped = this.plugin.getSenderFactory().wrap(SingletonPlayer.INSTANCE); + public CompletableFuture execute(StandaloneSender player, String command) { + Sender wrapped = this.plugin.getSenderFactory().wrap(player); List arguments = ArgumentTokenizer.EXECUTE.tokenizeInput(command); return executeCommand(wrapped, "lp", arguments); } @Override - public List tabComplete(String command) { - Sender wrapped = this.plugin.getSenderFactory().wrap(SingletonPlayer.INSTANCE); + public List tabComplete(StandaloneSender player, String command) { + Sender wrapped = this.plugin.getSenderFactory().wrap(player); List arguments = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(command); return tabCompleteCommand(wrapped, arguments); } diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java index ca21170a1..eec85c5b5 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java @@ -27,53 +27,56 @@ package me.lucko.luckperms.standalone; import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.sender.SenderFactory; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; import net.kyori.adventure.text.Component; import net.luckperms.api.util.Tristate; -import java.util.Locale; import java.util.UUID; -public class StandaloneSenderFactory extends SenderFactory { +public class StandaloneSenderFactory extends SenderFactory { public StandaloneSenderFactory(LPStandalonePlugin plugin) { super(plugin); } @Override - protected String getName(SingletonPlayer sender) { + protected String getName(StandaloneSender sender) { return sender.getName(); } @Override - protected UUID getUniqueId(SingletonPlayer sender) { + protected UUID getUniqueId(StandaloneSender sender) { return sender.getUniqueId(); } @Override - protected void sendMessage(SingletonPlayer sender, Component message) { - Component rendered = TranslationManager.render(message, Locale.getDefault()); + protected void sendMessage(StandaloneSender sender, Component message) { + Component rendered = TranslationManager.render(message, sender.getLocale()); sender.sendMessage(rendered); } @Override - protected Tristate getPermissionValue(SingletonPlayer sender, String node) { - return Tristate.TRUE; + protected Tristate getPermissionValue(StandaloneSender sender, String node) { + return sender.getPermissionValue(node); } @Override - protected boolean hasPermission(SingletonPlayer sender, String node) { + protected boolean hasPermission(StandaloneSender sender, String node) { + return sender.hasPermission(node); + } + + @Override + protected void performCommand(StandaloneSender sender, String command) { + + } + + @Override + protected boolean isConsole(StandaloneSender sender) { + return sender.isConsole(); + } + + @Override + protected boolean shouldSplitNewlines(StandaloneSender sender) { return true; } - - @Override - protected void performCommand(SingletonPlayer sender, String command) { - - } - - @Override - protected boolean isConsole(SingletonPlayer sender) { - return true; - } - } diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java index 43cfde390..35d48d7bd 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java @@ -29,39 +29,45 @@ import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.context.manager.QueryOptionsCache; import me.lucko.luckperms.standalone.LPStandalonePlugin; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.query.QueryOptions; import java.util.UUID; -public class StandaloneContextManager extends ContextManager { - private final QueryOptionsCache singletonCache = new QueryOptionsCache<>(SingletonPlayer.INSTANCE, this); +public class StandaloneContextManager extends ContextManager { + private final QueryOptionsCache singletonCache = new QueryOptionsCache<>(StandaloneUser.INSTANCE, this); public StandaloneContextManager(LPStandalonePlugin plugin) { - super(plugin, SingletonPlayer.class, SingletonPlayer.class); + super(plugin, StandaloneSender.class, StandaloneSender.class); } @Override - public UUID getUniqueId(SingletonPlayer player) { + public UUID getUniqueId(StandaloneSender player) { return player.getUniqueId(); } @Override - public QueryOptionsCache getCacheFor(SingletonPlayer subject) { + public QueryOptionsCache getCacheFor(StandaloneSender subject) { if (subject == null) { throw new NullPointerException("subject"); } - return this.singletonCache; + if (subject == StandaloneUser.INSTANCE) { + return this.singletonCache; + } + + // just return a new one every time - not optimal but this case should only be hit using unit tests anyway + return new QueryOptionsCache<>(subject, this); } @Override - protected void invalidateCache(SingletonPlayer subject) { + protected void invalidateCache(StandaloneSender subject) { this.singletonCache.invalidate(); } @Override - public QueryOptions formQueryOptions(SingletonPlayer subject, ImmutableContextSet contextSet) { + public QueryOptions formQueryOptions(StandaloneSender subject, ImmutableContextSet contextSet) { QueryOptions.Builder queryOptions = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); return queryOptions.context(contextSet).build(); } diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java index 485a3ccbe..362825e84 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java @@ -29,9 +29,9 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.standalone.app.integration.CommandExecutor; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; import me.lucko.luckperms.standalone.utils.CommandTester; import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import me.lucko.luckperms.standalone.utils.TestSender; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.luckperms.api.event.log.LogNotifyEvent; import org.junit.jupiter.api.Test; @@ -54,6 +54,7 @@ public class CommandsIntegrationTest { private static final Map CONFIG = ImmutableMap.builder() .put("log-notify", "false") + .put("commands-rate-limit", "false") .build(); @Test @@ -61,7 +62,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.creategroup") .whenRunCommand("creategroup test") .thenExpect("[LP] test was successfully created.") @@ -97,9 +98,9 @@ public class CommandsIntegrationTest { """ ) - .givenHasAllPermissions() + .givenHasPermissions("luckperms.group.meta.set") .whenRunCommand("group test meta set hello world") - .clearMessageBuffer() + .thenExpect("[LP] Set meta key 'hello' to 'world' for test in context global.") .givenHasPermissions("luckperms.group.setweight") .whenRunCommand("group test setweight 10") @@ -182,7 +183,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .whenRunCommand("creategroup test") .clearMessageBuffer() @@ -262,7 +263,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .whenRunCommand("creategroup test") .whenRunCommand("creategroup test2") .whenRunCommand("creategroup test3") @@ -321,7 +322,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .whenRunCommand("creategroup test") .clearMessageBuffer() @@ -445,7 +446,7 @@ public class CommandsIntegrationTest { plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); plugin.getStorage().savePlayerData(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), "Notch").join(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.user.info") .whenRunCommand("user Luck info") .thenExpect(""" @@ -508,7 +509,7 @@ public class CommandsIntegrationTest { plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.user.permission.set") .whenRunCommand("user Luck permission set test.node true") .thenExpect("[LP] Set test.node to true for luck in context global.") @@ -587,7 +588,7 @@ public class CommandsIntegrationTest { plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); - new CommandTester(plugin, executor) + new CommandTester(executor) .whenRunCommand("creategroup test2") .whenRunCommand("creategroup test3") .clearMessageBuffer() @@ -651,7 +652,7 @@ public class CommandsIntegrationTest { plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.user.meta.info") .whenRunCommand("user Luck meta info") .thenExpect(""" @@ -773,7 +774,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.createtrack") .whenRunCommand("createtrack test1") .thenExpect("[LP] test1 was successfully created.") @@ -884,7 +885,7 @@ public class CommandsIntegrationTest { plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); - new CommandTester(plugin, executor) + new CommandTester(executor) .whenRunCommand("createtrack staff") .whenRunCommand("createtrack premium") @@ -1023,7 +1024,7 @@ public class CommandsIntegrationTest { plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); - new CommandTester(plugin, executor) + new CommandTester(executor) .whenRunCommand("creategroup test") .whenRunCommand("user Luck permission set hello.world true server=survival") .whenRunCommand("group test permission set hello.world true world=nether") @@ -1045,12 +1046,12 @@ public class CommandsIntegrationTest { .givenHasPermissions("luckperms.search") .whenRunCommand("search ~~ group.%") .thenExpect(""" - [LP] Searching for users and groups with permissions ~~ group.%... - [LP] Found 2 entries from 2 users and 0 groups. - [LP] Showing user entries: (page 1 of 1 - 2 entries) - > luck - (group.test) - true - > luck - (group.default) - true - """ + [LP] Searching for users and groups with permissions ~~ group.%... + [LP] Found 2 entries from 2 users and 0 groups. + [LP] Showing user entries: (page 1 of 1 - 2 entries) + > luck - (group.test) - true + > luck - (group.default) - true + """ ); }); } @@ -1068,14 +1069,16 @@ public class CommandsIntegrationTest { CountDownLatch completed = new CountDownLatch(1); - SingletonPlayer.INSTANCE.addMessageSink(component -> { + TestSender console = new TestSender(); + console.setConsole(true); + console.addMessageSink(component -> { String plain = PlainTextComponentSerializer.plainText().serialize(component); if (plain.contains("Bulk update completed successfully")) { completed.countDown(); } }); - new CommandTester(plugin, executor) + new CommandTester(executor, console) .whenRunCommand("creategroup moderator") .whenRunCommand("creategroup admin") .whenRunCommand("user Luck parent add moderator server=survival") @@ -1084,7 +1087,6 @@ public class CommandsIntegrationTest { .whenRunCommand("group moderator rename mod") .clearMessageBuffer() - .givenHasPermissions("luckperms.bulkupdate") .whenRunCommand("bulkupdate all update permission group.mod \"permission == group.moderator\"") .thenExpectStartsWith("[LP] Running bulk update."); @@ -1113,6 +1115,151 @@ public class CommandsIntegrationTest { ), notchUser.normalData().asSet() ); + + // test normal players are unable to use + new CommandTester(executor) + .givenHasPermissions("luckperms.bulkupdate") + .whenRunCommand("bulkupdate all update permission group.mod \"permission == group.moderator\"") + .thenExpect("[LP] The bulk update command can only be used from the console."); + }); + } + + @Test + public void testLogCommands(@TempDir Path tempDir) { + Map config = new HashMap<>(CONFIG); + config.put("log-notify", "true"); + config.put("log-synchronously-in-commands", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + UUID luckUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); + + plugin.getStorage().savePlayerData(luckUniqueId, "Luck").join(); + plugin.getStorage().savePlayerData(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), "Notch").join(); + + TestSender testSender = new TestSender(); + testSender.setUniqueId(luckUniqueId); + testSender.setName("Luck"); + + new CommandTester(executor, testSender) + .whenRunCommand("group default permission set hello"); + + new CommandTester(executor) + .whenRunCommand("creategroup moderator") + .whenRunCommand("creategroup admin") + .whenRunCommand("createtrack staff") + .whenRunCommand("user Luck parent add moderator server=survival") + .whenRunCommand("user Notch parent add moderator") + .whenRunCommand("group admin parent add moderator") + .whenRunCommand("group moderator rename mod") + .whenRunCommand("user Luck permission set test.1") + .whenRunCommand("user Luck permission set test.2 false") + .whenRunCommand("user Luck permission set test.3 server=survival") + .whenRunCommand("user Luck permission settemp test.4 true 1h") + .whenRunCommand("user Luck permission settemp test.5 false 2d") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.log.userhistory") + .whenRunCommand("log userhistory Luck") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing history for user luck (page 1 of 1) + [LP] #1 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.5 false 2d + [LP] #2 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.4 true 1h + [LP] #3 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.3 true server=survival + [LP] #4 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.2 false + [LP] #5 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.1 true + [LP] #6 (1m ago) (StandaloneUser) [U] (luck) + [LP] > parent add moderator server=survival + """ + ) + + .givenHasPermissions("luckperms.log.grouphistory") + .whenRunCommand("log grouphistory admin") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing history for group admin (page 1 of 1) + [LP] #1 (1m ago) (StandaloneUser) [G] (admin) + [LP] > parent add moderator + [LP] #2 (1m ago) (StandaloneUser) [G] (admin) + [LP] > create + """ + ) + + .givenHasPermissions("luckperms.log.trackhistory") + .whenRunCommand("log trackhistory staff") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing history for track staff (page 1 of 1) + [LP] #1 (1m ago) (StandaloneUser) [T] (staff) + [LP] > create + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions (page 1 of 2) + [LP] #1 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.5 false 2d + [LP] #2 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.4 true 1h + [LP] #3 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.3 true server=survival + [LP] #4 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.2 false + [LP] #5 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.1 true + [LP] #6 (1m ago) (StandaloneUser) [G] (moderator) + [LP] > rename mod + [LP] #7 (1m ago) (StandaloneUser) [G] (admin) + [LP] > parent add moderator + [LP] #8 (1m ago) (StandaloneUser) [U] (notch) + [LP] > parent add moderator + [LP] #9 (1m ago) (StandaloneUser) [U] (luck) + [LP] > parent add moderator server=survival + [LP] #10 (1m ago) (StandaloneUser) [T] (staff) + [LP] > create + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent 2") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions (page 2 of 2) + [LP] #11 (1m ago) (StandaloneUser) [G] (admin) + [LP] > create + [LP] #12 (1m ago) (StandaloneUser) [G] (moderator) + [LP] > create + [LP] #13 (1m ago) (Luck) [G] (default) + [LP] > permission set hello true + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent 3") + .thenExpect("[LP] Invalid page number. Please enter a value between 1 and 2.") + + .givenHasPermissions("luckperms.log.search") + .whenRunCommand("log search hello") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions for query hello (page 1 of 1) + [LP] #1 (1m ago) (Luck) [G] (default) + [LP] > permission set hello true + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent Luck") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions by Luck (page 1 of 1) + [LP] #1 (1m ago) (Luck) [G] (default) + [LP] > permission set hello true + """ + ); }); } @@ -1121,7 +1268,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.user.info") .whenRunCommand("user unknown info") .thenExpect("[LP] A user for unknown could not be found.") @@ -1154,7 +1301,7 @@ public class CommandsIntegrationTest { CommandExecutor executor = app.getCommandExecutor(); String version = "v" + bootstrap.getVersion(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions(/* empty */) .whenRunCommand("") @@ -1188,7 +1335,10 @@ public class CommandsIntegrationTest { // by default, notifications are not sent to the user who initiated the event - override that app.getApi().getEventBus().subscribe(LogNotifyEvent.class, e -> e.setCancelled(false)); - new CommandTester(plugin, executor) + TestSender sender = new TestSender(); + plugin.addOnlineSender(sender); + + new CommandTester(executor, sender) .givenHasPermissions("luckperms.group.permission.set", "luckperms.log.notify") .whenRunCommand("group default permission set hello.world true server=test") .thenExpect(""" @@ -1208,7 +1358,7 @@ public class CommandsIntegrationTest { TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { CommandExecutor executor = app.getCommandExecutor(); - new CommandTester(plugin, executor) + new CommandTester(executor) .givenHasPermissions("luckperms.group.permission.set") .whenRunCommand("group default permission set hello.world true server=test") .thenExpect("[LP] You do not have permission to use this command!") diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java index 4fb253d58..42cdb965c 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java @@ -28,8 +28,10 @@ package me.lucko.luckperms.standalone; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; @@ -182,7 +184,7 @@ public class StorageIntegrationTest { assertNotNull(testTrack); assertEquals(ImmutableList.of("default", "test"), track.getGroups()); - Log actionLog = plugin.getStorage().getLog().join(); + LogPage actionLog = plugin.getStorage().getLogPage(FilterList.empty(), new PageParameters(1000, 1)).join(); assertTrue(actionLog.getContent().contains(exampleLogEntry)); List> groupSearchResult = plugin.getStorage().searchGroupNodes(StandardNodeMatchers.key(TEST_PERMISSION_1)).join(); diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java index 28b43f3b2..1d88138ff 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java @@ -36,7 +36,7 @@ import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.webeditor.WebEditorRequest; import me.lucko.luckperms.common.webeditor.WebEditorSession; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; import me.lucko.luckperms.standalone.utils.TestPluginProvider; import net.luckperms.api.model.data.DataType; import okhttp3.OkHttpClient; @@ -115,7 +115,7 @@ public class WebEditorIntegrationTest { assertFalse(holders.isEmpty()); // create a new editor session - Sender sender = plugin.getSenderFactory().wrap(SingletonPlayer.INSTANCE); + Sender sender = plugin.getSenderFactory().wrap(StandaloneUser.INSTANCE); WebEditorSession session = WebEditorSession.create(holders, Collections.emptyList(), sender, "lp", plugin); String bytebinKey = session.open(); String bytesocksKey = session.getSocket().getSocket().channelId(); diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java index 97d85a0fc..3b9d29f79 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java @@ -26,14 +26,12 @@ package me.lucko.luckperms.standalone.utils; import me.lucko.luckperms.standalone.app.integration.CommandExecutor; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; -import me.lucko.luckperms.standalone.utils.TestPluginBootstrap.TestPlugin; -import me.lucko.luckperms.standalone.utils.TestPluginBootstrap.TestSenderFactory; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.luckperms.api.util.Tristate; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.intellij.lang.annotations.RegExp; import java.util.ArrayList; import java.util.Collections; @@ -57,12 +55,12 @@ public final class CommandTester implements Consumer, Function permissions = null; @@ -72,9 +70,16 @@ public final class CommandTester implements Consumer, Function messageBuffer = Collections.synchronizedList(new ArrayList<>()); - public CommandTester(TestPlugin plugin, CommandExecutor executor) { - this.plugin = plugin; + public CommandTester(CommandExecutor executor, TestSender sender) { this.executor = executor; + this.sender = sender; + + this.sender.setPermissionChecker(this); + this.sender.addMessageSink(this); + } + + public CommandTester(CommandExecutor executor) { + this(executor, new TestSender()); } /** @@ -138,15 +143,7 @@ public final class CommandTester implements Consumer, Function, Function *
    *
  • Dependency loading system is replaced with a no-op stub that delegates to the test classloader
  • - *
  • Sender factory is extended and allows for permission checks to be intercepted
  • + *
  • Ability to register additional sender instances as being online
  • *
*

*/ @@ -81,7 +79,7 @@ public final class TestPluginBootstrap extends LPStandaloneBootstrap { } public static final class TestPlugin extends LPStandalonePlugin { - private TestSenderFactory senderFactory; + private final Set onlineSenders = new CopyOnWriteArraySet<>(); TestPlugin(LPStandaloneBootstrap bootstrap) { super(bootstrap); @@ -93,13 +91,15 @@ public final class TestPluginBootstrap extends LPStandaloneBootstrap { } @Override - protected void setupSenderFactory() { - this.senderFactory = new TestSenderFactory(this); + public Stream getOnlineSenders() { + return Stream.concat( + Stream.of(StandaloneUser.INSTANCE), + this.onlineSenders.stream() + ).map(player -> getSenderFactory().wrap(player)); } - @Override - public TestSenderFactory getSenderFactory() { - return this.senderFactory; + public void addOnlineSender(StandaloneSender player) { + this.onlineSenders.add(player); } } @@ -125,46 +125,4 @@ public final class TestPluginBootstrap extends LPStandaloneBootstrap { } } - - public static final class TestSenderFactory extends StandaloneSenderFactory { - - private Function permissionChecker; - - public TestSenderFactory(LPStandalonePlugin plugin) { - super(plugin); - } - - public void setPermissionChecker(Function permissionChecker) { - this.permissionChecker = permissionChecker; - } - - public void resetPermissionChecker() { - this.permissionChecker = null; - } - - @Override - protected boolean consoleHasAllPermissions() { - return false; - } - - @Override - protected void sendMessage(SingletonPlayer sender, Component message) { - Component rendered = TranslationManager.render(message, Locale.ENGLISH); - sender.sendMessage(rendered); - } - - @Override - protected Tristate getPermissionValue(SingletonPlayer sender, String node) { - return this.permissionChecker == null - ? super.getPermissionValue(sender, node) - : this.permissionChecker.apply(node); - } - - @Override - protected boolean hasPermission(SingletonPlayer sender, String node) { - return this.permissionChecker == null - ? super.hasPermission(sender, node) - : this.permissionChecker.apply(node).asBoolean(); - } - } } diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java new file mode 100644 index 000000000..e1723dea5 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java @@ -0,0 +1,113 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone.utils; + +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; +import net.kyori.adventure.text.Component; +import net.luckperms.api.util.Tristate; + +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; +import java.util.function.Function; + +public class TestSender implements StandaloneSender { + + private final Set> messageSinks; + + private String name = "StandaloneUser"; + private UUID uniqueId = UUID.randomUUID(); + private boolean isConsole = false; + + private Function permissionChecker; + + public TestSender() { + this.messageSinks = new CopyOnWriteArraySet<>(); + this.messageSinks.add(StandaloneUser.INSTANCE::sendMessage); + } + + @Override + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public UUID getUniqueId() { + return this.uniqueId; + } + + public void setUniqueId(UUID uuid) { + this.uniqueId = uuid; + } + + @Override + public void sendMessage(Component component) { + for (Consumer sink : this.messageSinks) { + sink.accept(component); + } + } + + @Override + public Tristate getPermissionValue(String permission) { + return this.permissionChecker == null + ? Tristate.TRUE + : this.permissionChecker.apply(permission); + } + + @Override + public boolean hasPermission(String permission) { + return getPermissionValue(permission).asBoolean(); + } + + @Override + public boolean isConsole() { + return this.isConsole; + } + + public void setConsole(boolean console) { + this.isConsole = console; + } + + @Override + public Locale getLocale() { + return Locale.ENGLISH; + } + + public void setPermissionChecker(Function permissionChecker) { + this.permissionChecker = permissionChecker; + } + + public void addMessageSink(Consumer sink) { + this.messageSinks.add(sink); + } +}