From 8624d86793d6fbe6d28fe6b9a3fcfd31b773a4c4 Mon Sep 17 00:00:00 2001 From: Risto Lahtela <24460436+AuroraLS3@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:05:05 +0200 Subject: [PATCH] Added a Join Address filter to query page --- .../plan/modules/FiltersModule.java | 4 ++ .../plan/settings/locale/Locale.java | 3 +- .../plan/settings/locale/lang/JSLang.java | 1 + .../filter/filters/JoinAddressFilter.java | 53 ++++++++++++++++ .../queries/objects/UserInfoQueries.java | 62 +++++++++++++++++-- .../UserInfoHostnameAllowNullPatch.java | 3 +- .../resources/assets/plan/web/js/filters.js | 12 ++++ .../database/queries/UserInfoQueriesTest.java | 22 +++++++ 8 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/JoinAddressFilter.java diff --git a/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java b/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java index 54b6159ea..12b72cded 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/modules/FiltersModule.java @@ -45,4 +45,8 @@ public interface FiltersModule { @IntoSet Filter filter5(ActivityIndexFilter filter); + @Binds + @IntoSet + Filter filter6(JoinAddressFilter filter); + } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/Locale.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/Locale.java index a3a6f2636..f024e1aab 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/Locale.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/Locale.java @@ -190,7 +190,8 @@ public class Locale extends HashMap { HtmlLang.LABEL_AVG_AFK_TIME, HtmlLang.LABEL_AVG_PLAYTIME, HtmlLang.SIDE_GEOLOCATIONS, - HtmlLang.LABEL_PER_PLAYER + HtmlLang.LABEL_PER_PLAYER, + HtmlLang.TITLE_JOIN_ADDRESSES }) { getNonDefault(extra).ifPresent(replacement -> diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java index 02b5bcfc7..e87cf968f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java @@ -41,6 +41,7 @@ public enum JSLang implements Lang { LABEL_WEEK_DAYS("'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"), QUERY_ARE_ACTIVITY_GROUP("are in Activity Groups"), + QUERY_JOINED_WITH_ADDRESS("joined with address"), QUERY_ARE_PLUGIN_GROUP("are in ${plugin}'s ${group} Groups"), QUERY_OF_PLAYERS("of Players who "), QUERY_AND("and "), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/JoinAddressFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/JoinAddressFilter.java new file mode 100644 index 000000000..162b2eac2 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/JoinAddressFilter.java @@ -0,0 +1,53 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.storage.database.queries.filter.filters; + +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.queries.filter.SpecifiedFilterInformation; +import com.djrapitops.plan.storage.database.queries.objects.UserInfoQueries; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.*; + +@Singleton +public class JoinAddressFilter extends MultiOptionFilter { + + private final DBSystem dbSystem; + + @Inject + public JoinAddressFilter(DBSystem dbSystem) {this.dbSystem = dbSystem;} + + @Override + public String getKind() { + return "joinAddresses"; + } + + @Override + public Map getOptions() { + return Collections.singletonMap("options", getSelectionOptions()); + } + + private List getSelectionOptions() { + return dbSystem.getDatabase().query(UserInfoQueries.uniqueJoinAddresses()); + } + + @Override + public Set getMatchingUUIDs(SpecifiedFilterInformation query) { + return dbSystem.getDatabase().query(UserInfoQueries.uuidsOfPlayersWithJoinAddresses(getSelected(query))); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/UserInfoQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/UserInfoQueries.java index 618d8cde5..77037feee 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/UserInfoQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/UserInfoQueries.java @@ -23,6 +23,7 @@ import com.djrapitops.plan.storage.database.queries.QueryAllStatement; import com.djrapitops.plan.storage.database.queries.QueryStatement; import com.djrapitops.plan.storage.database.sql.tables.UserInfoTable; import com.djrapitops.plan.utilities.java.Lists; +import org.apache.commons.text.TextStringBuilder; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -251,6 +252,25 @@ public class UserInfoQueries { }; } + public static Query> uniqueJoinAddresses() { + String sql = SELECT + DISTINCT + "LOWER(COALESCE(" + UserInfoTable.JOIN_ADDRESS + ", ?)) as address" + + FROM + UserInfoTable.TABLE_NAME + + ORDER_BY + UserInfoTable.JOIN_ADDRESS + " DESC"; + return new QueryStatement>(sql, 100) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, "unknown"); + } + + @Override + public List processResults(ResultSet set) throws SQLException { + List joinAddresses = new ArrayList<>(); + while (set.next()) joinAddresses.add(set.getString("address")); + return joinAddresses; + } + }; + } + public static Query> uuidsOfOperators() { return getUUIDsForBooleanGroup(UserInfoTable.OP, true); } @@ -266,15 +286,19 @@ public class UserInfoQueries { @Override public Set processResults(ResultSet set) throws SQLException { - Set uuids = new HashSet<>(); - while (set.next()) { - uuids.add(UUID.fromString(set.getString(UserInfoTable.USER_UUID))); - } - return uuids; + return extractUUIDs(set); } }; } + public static Set extractUUIDs(ResultSet set) throws SQLException { + Set uuids = new HashSet<>(); + while (set.next()) { + uuids.add(UUID.fromString(set.getString(UserInfoTable.USER_UUID))); + } + return uuids; + } + public static Query> uuidsOfNonOperators() { return getUUIDsForBooleanGroup(UserInfoTable.OP, false); } @@ -286,4 +310,32 @@ public class UserInfoQueries { public static Query> uuidsOfNotBanned() { return getUUIDsForBooleanGroup(UserInfoTable.BANNED, false); } + + public static Query> uuidsOfPlayersWithJoinAddresses(List joinAddresses) { + String selectLowercaseJoinAddresses = SELECT + + UserInfoTable.USER_UUID + ',' + + "LOWER(COALESCE(" + UserInfoTable.JOIN_ADDRESS + ", ?)) as address" + + FROM + UserInfoTable.TABLE_NAME; + String sql = SELECT + DISTINCT + UserInfoTable.USER_UUID + + FROM + '(' + selectLowercaseJoinAddresses + ") q1" + + WHERE + "address IN (" + + new TextStringBuilder().appendWithSeparators(joinAddresses.stream().map(item -> '?').iterator(), ",") + + ')'; // Don't append addresses directly, SQL incjection hazard + + return new QueryStatement>(sql) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, "unknown"); + for (int i = 1; i <= joinAddresses.size(); i++) { + String address = joinAddresses.get(i - 1); + statement.setString(i + 1, address); + } + } + + @Override + public Set processResults(ResultSet set) throws SQLException { + return extractUUIDs(set); + } + }; + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UserInfoHostnameAllowNullPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UserInfoHostnameAllowNullPatch.java index d1ff45295..4ee5a4c04 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UserInfoHostnameAllowNullPatch.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UserInfoHostnameAllowNullPatch.java @@ -21,6 +21,7 @@ import com.djrapitops.plan.storage.database.transactions.ExecStatement; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Types; import static com.djrapitops.plan.storage.database.sql.building.Sql.FROM; @@ -66,7 +67,7 @@ public class UserInfoHostnameAllowNullPatch extends Patch { ) { @Override public void prepare(PreparedStatement statement) throws SQLException { - statement.setString(1, null); + statement.setNull(1, Types.VARCHAR); } }); diff --git a/Plan/common/src/main/resources/assets/plan/web/js/filters.js b/Plan/common/src/main/resources/assets/plan/web/js/filters.js index eb350a4dd..151e5cd76 100644 --- a/Plan/common/src/main/resources/assets/plan/web/js/filters.js +++ b/Plan/common/src/main/resources/assets/plan/web/js/filters.js @@ -78,6 +78,14 @@ class OperatorsFilter extends MultipleChoiceFilter { } } +class JoinAddressFilter extends MultipleChoiceFilter { + constructor( + id, options + ) { + super(id, "joinAddresses", `joined with address`, options); + } +} + class PluginGroupsFilter extends MultipleChoiceFilter { constructor( id, kind, options @@ -187,6 +195,8 @@ function createFilter(filter, id) { return new BannedFilter(id, filter.options); case "operators": return new OperatorsFilter(id, filter.options); + case "joinAddresses": + return new JoinAddressFilter(id, filter.options); case "playedBetween": return new PlayedBetweenFilter(id, filter.options); case "registeredBetween": @@ -209,6 +219,8 @@ function getReadableFilterName(filter) { return "Ban status"; case "operators": return "Operator status"; + case "joinAddresses": + return "Join Addresses"; case "playedBetween": return "Played between"; case "registeredBetween": diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/UserInfoQueriesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/UserInfoQueriesTest.java index 40c81ca54..f9e9395fa 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/UserInfoQueriesTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/UserInfoQueriesTest.java @@ -309,4 +309,26 @@ public interface UserInfoQueriesTest extends DatabaseTestPreparer { Map result = db().query(UserInfoQueries.joinAddresses()); assertEquals(expected, result); } + + @Test + default void joinAddressFilterUUIDsAreFetched() { + joinAddressIsUpdatedUponSecondLogin(); + + Set expected = Collections.singleton(playerUUID); + Set result = db().query(UserInfoQueries.uuidsOfPlayersWithJoinAddresses( + Collections.singletonList(TestConstants.GET_PLAYER_HOSTNAME.get().toLowerCase())) + ); + assertEquals(expected, result); + } + + @Test + default void joinAddressFilterUUIDsAreFetchedWhenUnknown() { + joinAddressCanBeNull(); + + Set expected = Collections.singleton(playerUUID); + Set result = db().query(UserInfoQueries.uuidsOfPlayersWithJoinAddresses( + Collections.singletonList("unknown")) + ); + assertEquals(expected, result); + } }