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 34bbf110c..b8cdba19d 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 @@ -53,4 +53,8 @@ public interface FiltersModule { @IntoSet Filter filter7(GeolocationsFilter filter); + @Binds + @IntoSet + Filter filter8(PluginBooleanGroupFilter filter); + } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/PluginBooleanGroupFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/PluginBooleanGroupFilter.java new file mode 100644 index 000000000..930d6cbba --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/PluginBooleanGroupFilter.java @@ -0,0 +1,248 @@ +/* + * 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.identification.Server; +import com.djrapitops.plan.identification.ServerUUID; +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.Database; +import com.djrapitops.plan.storage.database.queries.Query; +import com.djrapitops.plan.storage.database.queries.QueryAllStatement; +import com.djrapitops.plan.storage.database.queries.QueryStatement; +import com.djrapitops.plan.storage.database.queries.filter.SpecifiedFilterInformation; +import com.djrapitops.plan.storage.database.queries.objects.ServerQueries; +import com.djrapitops.plan.storage.database.sql.tables.ExtensionPlayerValueTable; +import com.djrapitops.plan.storage.database.sql.tables.ExtensionPluginTable; +import com.djrapitops.plan.storage.database.sql.tables.ExtensionProviderTable; +import com.djrapitops.plan.storage.database.sql.tables.ServerTable; +import org.apache.commons.lang3.StringUtils; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +import static com.djrapitops.plan.storage.database.sql.building.Sql.*; + +@Singleton +public class PluginBooleanGroupFilter extends MultiOptionFilter { + + private final DBSystem dbSystem; + + @Inject + public PluginBooleanGroupFilter(DBSystem dbSystem) { + this.dbSystem = dbSystem; + } + + private static Query> pluginBooleanOptionsQuery() { + String selectOptions = SELECT + + "server." + ServerTable.SERVER_ID + " as server_id," + + "server." + ServerTable.NAME + " as server_name," + + "plugin." + ExtensionPluginTable.PLUGIN_NAME + " as plugin_name," + + "provider." + ExtensionProviderTable.TEXT + " as provider_text" + + FROM + ServerTable.TABLE_NAME + " server" + + INNER_JOIN + ExtensionPluginTable.TABLE_NAME + " plugin on plugin." + ExtensionPluginTable.SERVER_UUID + "=server." + ServerTable.SERVER_UUID + + INNER_JOIN + ExtensionProviderTable.TABLE_NAME + " provider on provider." + ExtensionProviderTable.PLUGIN_ID + "=plugin." + ExtensionPluginTable.ID + + INNER_JOIN + ExtensionPlayerValueTable.TABLE_NAME + " value on value." + ExtensionPlayerValueTable.PROVIDER_ID + "=provider." + ExtensionProviderTable.ID + + WHERE + "value." + ExtensionPlayerValueTable.BOOLEAN_VALUE + " IS NOT NULL" + + ORDER_BY + "server_name ASC, plugin_name ASC, provider_text ASC"; + return new QueryAllStatement>(selectOptions) { + @Override + public List processResults(ResultSet set) throws SQLException { + List options = new ArrayList<>(); + while (set.next()) { + int serverId = set.getInt("server_id"); + String serverName = set.getString("server_name"); + String pluginName = set.getString("plugin_name"); + String providerText = set.getString("provider_text"); + options.add(new PluginBooleanOption( + Server.getIdentifiableName(serverName, serverId), + pluginName, + providerText + )); + } + Collections.sort(options); + return options; + } + }; + } + + private static Query> playersInGroups( + Map selected, + Map namesToUUIDs + ) { + return db -> { + Set playerUUIDs = new HashSet<>(); + for (Map.Entry option : selected.entrySet()) { + PluginBooleanOption pluginBooleanOption = option.getKey(); + SelectedBoolean selectedBoolean = option.getValue(); + playerUUIDs.addAll( + db.query(playersInGroup( + namesToUUIDs.get(pluginBooleanOption.getServerName()), + pluginBooleanOption.getPluginName(), + pluginBooleanOption.getProviderText(), + selectedBoolean + )) + ); + } + return playerUUIDs; + }; + } + + private static Query> playersInGroup( + ServerUUID serverUUID, String pluginName, String providerText, SelectedBoolean selectedBoolean + ) { + String selectUUIDsWithBooleanValues = SELECT + DISTINCT + "value." + ExtensionPlayerValueTable.USER_UUID + " as uuid" + + FROM + ExtensionPluginTable.TABLE_NAME + " plugin" + + INNER_JOIN + ExtensionProviderTable.TABLE_NAME + " provider on provider." + ExtensionProviderTable.PLUGIN_ID + "=plugin." + ExtensionPluginTable.ID + + INNER_JOIN + ExtensionPlayerValueTable.TABLE_NAME + " value on value." + ExtensionPlayerValueTable.PROVIDER_ID + "=provider." + ExtensionProviderTable.ID + + WHERE + "plugin." + ExtensionPluginTable.SERVER_UUID + "=?" + + AND + "plugin." + ExtensionPluginTable.PLUGIN_NAME + "=?" + + AND + "provider." + ExtensionProviderTable.TEXT + "=?" + + AND + "value." + ExtensionPlayerValueTable.BOOLEAN_VALUE + (selectedBoolean == SelectedBoolean.BOTH ? "IS NOT NULL" : "=?"); + + return new QueryStatement>(selectUUIDsWithBooleanValues) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, serverUUID.toString()); + statement.setString(2, pluginName); + statement.setString(3, providerText); + if (selectedBoolean != SelectedBoolean.BOTH) { + statement.setBoolean(4, selectedBoolean == SelectedBoolean.TRUE); + } + } + + @Override + public Set processResults(ResultSet set) throws SQLException { + Set uuids = new HashSet<>(); + while (set.next()) { + uuids.add(UUID.fromString(set.getString("uuid"))); + } + return uuids; + } + }; + } + + @Override + public String getKind() { + return "pluginsBooleanGroups"; + } + + private List getOptionList() { + Database database = dbSystem.getDatabase(); + List pluginBooleanOptions = database.query(pluginBooleanOptionsQuery()); + + List options = new ArrayList<>(); + for (PluginBooleanOption pluginBooleanOption : pluginBooleanOptions) { + String names = pluginBooleanOption.format(); + options.add(names + ": true"); + options.add(names + ": false"); + } + + return options; + } + + @Override + public Map getOptions() { + return Collections.singletonMap("options", getOptionList()); + } + + @Override + public Set getMatchingUUIDs(SpecifiedFilterInformation query) { + Map selectedBooleanOptions = new HashMap<>(); + for (String selected : getSelected(query)) { + String[] optionAndBoolean = StringUtils.split(selected, ":", 2); + PluginBooleanOption pluginBooleanOption = PluginBooleanOption.parse(optionAndBoolean[0].trim()); + String selectedBoolean = optionAndBoolean[1].trim().toUpperCase(); + selectedBooleanOptions.computeIfPresent(pluginBooleanOption, (key, existing) -> SelectedBoolean.BOTH); + selectedBooleanOptions.computeIfAbsent(pluginBooleanOption, key -> SelectedBoolean.valueOf(selectedBoolean)); + } + + Database db = dbSystem.getDatabase(); + Map namesToUUIDs = db.query(ServerQueries.fetchServerNamesToUUIDs()); + return db.query(playersInGroups(selectedBooleanOptions, namesToUUIDs)); + } + + public enum SelectedBoolean { + TRUE, + FALSE, + BOTH + } + + private static class PluginBooleanOption implements Comparable { + private final String serverName; + private final String pluginName; + private final String providerText; + + public PluginBooleanOption(String serverName, String pluginName, String providerText) { + this.serverName = serverName; + this.pluginName = pluginName; + this.providerText = providerText; + } + + public static PluginBooleanOption parse(String fromFormatted) { + String[] split1 = StringUtils.split(fromFormatted, ",", 2); + String[] split2 = StringUtils.split(split1[1], "-", 2); + String serverName = split1[0].trim(); + String pluginName = split2[0].trim(); + String providerName = split2[1].trim(); + return new PluginBooleanOption(serverName, pluginName, providerName); + } + + public String getServerName() { + return serverName; + } + + public String getPluginName() { + return pluginName; + } + + public String getProviderText() { + return providerText; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PluginBooleanOption that = (PluginBooleanOption) o; + return Objects.equals(getServerName(), that.getServerName()) && Objects.equals(getPluginName(), that.getPluginName()) && Objects.equals(getProviderText(), that.getProviderText()); + } + + @Override + public int hashCode() { + return Objects.hash(getServerName(), getPluginName(), getProviderText()); + } + + @Override + public int compareTo(PluginBooleanOption o) { + int serverNameAlphabetical = String.CASE_INSENSITIVE_ORDER.compare(serverName, o.serverName); + if (serverNameAlphabetical != 0) return serverNameAlphabetical; + + int pluginNameAlphabetical = String.CASE_INSENSITIVE_ORDER.compare(pluginName, o.pluginName); + if (pluginNameAlphabetical != 0) return pluginNameAlphabetical; + + return String.CASE_INSENSITIVE_ORDER.compare(providerText, o.providerText); + } + + public String format() { + return serverName + ", " + pluginName + " - " + providerText; + } + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java index e2a143714..33bb85782 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/ServerQueries.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.building.Select; import com.djrapitops.plan.storage.database.sql.tables.ServerTable; +import com.djrapitops.plan.utilities.java.Maps; import org.apache.commons.lang3.math.NumberUtils; import java.sql.PreparedStatement; @@ -148,7 +149,7 @@ public class ServerQueries { public static Query> fetchServerNames() { String sql = Select.from(ServerTable.TABLE_NAME, - ServerTable.SERVER_UUID, ServerTable.NAME) + ServerTable.SERVER_ID, ServerTable.SERVER_UUID, ServerTable.NAME) .toString(); return new QueryAllStatement>(sql) { @@ -157,7 +158,7 @@ public class ServerQueries { Map names = new HashMap<>(); while (set.next()) { ServerUUID serverUUID = ServerUUID.fromString(set.getString(ServerTable.SERVER_UUID)); - names.put(serverUUID, set.getString(ServerTable.NAME)); + names.put(serverUUID, Server.getIdentifiableName(set.getString(ServerTable.NAME), set.getInt(ServerTable.SERVER_ID))); } return names; } @@ -233,4 +234,8 @@ public class ServerQueries { } }; } + + public static Query> fetchServerNamesToUUIDs() { + return db -> Maps.reverse(db.query(fetchServerNames())); + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/utilities/java/Maps.java b/Plan/common/src/main/java/com/djrapitops/plan/utilities/java/Maps.java index e69cfb623..cac71d1ee 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/utilities/java/Maps.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/utilities/java/Maps.java @@ -44,6 +44,14 @@ public class Maps { return new Builder<>(); } + public static Map reverse(Map map) { + Map reversed = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + reversed.put(entry.getValue(), entry.getKey()); + } + return reversed; + } + public static class Builder { private final Map map; 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 20624c443..4b1a407f1 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 @@ -102,6 +102,14 @@ class geolocationsFilter extends MultipleChoiceFilter { } } +class PluginBooleanGroupsFilter extends MultipleChoiceFilter { + constructor( + id, options + ) { + super(id, "pluginsBooleanGroups", `have Plugin boolean value`, options); + } +} + class PluginGroupsFilter extends MultipleChoiceFilter { constructor( id, kind, options @@ -220,6 +228,8 @@ function createFilter(filter, id) { return new PlayedBetweenFilter(id, filter.options); case "registeredBetween": return new RegisteredBetweenFilter(id, filter.options); + case "pluginsBooleanGroups": + return new PluginBooleanGroupsFilter(id, filter.options); default: throw new Error("Unsupported filter kind: '" + filter.kind + "'"); } @@ -246,6 +256,8 @@ function getReadableFilterName(filter) { return "Played between"; case "registeredBetween": return "Registered between"; + case "pluginsBooleanGroups": + return "Has plugin boolean value"; default: return filter.kind; }