diff --git a/Plan/api/src/main/java/com/djrapitops/plan/extension/icon/Icon.java b/Plan/api/src/main/java/com/djrapitops/plan/extension/icon/Icon.java index 277510d6e..8fc722d13 100644 --- a/Plan/api/src/main/java/com/djrapitops/plan/extension/icon/Icon.java +++ b/Plan/api/src/main/java/com/djrapitops/plan/extension/icon/Icon.java @@ -64,8 +64,9 @@ public class Icon { return color; } - public void setColor(Color color) { + public Icon setColor(Color color) { this.color = color; + return this; } @Override diff --git a/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/results/ExtensionTabData.java b/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/results/ExtensionTabData.java index 7ef1e5997..b46624713 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/results/ExtensionTabData.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/results/ExtensionTabData.java @@ -39,6 +39,7 @@ public class ExtensionTabData implements Comparable { private final List tableData; private List order; + private List descriptives; // Table and Graph data will be added later. @@ -52,6 +53,7 @@ public class ExtensionTabData implements Comparable { stringData = new HashMap<>(); tableData = new ArrayList<>(); + descriptives = new ArrayList<>(); } public TabInformation getTabInformation() { @@ -86,6 +88,17 @@ public class ExtensionTabData implements Comparable { return tableData; } + /** + * Get all Descriptives for this tabs data. + *

+ * Only available after the Tab has been built. + * + * @return List of {@link ExtensionDescriptive}s. + */ + public List getDescriptives() { + return descriptives; + } + @Override public int compareTo(ExtensionTabData other) { return Integer.compare(this.tabInformation.getTabPriority(), other.tabInformation.getTabPriority()); // Lower is first @@ -118,7 +131,6 @@ public class ExtensionTabData implements Comparable { } private void createOrderingList() { - List descriptives = new ArrayList<>(); booleanData.values().stream().map(ExtensionData::getDescriptive).forEach(descriptives::add); doubleData.values().stream().map(ExtensionData::getDescriptive).forEach(descriptives::add); percentageData.values().stream().map(ExtensionData::getDescriptive).forEach(descriptives::add); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/response/data/JSONResponse.java b/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/response/data/JSONResponse.java index 3a8354511..60aa5f062 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/response/data/JSONResponse.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/response/data/JSONResponse.java @@ -14,7 +14,6 @@ public class JSONResponse extends Response { public JSONResponse(T object) { super(ResponseType.JSON); - super.setHeader("HTTP/1.1 200 OK"); try { @@ -27,4 +26,10 @@ public class JSONResponse extends Response { super.setContent("{\"error\":\"Gson for json responses not available on this server: " + e.toString() + "\"}"); } } + + public JSONResponse(String jsonString) { + super(ResponseType.JSON); + super.setHeader("HTTP/1.1 200 OK"); + super.setContent(jsonString); + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableJSONParser.java b/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableJSONParser.java new file mode 100644 index 000000000..91768e398 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableJSONParser.java @@ -0,0 +1,235 @@ +package com.djrapitops.plan.utilities.html.tables; + +import com.djrapitops.plan.api.PlanAPI; +import com.djrapitops.plan.data.container.GeoInfo; +import com.djrapitops.plan.data.store.containers.PlayerContainer; +import com.djrapitops.plan.data.store.keys.PlayerKeys; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; +import com.djrapitops.plan.data.store.mutators.GeoInfoMutator; +import com.djrapitops.plan.data.store.mutators.SessionsMutator; +import com.djrapitops.plan.extension.FormatType; +import com.djrapitops.plan.extension.icon.Color; +import com.djrapitops.plan.extension.implementation.results.*; +import com.djrapitops.plan.utilities.comparators.PlayerContainerLastPlayedComparator; +import com.djrapitops.plan.utilities.formatting.Formatter; +import com.djrapitops.plan.utilities.formatting.Formatters; +import com.djrapitops.plan.utilities.html.Html; +import com.djrapitops.plan.utilities.html.icon.Family; +import com.djrapitops.plan.utilities.html.icon.Icon; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Parsing utility for creating jQuery Datatables JSON for a Players Table. + *

+ * See https://www.datatables.net/manual/data/orthogonal-data#HTML-5 for sort kinds + * + * @author Rsl1122 + */ +public class PlayersTableJSONParser { + + private final List players; + private final List extensionDescriptives; + private final Map extensionData; + + private final int maxPlayers; + private final long activeMsThreshold; + private final int activeLoginThreshold; + private final boolean openPlayerPageInNewTab; + + private Map> numberFormatters; + + private Formatter decimalFormatter; + private Formatter percentageFormatter; + + public PlayersTableJSONParser( + // Data + List players, + Map extensionData, + // Settings + int maxPlayers, long activeMsThreshold, int activeLoginThreshold, boolean openPlayerPageInNewTab, + // Formatters + Formatters formatters + ) { + // Data + this.players = players; + this.extensionData = extensionData; + extensionDescriptives = extensionData.values().stream() + .map(ExtensionTabData::getDescriptives) + .flatMap(Collection::stream) + .distinct().sorted() + .collect(Collectors.toList()); + // Settings + this.maxPlayers = maxPlayers; + this.activeMsThreshold = activeMsThreshold; + this.activeLoginThreshold = activeLoginThreshold; + this.openPlayerPageInNewTab = openPlayerPageInNewTab; + // Formatters + numberFormatters = new EnumMap<>(FormatType.class); + numberFormatters.put(FormatType.DATE_SECOND, formatters.secondLong()); + numberFormatters.put(FormatType.DATE_YEAR, formatters.yearLong()); + numberFormatters.put(FormatType.TIME_MILLISECONDS, formatters.timeAmount()); + numberFormatters.put(FormatType.NONE, Object::toString); + + this.decimalFormatter = formatters.decimals(); + this.percentageFormatter = formatters.percentage(); + + } + + public String toJSONString() { + String data = parseData(); + String columnHeaders = parseColumnHeaders(); + return "{columns:" + columnHeaders + ",data:" + data + '}'; + } + + private String parseData() { + StringBuilder dataJSON = new StringBuilder("["); + + PlanAPI planAPI = PlanAPI.getInstance(); + long now = System.currentTimeMillis(); + players.sort(new PlayerContainerLastPlayedComparator()); + + int currentPlayerNumber = 0; + for (PlayerContainer player : players) { + if (currentPlayerNumber >= maxPlayers) { + break; + } + + UUID playerUUID = player.getValue(PlayerKeys.UUID).orElse(null); + if (playerUUID == null) { + continue; + } + + appendPlayerData(dataJSON, planAPI, now, player); + appendExtensionData(dataJSON, extensionData.get(playerUUID)); + + currentPlayerNumber++; + } + return dataJSON.append(']').toString(); + } + + private void appendPlayerData(StringBuilder dataJSON, PlanAPI planAPI, long now, PlayerContainer player) { + String name = player.getValue(PlayerKeys.NAME).orElse("Unknown"); + String url = planAPI.getPlayerInspectPageLink(name); + + SessionsMutator sessionsMutator = SessionsMutator.forContainer(player); + int loginTimes = sessionsMutator.count(); + long playtime = sessionsMutator.toPlaytime(); + long registered = player.getValue(PlayerKeys.REGISTERED).orElse(0L); + long lastSeen = sessionsMutator.toLastSeen(); + + ActivityIndex activityIndex = player.getActivityIndex(now, activeMsThreshold, activeLoginThreshold); + boolean isBanned = player.getValue(PlayerKeys.BANNED).orElse(false); + String activityString = activityIndex.getFormattedValue(decimalFormatter) + + (isBanned ? " (Banned)" : " (" + activityIndex.getGroup() + ")"); + + String geolocation = GeoInfoMutator.forContainer(player).mostRecent().map(GeoInfo::getGeolocation).orElse("-"); + + Html link = openPlayerPageInNewTab ? Html.LINK_EXTERNAL : Html.LINK; + + appendData(dataJSON, + '"' + link.parse(url, name) + '"', + "{display:\"" + activityString + "\",sort: " + activityIndex + '}', + "{display:\"" + numberFormatters.get(FormatType.TIME_MILLISECONDS).apply(playtime) + "\",sort: " + playtime + '}', + loginTimes, + "{display:\"" + numberFormatters.get(FormatType.DATE_YEAR).apply(registered) + "\",sort: " + registered + '}', + "{display:\"" + numberFormatters.get(FormatType.DATE_YEAR).apply(lastSeen) + "\",sort: " + lastSeen + '}', + geolocation + ); + } + + private void appendExtensionData(StringBuilder dataJSON, ExtensionTabData tabData) { + for (ExtensionDescriptive descriptive : extensionDescriptives) { + dataJSON.append(','); + String key = descriptive.getName(); + + // If it's a double, append a double + Optional doubleValue = tabData.getDouble(key); + if (doubleValue.isPresent()) { + dataJSON.append(doubleValue.get().getFormattedValue(decimalFormatter)); + continue; + } + + // If it's a percentage, append a percentage + Optional percentageValue = tabData.getPercentage(key); + if (percentageValue.isPresent()) { + dataJSON.append(percentageValue.get().getFormattedValue(percentageFormatter)); + continue; + } + + Optional numberValue = tabData.getNumber(key); + if (numberValue.isPresent()) { + ExtensionNumberData numberData = numberValue.get(); + FormatType formatType = numberData.getFormatType(); + if (formatType == FormatType.NONE) { + // If it's a number, append a number + dataJSON.append(numberData.getFormattedValue(numberFormatters.get(formatType))); + } else { + // If it's a formatted number, append a formatted number and sort by the number value + dataJSON.append("{display: \"").append(numberData.getFormattedValue(numberFormatters.get(formatType))) + .append("\",sort: ").append(numberData.getRawValue()).append('}'); + } + continue; + } + + // If it's a String append a String, otherwise the player has no value for this extension provider. + String stringValue = tabData.getString(key).map(ExtensionStringData::getFormattedValue).orElse("-"); + dataJSON.append('"').append(stringValue).append('"'); + } + } + + private String getValue(ExtensionTabData tabData, String key) { + tabData.getPercentage(key).map(data -> data.getFormattedValue(percentageFormatter)); + tabData.getNumber(key).map(data -> data.getFormattedValue(numberFormatters.get(data.getFormatType()))); + tabData.getString(key).map(ExtensionStringData::getFormattedValue); + + return "-"; + } + + private void appendData(StringBuilder dataJSON, Serializable... dataRows) { + int max = dataRows.length; + for (int i = 0; i < max; i++) { + dataJSON.append("{").append(dataRows[i]).append("}"); + if (i < max - 1) { + dataJSON.append(','); + } + } + } + + private String parseColumnHeaders() { + StringBuilder columnHeaders = new StringBuilder("["); + + appendDataHeaders(columnHeaders, + Icon.called("user") + " Name", + Icon.called("check") + " Activity Index", + Icon.called("clock").of(Family.REGULAR) + " Playtime", + Icon.called("calendar-plus").of(Family.REGULAR) + " Sessions", + Icon.called("user-plus") + " Registered", + Icon.called("calendar-check").of(Family.REGULAR) + " Last Seen", + Icon.called("globe") + " Geolocation" + ); + + appendExtensionHeaders(columnHeaders); + + return columnHeaders.append(']').toString(); + } + + private void appendDataHeaders(StringBuilder columnHeaders, Serializable... headers) { + int max = headers.length; + for (int i = 0; i < max; i++) { + columnHeaders.append("{title: \"").append(headers[i]).append("\"}"); + if (i < max - 1) { + columnHeaders.append(','); + } + } + } + + private void appendExtensionHeaders(StringBuilder columnHeaders) { + for (ExtensionDescriptive provider : extensionDescriptives) { + columnHeaders.append(','); + columnHeaders.append(Icon.fromExtensionIcon(provider.getIcon().setColor(Color.NONE)).toHtml()).append(' ').append(provider.getText()); + } + } +} \ No newline at end of file