Merge branch 'master' into standalone-mode

# Conflicts:
#	Plan/settings.gradle
This commit is contained in:
Aurora Lahtela 2022-09-13 16:00:29 +03:00
commit 3b09fd8da2
936 changed files with 64951 additions and 16637 deletions

View File

@ -12,9 +12,13 @@ labels: 'Bug'
### Exceptions & Other Logs
<!-- If reporting an Exception, please provide the error log from /plugins/Plan/logs, it has context in it -->
```
Place log contents here
<!-- Paste log contents inside the backticks below -->
```
```
### Plugin versions
<!-- Versions of Plan and any other related plugins -->
### Additional information
<!-- Any additional information, plugin versions, context, what was attempted, etc -->
<!-- Any additional information, context, what was attempted, etc -->

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,7 @@
contact_links:
- name: Question
url: https://github.com/plan-player-analytics/Plan/discussions/new
about: Ask a question about the plugin on the Discussions forum
- name: Installation help
url: https://discordapp.com/invite/yXKmjzT
about: Get help for installations on Discord

View File

@ -1,10 +0,0 @@
---
name: Suggestion
about: Feature request or an addition
---
### Is your feature request related to a problem? Please describe.
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
### I would like to be able to..
<!-- A clear and concise description of what you want to happen. -->

View File

@ -0,0 +1,15 @@
name: Suggestion
description: Feature request or an addition
body:
- type: textarea
attributes:
label: I would like to be able to..
description: A clear and concise description of what you want to happen
validations:
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: This section helps figure out why this feature is needed - What would this feature be used for? How would you use this feature?
validations:
required: true

View File

@ -1,16 +0,0 @@
---
name: Plugin Suggestion
about: Suggest data from another plugin for Plan
labels: 'New Data, DataExtensions, Help Wanted'
---
### Data to display
<!-- List any data you would like to see from the plugin -->
### Plugin information
<!-- Required -->
**API or Source Code:** [Link](URL HERE)
<!-- Required -->
**Project page or Downloads:** [Link](URL HERE)

View File

@ -0,0 +1,20 @@
name: Plugin Suggestion
description: Suggest data from another plugin for Plan
labels: [New Data, DataExtensions, Help Wanted]
body:
- type: textarea
attributes:
label: Data to display
description: List any data you would like to see from the plugin
validations:
required: true
- type: input
attributes:
label: Link to API or Source Code
validations:
required: true
- type: input
attributes:
label: Link to Project page or Downloads
validations:
required: true

View File

@ -1,12 +0,0 @@
---
name: Question
about: Ask a Question about the plugin, see 'Possible Pitfalls' under Projects before asking your question.
labels: 'Question'
---
<!-- Remove this comment and write your question here -->
### Additional context
<!-- Any additional information to understand the question better -->

View File

@ -3,9 +3,13 @@ updates:
- package-ecosystem: gradle
directory: "/Plan"
schedule:
interval: daily
interval: weekly
day: friday
open-pull-requests-limit: 99
ignore:
- dependency-name: net.fabricmc:fabric-loader
versions:
- ">=0, < 2"
- dependency-name: com.djrapitops:Extension-ProtocolSupport
versions:
- ">= 4.a, < 5"
@ -15,3 +19,9 @@ updates:
- dependency-name: com.h2database:h2
versions:
- "> 1.4.199"
- package-ecosystem: npm
directory: "/Plan/react/dashboard"
schedule:
interval: weekly
day: friday
open-pull-requests-limit: 99

96
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,96 @@
name: CI
on: [push, pull_request]
jobs:
test:
name: Test, Build & Upload
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:latest
ports:
- 3306
env:
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: password
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '17'
- name: Setup Selenium Webdriver
uses: nanasess/setup-chromedriver@v1
- name: Setup Selenium Webdriver settings
run: |
export DISPLAY=:99
chromedriver --url-base=/wd/hub &
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
- name: Verify MariaDB connection
env:
PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do
sleep 1
done
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build jars
run: |
cd Plan
./gradlew shadowJar
- name: Get versions
run: |
cd Plan
./gradlew snapshotVersion jarNameVersion
git_hash=$(git rev-parse --short "$GITHUB_SHA")
echo "git_hash=$git_hash" >> $GITHUB_ENV
echo "snapshotVersion=$(cat build/versions/snapshot.txt)" >> $GITHUB_ENV
echo "versionString=$(cat build/versions/jar.txt)" >> $GITHUB_ENV
echo "artifactPath=$(pwd)/builds" >> $GITHUB_ENV
- name: Upload Plan.jar
uses: actions/upload-artifact@v3
with:
name: Plan-${{ env.versionString }}-${{ env.git_hash }}.jar
path: ${{ env.artifactPath }}/Plan-${{ env.snapshotVersion }}.jar
- name: Upload PlanFabric.jar
uses: actions/upload-artifact@v3
with:
name: PlanFabric-${{ env.versionString }}-${{ env.git_hash }}.jar
path: ${{ env.artifactPath }}/PlanFabric-${{ env.snapshotVersion }}.jar
- name: Test
env:
MYSQL_DB: test
MYSQL_USER: user
MYSQL_PASS: password
MYSQL_PORT: ${{ job.services.mariadb.ports[3306] }}
CHROMEDRIVER: /usr/local/bin/chromedriver
run: |
cd Plan
./gradlew build
- name: SonarCloud
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
if: "${{ env.SONAR_TOKEN != '' }}"
run: |
cd Plan
./gradlew sonar -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=player-analytics-plan

View File

@ -1,44 +0,0 @@
name: Java PR CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:latest
ports:
- 3306
env:
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: password
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Verify MariaDB connection
env:
PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do
sleep 1
done
- name: Build with Gradle
env:
MYSQL_DB: test
MYSQL_USER: user
MYSQL_PASS: password
MYSQL_PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
cd Plan
./gradlew build

View File

@ -1,51 +0,0 @@
name: Java CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:latest
ports:
- 3306
env:
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: password
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Verify MariaDB connection
env:
PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do
sleep 1
done
- name: Build with Gradle
env:
MYSQL_DB: test
MYSQL_USER: user
MYSQL_PASS: password
MYSQL_PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
cd Plan
./gradlew build
- name: SonarCloud
run: |
cd Plan
./gradlew sonar -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=player-analytics-plan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -1,30 +0,0 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Update html-branch
on:
release:
types: [ published ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1
- name: Get git TAG
id: tagName
uses: olegtarasov/get-tag@v2.1
- name: Copy assets 🔧
run: |
mkdir -p workingdir/Plan/src/main/resources/assets/plan
cp -r Plan/common/src/main/resources/assets/plan workingdir/Plan/src/main/resources/assets/plan
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.0.0
with:
branch: html # The branch the action should deploy to.
folder: workingdir # The folder the action should deploy.
commit-message: ${{ steps.tagName.outputs.tag }}

28
.github/workflows/issues.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Issue Automation
on:
issues:
types:
- opened
# map fields with customized labels
env:
inbox: INBOX
refinement: Needs Refinement
refined: Refined & Actionable
assigned: Assigned to Milestone
done: Done
jobs:
issue_opened:
name: New Issue Opened
runs-on: ubuntu-latest
if: github.event_name == 'issues' && github.event.action == 'opened'
steps:
- name: Add issue to ${{ env.inbox }}
uses: leonsteinhaeuser/project-beta-automations@v1.2.1
with:
gh_token: ${{ secrets.GITHUB_TOKEN }}
organization: plan-player-analytics
project_id: 3
resource_node_id: ${{ github.event.issue.node_id }}
status_value: ${{ env.inbox }} # Target status

View File

@ -16,6 +16,11 @@ jobs:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1
- name: Set up JDK 🍵
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '17'
- name: Build Javadocs 🔧
run: |
cd Plan
@ -31,8 +36,9 @@ jobs:
cp scripts/index.html javadocs/index.html
cd javadocs
touch .nojekyll
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.0.0
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: gh-pages # The branch the action should deploy to.
folder: javadocs # The folder the action should deploy.

137
.github/workflows/on-release.yml vendored Normal file
View File

@ -0,0 +1,137 @@
name: Release
on:
release:
types: [ published ]
jobs:
update_versions_txt:
name: Update versions.txt
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1
- name: Get Download URL
run: |
curl 'https://api.github.com/repos/plan-player-analytics/plan/releases/${{ github.event.release.id }}/assets' | jq -r '.[] | {name: .name, url: .browser_download_url} | select(.url | strings | test("Fabric") | not)' > asset.txt
jq -r '.url' asset.txt > url.txt
echo "RELEASE_DOWNLOAD_URL=$(cat url.txt)" >> $GITHUB_ENV
- name: Write REL release line
if: ${{ github.event.release.prerelease == false }}
run: |
echo "REL|${{ github.event.release.tag_name }}|${{ env.RELEASE_DOWNLOAD_URL }}|https://github.com/plan-player-analytics/Plan/releases" > release_line.txt
- name: Write DEV release line
if: ${{ github.event.release.prerelease == true }}
run: |
echo "DEV|${{ github.event.release.tag_name }}|${{ env.RELEASE_DOWNLOAD_URL }}|${{ github.event.release.html_url }}" > release_line.txt
- name: Append to versions.txt
run: |
cat versions.txt > temp.txt
cat release_line.txt temp.txt > versions.txt
- name: Commit and push changes
uses: EndBug/add-and-commit@v9
with:
committer_name: GitHub Actions
committer_email: 41898282+github-actions[bot]@users.noreply.github.com
message: Update versions.txt ${{ github.event.release.name }}
add: versions.txt
update_html:
name: Update html-branch
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1
- name: Get git TAG
id: tagName
uses: olegtarasov/get-tag@v2.1
- name: Copy assets 🔧
run: |
mkdir -p workingdir/Plan/src/main/resources/assets/plan
mkdir -p workingdir/react/dashboard
cp -r Plan/common/src/main/resources/assets/plan workingdir/Plan/src/main/resources/assets/plan
cp -r Plan/react/dashboard workingdir/react/dashboard
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.0.0
with:
branch: html # The branch the action should deploy to.
folder: workingdir # The folder the action should deploy.
commit-message: ${{ steps.tagName.outputs.tag }}
upload_release_ore:
name: Ore Upload
runs-on: ubuntu-latest
steps:
- name: Download release artifact for upload
run: |
curl 'https://api.github.com/repos/plan-player-analytics/plan/releases/${{ github.event.release.id }}/assets' | jq -r '.[] | {name: .name, url: .browser_download_url} | select(.url | strings | test("Fabric") | not)' > asset.txt
jq -r '.url' asset.txt > url.txt
jq -r '.name' asset.txt > name.txt
wget -i url.txt
echo "JAR_FILENAME=$(cat name.txt)" >> $GITHUB_ENV
- name: Upload artifact for ore upload
uses: actions/upload-artifact@v3
with:
name: ${{ github.event.release.name }}
path: ${{ env.JAR_FILENAME }}
- name: Upload release to Ore 🚀
if: ${{ github.event.release.prerelease == false }}
uses: dualspiral/ore-upload-action@v1
with:
plugin: ${{ github.event.release.name }}
description: ${{ github.event.release.body }}
apiKey: ${{ secrets.ORE_API_TOKEN }}
channel: Release
pluginId: plan
createForumPost: true
- name: Upload DEV release to Ore 🚀
if: ${{ github.event.release.prerelease == true }}
uses: dualspiral/ore-upload-action@v1
with:
plugin: ${{ github.event.release.name }}
description: ${{ github.event.release.body }}
apiKey: ${{ secrets.ORE_API_TOKEN }}
channel: DEV
pluginId: plan
createForumPost: false
upload_release_curseforge:
name: CurseForge Upload
runs-on: ubuntu-latest
steps:
- name: Download release artifact for upload
run: |
curl 'https://api.github.com/repos/plan-player-analytics/plan/releases/${{ github.event.release.id }}/assets' | jq -r '.[] | {name: .name, url: .browser_download_url} | select(.url | strings | test("Fabric"))' > asset.txt
jq -r '.url' asset.txt > url.txt
jq -r '.name' asset.txt > name.txt
wget -i url.txt
echo "JAR_FILENAME=$(cat name.txt)" >> $GITHUB_ENV
- name: Upload release to CurseForge 🚀
if: ${{ github.event.release.prerelease == false }}
uses: itsmeow/curseforge-upload@master
with:
token: ${{ secrets.CF_API_TOKEN }}
project_id: 508727
game_endpoint: minecraft
file_path: ${{ env.JAR_FILENAME }}
changelog: ${{ github.event.release.body }}
changelog_type: markdown
display_name: ${{ github.event.release.name }}
game_versions: "2:Java 17,73407:1.19,Fabric"
release_type: release
relations: fabric-api:requiredDependency,luckperms:optionalDependency
- name: Upload prerelease to CurseForge 🚀
if: ${{ github.event.release.prerelease == true }}
uses: itsmeow/curseforge-upload@master
with:
token: ${{ secrets.CF_API_TOKEN }}
project_id: 508727
game_endpoint: minecraft
file_path: ${{ env.JAR_FILENAME }}
changelog: ${{ github.event.release.body }}
changelog_type: markdown
display_name: ${{ github.event.release.name }}
game_versions: "2:Java 17,73407:1.19,Fabric"
release_type: beta
relations: fabric-api:requiredDependency,luckperms:optionalDependency

34
.gitignore vendored
View File

@ -3,14 +3,18 @@ Plan.iml
PlanPluginBridge.iml
.sonar/
builds/
# Nukkit creates server.log during tests for some reason.
# Nukkit & Fabric create log files during tests for some reason.
server.log
/Plan/fabric/logs/
*.db
**/.gradle
out/
# Fabric
remappedSrc/
# Created by https://www.gitignore.io/api/maven,eclipse,intellij,netbeans,osx,windows,notepadpp,windows,java
### Maven ###
@ -164,33 +168,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
### NotepadPP ###
# Notepad++ backups #
*.bak
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Java ###
*.class
@ -203,3 +180,4 @@ $RECYCLE.BIN/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
/.vs

View File

@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team on discord https://discordapp.com/invite/yXKmjzT (you can send a DM to Rsl1122). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team on discord https://discordapp.com/invite/yXKmjzT (you can send a DM to AuroraLS3). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@ -4,7 +4,11 @@ dependencies {
compileOnly "com.google.code.gson:gson:$gsonVersion"
}
ext.apiVersion = '5.4-R0.1'
compileJava {
options.release = 8
}
ext.apiVersion = '5.5-R0.1'
publishing {
repositories {

View File

@ -39,28 +39,22 @@ import java.util.function.Predicate;
public final class CompositeResolver implements Resolver {
private final List<String> prefixes;
private final List<Function<Request, Optional<Response>>> resolvers;
private final List<Predicate<Request>> canAccess;
private final List<Resolver> resolvers;
CompositeResolver() {
this.prefixes = new ArrayList<>();
this.resolvers = new ArrayList<>();
this.canAccess = new ArrayList<>();
}
public static CompositeResolver.Builder builder() {
return new Builder();
}
private Optional<Function<Request, Optional<Response>>> getResolver(URIPath target) {
private Optional<Resolver> getResolver(URIPath target) {
return target.getPart(0).flatMap(this::findResolver);
}
private Optional<Predicate<Request>> getAccessCheck(URIPath target) {
return target.getPart(0).flatMap(this::findAccessCheck);
}
private Optional<Function<Request, Optional<Response>>> findResolver(String prefix) {
private Optional<Resolver> findResolver(String prefix) {
for (int i = 0; i < prefixes.size(); i++) {
if (prefixes.get(i).equals(prefix)) {
return Optional.of(resolvers.get(i));
@ -69,21 +63,11 @@ public final class CompositeResolver implements Resolver {
return Optional.empty();
}
private Optional<Predicate<Request>> findAccessCheck(String prefix) {
for (int i = 0; i < prefixes.size(); i++) {
if (prefixes.get(i).equals(prefix)) {
return Optional.of(canAccess.get(i));
}
}
return Optional.empty();
}
void add(String prefix, Resolver resolver) {
if (prefix == null) throw new IllegalArgumentException("Prefix can not be null");
if (resolver == null) throw new IllegalArgumentException("Resolver can not be null");
prefixes.add(prefix);
resolvers.add(resolver::resolve);
canAccess.add(resolver::canAccess);
resolvers.add(resolver);
}
void add(String prefix, Function<Request, Response> resolver, Predicate<Request> accessCheck) {
@ -93,22 +77,27 @@ public final class CompositeResolver implements Resolver {
}
if (accessCheck == null) throw new IllegalArgumentException("Predicate<Request> accessCheck can not be null");
prefixes.add(prefix);
resolvers.add(request -> Optional.ofNullable(resolver.apply(request)));
canAccess.add(accessCheck);
resolvers.add(new FunctionalResolverWrapper(request -> Optional.ofNullable(resolver.apply(request)), accessCheck));
}
@Override
public boolean canAccess(Request request) {
Request forThis = request.omitFirstInPath();
return getAccessCheck(forThis.getPath())
.map(resolver -> resolver.test(forThis))
return getResolver(forThis.getPath())
.map(resolver -> resolver.canAccess(forThis))
.orElse(true);
}
@Override
public Optional<Response> resolve(Request request) {
Request forThis = request.omitFirstInPath();
return getResolver(forThis.getPath()).flatMap(resolver -> resolver.apply(forThis));
return getResolver(forThis.getPath()).flatMap(resolver -> resolver.resolve(forThis));
}
@Override
public boolean requiresAuth(Request request) {
Request forThis = request.omitFirstInPath();
return getResolver(forThis.getPath()).map(resolver -> resolver.requiresAuth(forThis)).orElse(true);
}
public static class Builder {

View File

@ -0,0 +1,44 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
public class FunctionalResolverWrapper implements Resolver {
private final Function<Request, Optional<Response>> resolver;
private final Predicate<Request> accessCheck;
public FunctionalResolverWrapper(Function<Request, Optional<Response>> resolver, Predicate<Request> accessCheck) {
this.resolver = resolver;
this.accessCheck = accessCheck;
}
@Override
public boolean canAccess(Request request) {
return accessCheck.test(request);
}
@Override
public Optional<Response> resolve(Request request) {
return resolver.apply(request);
}
}

View File

@ -66,6 +66,11 @@ public class ResponseBuilder {
return this;
}
protected ResponseBuilder removeHeader(String header) {
response.headers.remove(header);
return this;
}
/**
* Utility method for building redirects.
*
@ -103,7 +108,8 @@ public class ResponseBuilder {
}
}
return setContent(content.getBytes(charset));
return setContent(content.getBytes(charset))
.removeHeader("Accept-Ranges"); // Can compress
}
/**

View File

@ -34,25 +34,30 @@ public final class Request {
private final URIQuery query;
private final WebUser user;
private final Map<String, String> headers;
private final byte[] requestBody;
/**
* Constructor.
*
* @param method HTTP method, GET, PUT, POST, etc
* @param path Requested path /example/target
* @param query Request parameters ?param=value etc
* @param user Web user doing the request (if authenticated)
* @param headers Request headers https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param method HTTP method, GET, PUT, POST, etc
* @param path Requested path /example/target
* @param query Request parameters ?param=value etc
* @param user Web user doing the request (if authenticated)
* @param headers Request headers https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param requestBody Raw body as bytes, if present
*/
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers) {
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers, byte[] requestBody) {
this.method = method;
this.path = path;
this.query = query;
this.user = user;
this.headers = headers;
this.requestBody = requestBody;
}
// Special constructor that figures out URIPath and URIQuery from "/path/and?query=params"
/**
* Special constructor that figures out URIPath and URIQuery from "/path/and?query=params" and has no request body.
*/
public Request(String method, String target, WebUser user, Map<String, String> headers) {
this.method = method;
if (target.contains("?")) {
@ -65,6 +70,7 @@ public final class Request {
}
this.user = user;
this.headers = headers;
this.requestBody = new byte[0];
}
/**
@ -94,6 +100,15 @@ public final class Request {
return query;
}
/**
* Get the raw body, if present.
*
* @return byte[].
*/
public byte[] getRequestBody() {
return requestBody;
}
/**
* Get the user making the request.
*
@ -114,7 +129,7 @@ public final class Request {
}
public Request omitFirstInPath() {
return new Request(method, path.omitFirst(), query, user, headers);
return new Request(method, path.omitFirst(), query, user, headers, requestBody);
}
@Override
@ -125,6 +140,7 @@ public final class Request {
", query=" + query +
", user=" + user +
", headers=" + headers +
", body=" + requestBody.length +
'}';
}
}
}

View File

@ -56,19 +56,35 @@ public final class URIQuery {
}
String[] keyAndValue = StringUtils.split(kv, "=", 2);
if (keyAndValue.length >= 2) {
try {
parameters.put(
URLDecoder.decode(keyAndValue[0], StandardCharsets.UTF_8.name()),
URLDecoder.decode(keyAndValue[1], StandardCharsets.UTF_8.name())
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
}
parseAndPutKeyValuePair(parameters, keyAndValue);
} else if (keyAndValue.length == 1) {
parseAndPutKeyEmptyValue(parameters, keyAndValue[0]);
}
}
return parameters;
}
private void parseAndPutKeyValuePair(Map<String, String> parameters, String[] keyAndValue) {
try {
parameters.put(
URLDecoder.decode(keyAndValue[0], StandardCharsets.UTF_8.name()),
URLDecoder.decode(keyAndValue[1], StandardCharsets.UTF_8.name())
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
}
}
private void parseAndPutKeyEmptyValue(Map<String, String> parameters, String s) {
try {
parameters.put(
URLDecoder.decode(s, StandardCharsets.UTF_8.name()), ""
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
}
}
/**
* Obtain an URI parameter by key.
*
@ -80,6 +96,8 @@ public final class URIQuery {
}
public String asString() {
if (byKey.isEmpty()) return "";
StringBuilder builder = new StringBuilder("?");
int i = 0;
int max = byKey.size();
@ -97,8 +115,10 @@ public final class URIQuery {
@Override
public String toString() {
Map<String, String> copyOfByKey = new HashMap<>(this.byKey);
copyOfByKey.remove("password");
return "URIQuery{" +
"byKey=" + byKey +
"byKey=" + copyOfByKey +
'}';
}
}

View File

@ -16,29 +16,37 @@
*/
package com.djrapitops.plan.delivery.web.resolver.request;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.*;
public final class WebUser {
private final String playerName;
private final String username;
private final UUID playerUUID;
private final Set<String> permissions;
public WebUser(String playerName) {
this.playerName = playerName;
this.playerUUID = null;
this.username = playerName;
this.permissions = new HashSet<>();
}
public WebUser(String playerName, String username, Collection<String> permissions) {
public WebUser(String playerName, UUID playerUUID, String username, Collection<String> permissions) {
this.playerName = playerName;
this.playerUUID = playerUUID;
this.username = username;
this.permissions = new HashSet<>(permissions);
}
/**
* @deprecated WebUser now stores the UUID of the linked user
*/
@Deprecated
public WebUser(String playerName, String username, Collection<String> permissions) {
this(playerName, null, username, permissions);
}
/**
* @deprecated WebUser now has username and player name.
*/
@ -60,6 +68,14 @@ public final class WebUser {
return username;
}
public Optional<UUID> getUUID() {
return Optional.ofNullable(playerUUID);
}
public Set<String> getPermissions() {
return permissions;
}
@Override
public String toString() {
return "WebUser{" +

View File

@ -45,7 +45,6 @@ public final class ExtensionExtractor {
private PluginInfo pluginInfo;
private List<TabInfo> tabInformation;
private List<InvalidateMethod> invalidMethods;
private MethodAnnotations methodAnnotations;
private Map<ExtensionMethod.ParameterType, ExtensionMethods> methods;
private Collection<Method> conditionalMethods;
private Collection<Tab> tabAnnotations;
@ -102,12 +101,9 @@ public final class ExtensionExtractor {
* @deprecated No longer used anywhere, no-op.
*/
@Deprecated
public void extractAnnotationInformation() {
// no-op
}
public void extractAnnotationInformation() {/* no-op */}
private void extractMethods() {
methodAnnotations = new MethodAnnotations();
methods = new EnumMap<>(ExtensionMethod.ParameterType.class);
methods.put(ExtensionMethod.ParameterType.SERVER_NONE, new ExtensionMethods());
methods.put(ExtensionMethod.ParameterType.PLAYER_STRING, new ExtensionMethods());
@ -132,55 +128,41 @@ public final class ExtensionExtractor {
method.getAnnotation(BooleanProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addBooleanMethod(method);
methodAnnotations.put(method.getMethod(), BooleanProvider.class, annotation);
});
method.getAnnotation(NumberProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addNumberMethod(method);
methodAnnotations.put(method.getMethod(), NumberProvider.class, annotation);
});
method.getAnnotation(DoubleProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addDoubleMethod(method);
methodAnnotations.put(method.getMethod(), DoubleProvider.class, annotation);
});
method.getAnnotation(PercentageProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addPercentageMethod(method);
methodAnnotations.put(method.getMethod(), PercentageProvider.class, annotation);
});
method.getAnnotation(StringProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addStringMethod(method);
methodAnnotations.put(method.getMethod(), StringProvider.class, annotation);
});
method.getAnnotation(TableProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addTableMethod(method);
methodAnnotations.put(method.getMethod(), TableProvider.class, annotation);
});
method.getAnnotation(GroupProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addGroupMethod(method);
methodAnnotations.put(method.getMethod(), GroupProvider.class, annotation);
});
method.getAnnotation(DataBuilderProvider.class).ifPresent(annotation -> {
validateMethod(method, annotation);
methods.get(method.getParameterType()).addDataBuilderMethod(method);
methodAnnotations.put(method.getMethod(), DataBuilderProvider.class, annotation);
});
method.getAnnotation(Conditional.class).ifPresent(annotation -> {
conditionalMethods.add(method.getMethod());
methodAnnotations.put(method.getMethod(), Conditional.class, annotation);
});
method.getAnnotation(Tab.class).ifPresent(annotation -> {
tabAnnotations.add(annotation);
methodAnnotations.put(method.getMethod(), Tab.class, annotation);
});
method.getAnnotation(Conditional.class).ifPresent(annotation -> conditionalMethods.add(method.getMethod()));
method.getAnnotation(Tab.class).ifPresent(tabAnnotations::add);
}
if (methodAnnotations.isEmpty()) {
if (methods.values().stream().allMatch(ExtensionMethods::isEmpty)) {
throw new IllegalArgumentException(extensionName + " class had no methods annotated with a Provider annotation");
}
@ -440,10 +422,12 @@ public final class ExtensionExtractor {
return tabInformation;
}
/**
* @deprecated During refactoring MethodAnnotations was removed. Using {@link ExtensionExtractor#getMethods()} instead.
*/
@Deprecated
public MethodAnnotations getMethodAnnotations() {
if (methodAnnotations == null) extractMethods();
return methodAnnotations;
return new MethodAnnotations();
}
public Map<ExtensionMethod.ParameterType, ExtensionMethods> getMethods() {
@ -455,4 +439,10 @@ public final class ExtensionExtractor {
if (invalidMethods == null) extractInvalidMethods();
return invalidMethods;
}
// Visible for testing
Collection<Method> getConditionalMethods() {
if (conditionalMethods == null) extractMethods();
return conditionalMethods;
}
}

View File

@ -142,4 +142,15 @@ public class ExtensionMethods {
", dataBuilderProviders=" + dataBuilderProviders +
'}';
}
public boolean isEmpty() {
return booleanProviders.isEmpty()
&& numberProviders.isEmpty()
&& doubleProviders.isEmpty()
&& percentageProviders.isEmpty()
&& stringProviders.isEmpty()
&& tableProviders.isEmpty()
&& groupProviders.isEmpty()
&& dataBuilderProviders.isEmpty();
}
}

View File

@ -24,8 +24,17 @@ import java.util.Optional;
* @author AuroraLS3
*/
public enum Family {
/**
* 'fas' (solid) Font awesome family.
*/
SOLID,
/**
* 'far' (regular) Font awesome family.
*/
REGULAR,
/**
* 'fab' (brand) Font awesome family.
*/
BRAND;
public static Optional<Family> getByName(String name) {

View File

@ -25,6 +25,9 @@ package com.djrapitops.plan.extension.icon;
*/
public class Icon {
// Implementation detail, set during icon storage to optimize relation inserts.
int id;
private Family type;
private String name;
private Color color;

View File

@ -48,6 +48,7 @@ public final class Table {
private final String[] columns;
private final Icon[] icons;
private final TableColumnFormat[] tableColumnFormats;
private final List<Object[]> rows;
@ -56,6 +57,10 @@ public final class Table {
columns = new String[5];
icons = new Icon[5];
tableColumnFormats = new TableColumnFormat[]{
TableColumnFormat.NONE, TableColumnFormat.NONE, TableColumnFormat.NONE,
TableColumnFormat.NONE, TableColumnFormat.NONE
};
rows = new ArrayList<>();
}
@ -91,6 +96,10 @@ public final class Table {
return rows;
}
public TableColumnFormat[] getTableColumnFormats() {
return tableColumnFormats;
}
/**
* Factory for creating new {@link Table} objects.
*/
@ -171,6 +180,61 @@ public final class Table {
return column(4, columnName, icon);
}
private Factory columnFormat(int index, TableColumnFormat tableColumnFormat) {
building.tableColumnFormats[index] = tableColumnFormat;
return this;
}
/**
* Apply formatting to column one.
*
* @param tableColumnFormat Format to apply
* @return Factory.
*/
public Factory columnOneFormat(TableColumnFormat tableColumnFormat) {
return columnFormat(0, tableColumnFormat);
}
/**
* Apply formatting to column two.
*
* @param tableColumnFormat Format to apply
* @return Factory.
*/
public Factory columnTwoFormat(TableColumnFormat tableColumnFormat) {
return columnFormat(1, tableColumnFormat);
}
/**
* Apply formatting to column three.
*
* @param tableColumnFormat Format to apply
* @return Factory.
*/
public Factory columnThreeFormat(TableColumnFormat tableColumnFormat) {
return columnFormat(2, tableColumnFormat);
}
/**
* Apply formatting to column four.
*
* @param tableColumnFormat Format to apply
* @return Factory.
*/
public Factory columnFourFormat(TableColumnFormat tableColumnFormat) {
return columnFormat(3, tableColumnFormat);
}
/**
* Apply formatting to column five.
*
* @param tableColumnFormat Format to apply
* @return Factory.
*/
public Factory columnFiveFormat(TableColumnFormat tableColumnFormat) {
return columnFormat(4, tableColumnFormat);
}
/**
* Add a row of values to the table.
*

View File

@ -0,0 +1,44 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.extension.table;
public enum TableColumnFormat {
/**
* String variables to be formatted as links to player page.
*/
PLAYER_NAME,
/**
* String variables to be formatted as colored (using § character).
*/
CHAT_COLORED,
/**
* Number variables to be formatted as time amount (eg. 1h 30m 25s).
*/
TIME_MILLISECONDS,
/**
* Number epoch millisecond to be formatted as date without second indicator.
*/
DATE_YEAR,
/**
* Number epoch millisecond to be formatted as date with second indicator.
*/
DATE_SECOND,
/**
* Default formatting, no extra formatting applied.
*/
NONE
}

View File

@ -265,7 +265,7 @@ class ExtensionExtractorTest {
}
ExtensionExtractor underTest = new ExtensionExtractor(new Extension());
assertEquals("Extension.method did not have any associated Provider for Conditional.", assertThrows(IllegalArgumentException.class, underTest::validateAnnotations).getMessage());
assertEquals("Extension class had no methods annotated with a Provider annotation", assertThrows(IllegalArgumentException.class, underTest::validateAnnotations).getMessage());
}
@Test
@ -489,8 +489,13 @@ class ExtensionExtractorTest {
String expected = Stream.of(extension.getClass().getMethod("method").getAnnotation(Conditional.class))
.map(Conditional::value).findFirst().orElseThrow(AssertionError::new);
String result = underTest.getMethodAnnotations().getAnnotations(Conditional.class).stream()
.map(Conditional::value).findFirst().orElseThrow(AssertionError::new);
String result = underTest.getConditionalMethods().stream()
.map(method -> new ExtensionMethod(extension, method))
.map(extensionMethod -> extensionMethod.getAnnotation(Conditional.class))
.filter(Optional::isPresent)
.map(Optional::get)
.map(Conditional::value)
.findFirst().orElseThrow(AssertionError::new);
assertEquals(expected, result);
}

View File

@ -1,19 +1,23 @@
import java.nio.file.Files
// Aggregate Javadocs
buildscript {
repositories { mavenCentral() }
repositories {
mavenCentral()
}
dependencies {
classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.+'
}
}
plugins {
id "com.github.johnrengelman.shadow" version "7.0.0"
id "com.github.johnrengelman.shadow" version "7.1.2" apply false
id "java"
id 'java-library'
id "jacoco"
id "checkstyle"
id "org.sonarqube" version "3.3"
// id "net.ltgt.apt" version "0.21"
// id "net.ltgt.apt-idea" version "0.21"
id "org.sonarqube" version "3.4.0.2513"
id 'fabric-loom' version '0.12.+' apply false
}
apply plugin: 'nebula-aggregate-javadocs'
@ -30,13 +34,12 @@ def determineBuildVersion = {
def buildVersion = determineBuildVersion()
allprojects {
wrapper.gradleVersion = "7.0.2"
group "com.djrapitops"
version "5.4-SNAPSHOT"
version "5.5-SNAPSHOT"
ext.majorVersion = '5'
ext.minorVersion = '4'
ext.minorVersion = '5'
ext.buildVersion = buildVersion
ext.fullVersion = project.ext.majorVersion + '.' + project.ext.minorVersion + ' build ' + project.ext.buildVersion
@ -52,70 +55,81 @@ subprojects {
// Build plugins
apply plugin: "com.github.johnrengelman.shadow"
apply plugin: "java"
apply plugin: "java-library"
apply plugin: "maven-publish"
// Report plugins
apply plugin: "checkstyle"
apply plugin: "jacoco"
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
compileJava {
options.release = 11
}
ext {
daggerVersion = "2.37"
daggerCompilerVersion = "2.37"
daggerVersion = "2.43.2"
palVersion = "5.0.0"
palVersion = "5.1.0"
bukkitVersion = "1.13.2-R0.1-SNAPSHOT"
spigotVersion = "1.13.2-R0.1-SNAPSHOT"
paperVersion = "1.13.2-R0.1-SNAPSHOT"
spongeVersion = "7.3.0"
spongeVersion = "8.1.0"
nukkitVersion = "1.0-SNAPSHOT"
bungeeVersion = "1.16-R0.4"
velocityVersion = "3.0.0-SNAPSHOT"
redisBungeeVersion = "0.6.3"
redisBungeeVersion = "0.3.8-SNAPSHOT"
commonsTextVersion = "1.9"
commonsCompressVersion = "1.20"
commonsCompressVersion = "1.21"
commonsCodecVersion = "1.15"
caffeineVersion = "3.1.1"
mysqlVersion = "8.0.30"
jettyVersion = "11.0.11"
caffeineVersion = "2.9.2"
mysqlVersion = "8.0.25"
sqliteVersion = "3.36.0.1"
hikariVersion = "4.0.3"
slf4jVersion = "1.7.31"
geoIpVersion = "2.15.0"
gsonVersion = "2.8.7"
bstatsVersion = "2.2.1"
placeholderapiVersion = "2.10.9"
mysqlVersion = "8.0.26"
sqliteVersion = "3.39.2.1"
hikariVersion = "5.0.1"
slf4jVersion = "2.0.0"
geoIpVersion = "3.0.1"
gsonVersion = "2.9.1"
dependencyDownloadVersion = "1.2.1"
bstatsVersion = "3.0.0"
placeholderapiVersion = "2.11.2"
nkPlaceholderapiVersion = "1.4-SNAPSHOT"
junitVersion = "5.9.0"
mockitoVersion = "4.8.0"
testContainersVersion = "1.17.3"
swaggerVersion = "2.2.2"
}
repositories {
mavenCentral()
google()
maven { url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" } // Spigot
maven { url = "https://papermc.io/repo/repository/maven-public/" } // Paper
maven { url = "https://repo.spongepowered.org/repository/maven-public/" } // Sponge
maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } // BungeeCord
maven { url = "https://repo.md-5.net/content/repositories/snapshots/" } // RedisBungee
maven { url = "https://repo.velocitypowered.com/snapshots/" } // Velocity
maven { url = "https://repo.opencollab.dev/maven-snapshots/" } // Nukkit snapshot
maven { url = "https://repo.opencollab.dev/maven-releases/" } // Nukkit release
maven { url = "https://repo.codemc.org/repository/maven-public" } // bStats
maven { url = "https://repo.playeranalytics.net/releases" } // Plan
}
dependencies {
// Dependency Injection used across the project
implementation "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerCompilerVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$daggerCompilerVersion"
shadow "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
testImplementation "com.google.dagger:dagger:$daggerVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
// Test Tooling Dependencies
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' // JUnit 5
testImplementation "org.mockito:mockito-core:3.11.2" // Mockito Core
testImplementation "org.mockito:mockito-junit-jupiter:3.11.2" // Mockito JUnit 5 Extension
testImplementation "org.seleniumhq.selenium:selenium-java:3.141.59" // Selenium (Browser tests)
testImplementation "com.jayway.awaitility:awaitility:1.7.0" // Awaitility (Concurrent wait conditions)
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" // JUnit 5
testImplementation "org.mockito:mockito-core:$mockitoVersion" // Mockito Core
testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion" // Mockito JUnit 5 Extension
testImplementation "com.jayway.awaitility:awaitility:1.7.0"
// Awaitility (Concurrent wait conditions)
// Testing dependencies required by Plan
testImplementation "org.xerial:sqlite-jdbc:$sqliteVersion" // SQLite
@ -123,7 +137,11 @@ subprojects {
}
configurations {
testArtifacts.extendsFrom testRuntimeOnly
// Include shadowed dependencies in compile classpath of dependent modules
api.extendsFrom shadow
testArtifacts.extendsFrom testRuntimeOnly // Test classes available to other modules
testImplementation.extendsFrom shadow // Include shadowed dependencies in test classpath
}
// Test classes available to other modules
task testJar(type: Jar) {
@ -143,13 +161,13 @@ subprojects {
}
checkstyle {
toolVersion "8.33"
toolVersion "8.44"
getConfigDirectory().set file("$rootProject.projectDir/config/checkstyle")
}
jacocoTestReport {
reports {
xml.enabled true
xml.required = true
// xml.destination file("${buildDir}/jacoco/report.xml")
}
}
@ -163,6 +181,47 @@ sonarqube {
properties {
property "sonar.projectName", "Player Analytics"
property "sonar.projectKey", "com.djrapitops:Plan"
property "sonar.coverage.jacoco.xmlReportPaths", "**/build/report/jacoco/test/jacocoTestReport.xml"
property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml"
}
}
abstract class PrintSnapshotVersionTask extends DefaultTask {
@TaskAction
def print() {
def versionsDir = project.file("$project.buildDir/versions")
def textFile = project.file("$project.buildDir/versions/snapshot.txt")
versionsDir.mkdirs()
Files.deleteIfExists(textFile.toPath())
textFile.createNewFile()
textFile << "$project.version"
}
}
abstract class PrintJarNameVersionTask extends DefaultTask {
@TaskAction
def print() {
def versionsDir = project.file("$project.buildDir/versions")
def textFile = project.file("$project.buildDir/versions/jar.txt")
versionsDir.mkdirs()
Files.deleteIfExists(textFile.toPath())
textFile.createNewFile()
textFile << "$project.majorVersion.$project.minorVersion-build-$project.buildVersion"
}
}
abstract class PrintHumanReadableVersionTask extends DefaultTask {
@TaskAction
def print() {
def versionsDir = project.file("$project.buildDir/versions")
def textFile = project.file("$project.buildDir/versions/human.txt")
versionsDir.mkdirs()
Files.deleteIfExists(textFile.toPath())
textFile.createNewFile()
textFile << "$project.fullVersion"
}
}
// Create a task using the task type
tasks.register('snapshotVersion', PrintSnapshotVersionTask)
tasks.register('jarNameVersion', PrintJarNameVersionTask)
tasks.register('humanReadableVersion', PrintHumanReadableVersionTask)

View File

@ -5,21 +5,22 @@ repositories {
}
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
implementation "net.playeranalytics:platform-abstraction-layer-bukkit:$palVersion"
implementation "org.bstats:bstats-bukkit:$bstatsVersion"
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-bukkit:$palVersion"
shadow "org.bstats:bstats-bukkit:$bstatsVersion"
compileOnly "me.clip:placeholderapi:$placeholderapiVersion"
compileOnly "com.destroystokyo.paper:paper-api:$paperVersion"
testImplementation "com.destroystokyo.paper:paper-api:$paperVersion"
testImplementation "com.destroystokyo.paper:paper-api:$paperVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}
shadowJar {
relocate 'org.bstats', 'com.djrapitops.plan.utilities.metrics'
relocate 'org.slf4j', 'plan.org.slf4j'
configurations = [project.configurations.shadow]
relocate 'org.bstats', 'net.playeranalytics.bstats.utilities.metrics'
}

View File

@ -35,6 +35,8 @@ import javax.inject.Singleton;
@Singleton
public class BukkitServerShutdownSave extends ServerShutdownSave {
private static final String IS_STOPPED = "isStopped";
@Inject
public BukkitServerShutdownSave(
Locale locale,
@ -57,7 +59,7 @@ public class BukkitServerShutdownSave extends ServerShutdownSave {
Class<?> minecraftServerClass = Reflection.getMinecraftClass("MinecraftServer");
Object minecraftServer = Reflection.getField(minecraftServerClass, "SERVER", minecraftServerClass).get(null);
return Reflection.getField(minecraftServerClass, "isStopped", boolean.class).get(minecraftServer);
return Reflection.getField(minecraftServerClass, IS_STOPPED, boolean.class).get(minecraftServer);
} catch (Exception | NoClassDefFoundError | NoSuchFieldError e) {
return false;
}
@ -71,7 +73,7 @@ public class BukkitServerShutdownSave extends ServerShutdownSave {
Class<?> craftServerClass = Reflection.getCraftBukkitClass("CraftServer");
Object minecraftServer = Reflection.getField(craftServerClass, "console", minecraftServerClass).get(Bukkit.getServer());
return (Boolean) minecraftServerClass.getMethod("isStopped").invoke(minecraftServer);
return (Boolean) minecraftServerClass.getMethod(IS_STOPPED).invoke(minecraftServer);
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
return false;
}
@ -85,7 +87,7 @@ public class BukkitServerShutdownSave extends ServerShutdownSave {
Class<?> craftServerClass = Reflection.getCraftBukkitClass("CraftServer");
Object minecraftServer = Reflection.getField(craftServerClass, "console", minecraftServerClass).get(Bukkit.getServer());
return (Boolean) minecraftServerClass.getMethod("isStopped").invoke(minecraftServer);
return (Boolean) minecraftServerClass.getMethod(IS_STOPPED).invoke(minecraftServer);
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
return false;
}

View File

@ -25,6 +25,8 @@ import com.djrapitops.plan.gathering.ServerShutdownSave;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.settings.theme.PlanColorScheme;
import com.djrapitops.plan.utilities.java.ThreadContextClassLoaderSwap;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.BukkitPlatformLayer;
import net.playeranalytics.plugin.PlatformAbstractionLayer;
import net.playeranalytics.plugin.scheduling.RunnableFactory;
@ -55,6 +57,7 @@ public class Plan extends JavaPlugin implements PlanPlugin {
private PluginLogger pluginLogger;
private RunnableFactory runnableFactory;
private PlatformAbstractionLayer abstractionLayer;
private ErrorLogger errorLogger;
@Override
public void onLoad() {
@ -71,7 +74,8 @@ public class Plan extends JavaPlugin implements PlanPlugin {
.server(getServer())
.build();
try {
system = component.system();
system = ThreadContextClassLoaderSwap.performOperation(getClass().getClassLoader(), component::system);
errorLogger = component.errorLogger();
serverShutdownSave = component.serverShutdownSave();
locale = system.getLocaleSystem().getLocale();
system.enable();
@ -170,42 +174,42 @@ public class Plan extends JavaPlugin implements PlanPlugin {
pluginLogger.warn("Attempted to register '" + name + "'-command, but it is not in plugin.yml!");
continue;
}
registering.setExecutor(new BukkitCommand(runnableFactory, system.getErrorLogger(), command));
registering.setExecutor(new BukkitCommand(runnableFactory, errorLogger, command));
}
}
/**
* @deprecated Deprecated due to use of APF Config
* @deprecated Deprecated due to use of custom Config
*/
@Override
@Deprecated
@Deprecated(since = "Config.java (2018)")
public void reloadConfig() {
throw new IllegalStateException("This method should be used on this plugin. Use onReload() instead");
}
/**
* @deprecated Deprecated due to use of APF Config
* @deprecated Deprecated due to use of custom Config
*/
@Override
@Deprecated
@Deprecated(since = "Config.java (2018)")
public FileConfiguration getConfig() {
throw new IllegalStateException("This method should be used on this plugin. Use getMainConfig() instead");
}
/**
* @deprecated Deprecated due to use of APF Config
* @deprecated Deprecated due to use of custom Config
*/
@Override
@Deprecated
@Deprecated(since = "Config.java (2018)")
public void saveConfig() {
throw new IllegalStateException("This method should be used on this plugin. Use getMainConfig().save() instead");
}
/**
* @deprecated Deprecated due to use of APF Config
* @deprecated Deprecated due to use of custom Config
*/
@Override
@Deprecated
@Deprecated(since = "Config.java (2018)")
public void saveDefaultConfig() {
throw new IllegalStateException("This method should be used on this plugin.");
}
@ -214,4 +218,4 @@ public class Plan extends JavaPlugin implements PlanPlugin {
public PlanSystem getSystem() {
return system;
}
}
}

View File

@ -24,6 +24,7 @@ import com.djrapitops.plan.modules.bukkit.BukkitPlanModule;
import com.djrapitops.plan.modules.bukkit.BukkitServerPropertiesModule;
import com.djrapitops.plan.modules.bukkit.BukkitSuperClassBindingModule;
import com.djrapitops.plan.modules.bukkit.BukkitTaskModule;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import dagger.BindsInstance;
import dagger.Component;
import net.playeranalytics.plugin.PlatformAbstractionLayer;
@ -59,6 +60,8 @@ public interface PlanBukkitComponent {
ServerShutdownSave serverShutdownSave();
ErrorLogger errorLogger();
@Component.Builder
interface Builder {

View File

@ -0,0 +1,66 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.addons.placeholderapi;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.settings.config.PlanConfig;
import me.clip.placeholderapi.PlaceholderAPI;
import net.playeranalytics.plugin.scheduling.RunnableFactory;
import net.playeranalytics.plugin.scheduling.TimeAmount;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Singleton
public class PlaceholderCacheRefreshTask extends TaskSystem.Task {
private final PlanConfig config;
@Inject
public PlaceholderCacheRefreshTask(PlanConfig config) {
this.config = config;
}
@Override
public void register(RunnableFactory runnableFactory) {
runnableFactory.create(this).runTaskTimer(
TimeAmount.toTicks(60, TimeUnit.SECONDS),
TimeAmount.toTicks(15, TimeUnit.SECONDS)
);
}
@Override
public void run() {
if (config.getNode("Plugins.PlaceholderAPI").isEmpty()) {
cancel(); // Cancel the task and don't do anything if PlaceholderAPI is not installed.
return;
}
List<String> placeholders = config.getStringList("Plugins.PlaceholderAPI.Load_these_placeholders_on_join");
if (placeholders.isEmpty() || placeholders.size() == 1 && placeholders.contains("%plan_server_uuid%")) {
// Don't do anything if using default settings or placeholder api is not installed
return;
}
for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
PlaceholderAPI.setPlaceholders(onlinePlayer, placeholders.toString());
}
}
}

View File

@ -28,9 +28,7 @@ import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@ -101,7 +99,7 @@ public class PlanPlaceholderExtension extends PlaceholderExpansion {
private String getPlaceholderValue(String params, UUID uuid) {
try {
String value = placeholders.onPlaceholderRequest(uuid, params, Collections.emptyList());
String value = placeholders.onPlaceholderRequest(uuid, parseRequest(params), parseParameters(params));
if ("true".equals(value)) { //hack
value = PlaceholderAPIPlugin.booleanTrue();
@ -116,6 +114,23 @@ public class PlanPlaceholderExtension extends PlaceholderExpansion {
}
}
private String parseRequest(String params) {
return params.split(":")[0];
}
private List<String> parseParameters(String params) {
List<String> parameters = new ArrayList<>();
boolean first = true;
for (String parameter : params.split(":")) {
if (first) {
first = false;
} else {
parameters.add(parameter);
}
}
return parameters;
}
private String getCached(String params, UUID uuid) {
String key = params + "-" + uuid;

View File

@ -24,15 +24,33 @@ import java.util.UUID;
public class BukkitCMDSender implements CMDSender {
private static Boolean hasChatAPI = null;
final CommandSender sender;
public BukkitCMDSender(CommandSender sender) {
this.sender = sender;
}
protected static synchronized boolean hasBungeeChatAPI() {
if (hasChatAPI == null) {
try {
Class.forName("net.md_5.bungee.api.chat.ComponentBuilder");
hasChatAPI = true;
} catch (ClassNotFoundException e) {
hasChatAPI = false;
}
}
return hasChatAPI;
}
@Override
public MessageBuilder buildMessage() {
return new BukkitPartBuilder(this);
if (hasBungeeChatAPI()) {
return new BukkitPartBuilder(this);
} else {
return new ConsoleMessageBuilder(sender::sendMessage);
}
}
@Override

View File

@ -53,7 +53,7 @@ public class BukkitPlayerCMDSender extends BukkitCMDSender {
@Override
public boolean supportsChatEvents() {
return true;
return hasBungeeChatAPI();
}
@Override

View File

@ -0,0 +1,85 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.gathering.domain;
import org.bukkit.entity.Player;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.UUID;
public class BukkitPlayerData implements PlatformPlayerData {
private final Player player;
private final String joinAddress;
public BukkitPlayerData(Player player, String joinAddress) {
this.player = player;
this.joinAddress = joinAddress;
}
@Override
public UUID getUUID() {
return player.getUniqueId();
}
@Override
public String getName() {
return player.getName();
}
@Override
public Optional<String> getDisplayName() {
return Optional.of(player.getDisplayName());
}
@Override
public Optional<Boolean> isBanned() {
return Optional.of(player.isBanned());
}
@Override
public Optional<Boolean> isOperator() {
return Optional.of(player.isOp());
}
@Override
public Optional<String> getJoinAddress() {
return Optional.ofNullable(joinAddress);
}
@Override
public Optional<String> getCurrentWorld() {
return Optional.of(player.getWorld().getName());
}
@Override
public Optional<String> getCurrentGameMode() {
return Optional.ofNullable(player.getGameMode()).map(Enum::name);
}
@Override
public Optional<Long> getRegisterDate() {
return Optional.of(player.getFirstPlayed());
}
@Override
public Optional<InetAddress> getIPAddress() {
return Optional.ofNullable(player.getAddress()).map(InetSocketAddress::getAddress);
}
}

View File

@ -28,9 +28,9 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Listener that keeps track of actions that are not considered being AFK.
@ -51,7 +51,7 @@ public class BukkitAFKListener implements Listener {
@Inject
public BukkitAFKListener(PlanConfig config, ErrorLogger errorLogger) {
this.errorLogger = errorLogger;
this.ignorePermissionInfo = new HashMap<>();
this.ignorePermissionInfo = new ConcurrentHashMap<>();
BukkitAFKListener.assignAFKTracker(config);
}

View File

@ -20,7 +20,7 @@ import com.djrapitops.plan.delivery.domain.Nickname;
import com.djrapitops.plan.gathering.cache.NicknameCache;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.transactions.events.NicknameStoreTransaction;
import com.djrapitops.plan.storage.database.transactions.events.StoreNicknameTransaction;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import org.bukkit.entity.Player;
@ -76,7 +76,7 @@ public class ChatListener implements Listener {
UUID uuid = player.getUniqueId();
String displayName = player.getDisplayName();
dbSystem.getDatabase().executeTransaction(new NicknameStoreTransaction(
dbSystem.getDatabase().executeTransaction(new StoreNicknameTransaction(
uuid, new Nickname(displayName, time, serverInfo.getServerUUID()),
(playerUUID, name) -> nicknameCache.getDisplayName(playerUUID).map(name::equals).orElse(false)
));

View File

@ -20,6 +20,8 @@ import com.djrapitops.plan.delivery.formatting.EntityNameFormatter;
import com.djrapitops.plan.delivery.formatting.ItemNameFormatter;
import com.djrapitops.plan.gathering.cache.SessionCache;
import com.djrapitops.plan.gathering.domain.ActiveSession;
import com.djrapitops.plan.gathering.domain.PlayerKill;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.processing.processors.player.MobKillProcessor;
import com.djrapitops.plan.processing.processors.player.PlayerKillProcessor;
@ -45,14 +47,17 @@ import java.util.Optional;
*/
public class DeathEventListener implements Listener {
private final ServerInfo serverInfo;
private final Processing processing;
private final ErrorLogger errorLogger;
@Inject
public DeathEventListener(
ServerInfo serverInfo,
Processing processing,
ErrorLogger errorLogger
) {
this.serverInfo = serverInfo;
this.processing = processing;
this.errorLogger = errorLogger;
}
@ -69,13 +74,13 @@ public class DeathEventListener implements Listener {
try {
Optional<Player> foundKiller = findKiller(dead);
if (!foundKiller.isPresent()) {
if (foundKiller.isEmpty()) {
return;
}
Player killer = foundKiller.get();
Runnable processor = dead instanceof Player
? new PlayerKillProcessor(killer.getUniqueId(), time, dead.getUniqueId(), findWeapon(dead))
? new PlayerKillProcessor(getKiller(killer), getVictim((Player) dead), serverInfo.getServerIdentifier(), findWeapon(dead), time)
: new MobKillProcessor(killer.getUniqueId());
processing.submitCritical(processor);
} catch (Exception e) {
@ -83,6 +88,14 @@ public class DeathEventListener implements Listener {
}
}
private PlayerKill.Killer getKiller(Player killer) {
return new PlayerKill.Killer(killer.getUniqueId(), killer.getName());
}
private PlayerKill.Victim getVictim(Player victim) {
return new PlayerKill.Victim(victim.getUniqueId(), victim.getName());
}
public Optional<Player> findKiller(Entity dead) {
EntityDamageEvent entityDamageEvent = dead.getLastDamageCause();
if (!(entityDamageEvent instanceof EntityDamageByEntityEvent)) {

View File

@ -21,7 +21,7 @@ import com.djrapitops.plan.gathering.domain.ActiveSession;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.WorldAliasSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.transactions.events.WorldNameStoreTransaction;
import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import org.bukkit.entity.Player;
@ -78,7 +78,7 @@ public class GameModeChangeListener implements Listener {
String gameMode = event.getNewGameMode().name();
String worldName = player.getWorld().getName();
dbSystem.getDatabase().executeTransaction(new WorldNameStoreTransaction(serverInfo.getServerUUID(), worldName));
dbSystem.getDatabase().executeTransaction(new StoreWorldNameTransaction(serverInfo.getServerUUID(), worldName));
worldAliasSettings.addWorld(worldName);
Optional<ActiveSession> cachedSession = SessionCache.getCachedSession(uuid);

View File

@ -16,29 +16,20 @@
*/
package com.djrapitops.plan.gathering.listeners.bukkit;
import com.djrapitops.plan.delivery.domain.Nickname;
import com.djrapitops.plan.delivery.domain.PlayerName;
import com.djrapitops.plan.delivery.domain.ServerName;
import com.djrapitops.plan.delivery.export.Exporter;
import com.djrapitops.plan.extension.CallEvents;
import com.djrapitops.plan.extension.ExtensionSvc;
import com.djrapitops.plan.gathering.cache.NicknameCache;
import com.djrapitops.plan.gathering.cache.SessionCache;
import com.djrapitops.plan.gathering.domain.ActiveSession;
import com.djrapitops.plan.gathering.geolocation.GeolocationCache;
import com.djrapitops.plan.gathering.cache.JoinAddressCache;
import com.djrapitops.plan.gathering.domain.BukkitPlayerData;
import com.djrapitops.plan.gathering.domain.event.PlayerJoin;
import com.djrapitops.plan.gathering.domain.event.PlayerLeave;
import com.djrapitops.plan.gathering.events.PlayerJoinEventConsumer;
import com.djrapitops.plan.gathering.events.PlayerLeaveEventConsumer;
import com.djrapitops.plan.gathering.listeners.Status;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
import com.djrapitops.plan.settings.config.paths.ExportSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.transactions.events.*;
import com.djrapitops.plan.storage.database.transactions.events.BanStatusTransaction;
import com.djrapitops.plan.storage.database.transactions.events.KickStoreTransaction;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@ -48,11 +39,7 @@ import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import javax.inject.Inject;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
/**
* Event Listener for PlayerJoin, PlayerQuit and PlayerKickEvents.
@ -61,61 +48,47 @@ import java.util.function.Supplier;
*/
public class PlayerOnlineListener implements Listener {
private final PlanConfig config;
private final Processing processing;
private final PlayerJoinEventConsumer playerJoinEventConsumer;
private final PlayerLeaveEventConsumer playerLeaveEventConsumer;
private final JoinAddressCache joinAddressCache;
private final ServerInfo serverInfo;
private final DBSystem dbSystem;
private final ExtensionSvc extensionService;
private final Exporter exporter;
private final GeolocationCache geolocationCache;
private final NicknameCache nicknameCache;
private final SessionCache sessionCache;
private final ErrorLogger errorLogger;
private final Status status;
private final Map<UUID, String> joinAddresses;
@Inject
public PlayerOnlineListener(
PlanConfig config,
Processing processing,
PlayerJoinEventConsumer playerJoinEventConsumer,
PlayerLeaveEventConsumer playerLeaveEventConsumer,
JoinAddressCache joinAddressCache,
ServerInfo serverInfo,
DBSystem dbSystem,
ExtensionSvc extensionService,
Exporter exporter,
GeolocationCache geolocationCache,
NicknameCache nicknameCache,
SessionCache sessionCache,
Status status,
ErrorLogger errorLogger
) {
this.config = config;
this.processing = processing;
this.playerJoinEventConsumer = playerJoinEventConsumer;
this.playerLeaveEventConsumer = playerLeaveEventConsumer;
this.joinAddressCache = joinAddressCache;
this.serverInfo = serverInfo;
this.dbSystem = dbSystem;
this.extensionService = extensionService;
this.exporter = exporter;
this.geolocationCache = geolocationCache;
this.nicknameCache = nicknameCache;
this.sessionCache = sessionCache;
this.status = status;
this.errorLogger = errorLogger;
joinAddresses = new HashMap<>();
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLogin(PlayerLoginEvent event) {
try {
PlayerLoginEvent.Result result = event.getResult();
UUID playerUUID = event.getPlayer().getUniqueId();
ServerUUID serverUUID = serverInfo.getServerUUID();
boolean banned = result == PlayerLoginEvent.Result.KICK_BANNED;
boolean banned = PlayerLoginEvent.Result.KICK_BANNED == event.getResult();
String joinAddress = event.getHostname();
if (!joinAddress.isEmpty()) {
joinAddresses.put(playerUUID, joinAddress.substring(0, joinAddress.lastIndexOf(':')));
joinAddress = joinAddress.substring(0, joinAddress.lastIndexOf(':'));
joinAddressCache.put(playerUUID, joinAddress);
}
dbSystem.getDatabase().executeTransaction(new BanStatusTransaction(playerUUID, serverUUID, () -> banned));
dbSystem.getDatabase().executeTransaction(new BanStatusTransaction(playerUUID, serverUUID, banned));
} catch (Exception e) {
errorLogger.error(e, ErrorContext.builder().related(event, event.getResult()).build());
}
@ -156,64 +129,28 @@ public class PlayerOnlineListener implements Listener {
}
private void actOnJoinEvent(PlayerJoinEvent event) {
Player player = event.getPlayer();
UUID playerUUID = player.getUniqueId();
ServerUUID serverUUID = serverInfo.getServerUUID();
long time = System.currentTimeMillis();
UUID playerUUID = event.getPlayer().getUniqueId();
BukkitAFKListener.afkTracker.performedAction(playerUUID, time);
String world = player.getWorld().getName();
String gm = player.getGameMode().name();
Database database = dbSystem.getDatabase();
database.executeTransaction(new WorldNameStoreTransaction(serverUUID, world));
InetAddress address = player.getAddress().getAddress();
Supplier<String> getHostName = () -> getHostname(player);
String playerName = player.getName();
String displayName = player.getDisplayName();
boolean gatheringGeolocations = config.isTrue(DataGatheringSettings.GEOLOCATIONS);
if (gatheringGeolocations) {
database.executeTransaction(
new GeoInfoStoreTransaction(playerUUID, address, time, geolocationCache::getCountry)
);
}
database.executeTransaction(new PlayerServerRegisterTransaction(playerUUID,
player::getFirstPlayed, playerName, serverUUID, getHostName));
database.executeTransaction(new OperatorStatusTransaction(playerUUID, serverUUID, player.isOp()));
ActiveSession session = new ActiveSession(playerUUID, serverUUID, time, world, gm);
session.getExtraData().put(PlayerName.class, new PlayerName(playerName));
session.getExtraData().put(ServerName.class, new ServerName(serverInfo.getServer().getIdentifiableName()));
sessionCache.cacheSession(playerUUID, session)
.ifPresent(previousSession -> database.executeTransaction(new SessionEndTransaction(previousSession)));
database.executeTransaction(new NicknameStoreTransaction(
playerUUID, new Nickname(displayName, time, serverUUID),
(uuid, name) -> nicknameCache.getDisplayName(playerUUID).map(name::equals).orElse(false)
));
processing.submitNonCritical(() -> extensionService.updatePlayerValues(playerUUID, playerName, CallEvents.PLAYER_JOIN));
if (config.isTrue(ExportSettings.EXPORT_ON_ONLINE_STATUS_CHANGE)) {
processing.submitNonCritical(() -> exporter.exportPlayerPage(playerUUID, playerName));
}
}
private String getHostname(Player player) {
return joinAddresses.get(player.getUniqueId());
playerJoinEventConsumer.onJoinGameServer(PlayerJoin.builder()
.server(serverInfo.getServer())
.player(new BukkitPlayerData(event.getPlayer(), joinAddressCache.getNullableString(playerUUID)))
.time(time)
.build());
}
@EventHandler(priority = EventPriority.NORMAL)
public void beforePlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
UUID playerUUID = player.getUniqueId();
String playerName = player.getName();
processing.submitNonCritical(() -> extensionService.updatePlayerValues(playerUUID, playerName, CallEvents.PLAYER_LEAVE));
try {
playerLeaveEventConsumer.beforeLeave(PlayerLeave.builder()
.server(serverInfo.getServer())
.player(new BukkitPlayerData(event.getPlayer(), null))
.time(System.currentTimeMillis())
.build());
} catch (Exception e) {
errorLogger.error(e, ErrorContext.builder().related(event).build());
}
}
@EventHandler(priority = EventPriority.MONITOR)
@ -227,23 +164,13 @@ public class PlayerOnlineListener implements Listener {
private void actOnQuitEvent(PlayerQuitEvent event) {
long time = System.currentTimeMillis();
Player player = event.getPlayer();
String playerName = player.getName();
UUID playerUUID = player.getUniqueId();
ServerUUID serverUUID = serverInfo.getServerUUID();
UUID playerUUID = event.getPlayer().getUniqueId();
BukkitAFKListener.afkTracker.loggedOut(playerUUID, time);
joinAddresses.remove(playerUUID);
nicknameCache.removeDisplayName(playerUUID);
dbSystem.getDatabase().executeTransaction(new BanStatusTransaction(playerUUID, serverUUID, player::isBanned));
sessionCache.endSession(playerUUID, time)
.ifPresent(endedSession -> dbSystem.getDatabase().executeTransaction(new SessionEndTransaction(endedSession)));
if (config.isTrue(ExportSettings.EXPORT_ON_ONLINE_STATUS_CHANGE)) {
processing.submitNonCritical(() -> exporter.exportPlayerPage(playerUUID, playerName));
}
playerLeaveEventConsumer.onLeaveGameServer(PlayerLeave.builder()
.server(serverInfo.getServer())
.player(new BukkitPlayerData(event.getPlayer(), null))
.time(System.currentTimeMillis())
.build());
}
}

View File

@ -21,7 +21,7 @@ import com.djrapitops.plan.gathering.domain.ActiveSession;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.WorldAliasSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.transactions.events.WorldNameStoreTransaction;
import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import org.bukkit.entity.Player;
@ -72,7 +72,7 @@ public class WorldChangeListener implements Listener {
String worldName = player.getWorld().getName();
String gameMode = player.getGameMode().name();
dbSystem.getDatabase().executeTransaction(new WorldNameStoreTransaction(serverInfo.getServerUUID(), worldName));
dbSystem.getDatabase().executeTransaction(new StoreWorldNameTransaction(serverInfo.getServerUUID(), worldName));
worldAliasSettings.addWorld(worldName);
Optional<ActiveSession> cachedSession = SessionCache.getCachedSession(uuid);

View File

@ -44,6 +44,7 @@ import org.bukkit.event.player.PlayerQuitEvent;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -62,16 +63,16 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
//the server is pinging the client every 40 Ticks (2 sec) - so check it then
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/PlayerConnection.java#L178
private static boolean pingMethodAvailable;
private final Map<UUID, Long> startRecording;
private final Map<UUID, List<DateObj<Integer>>> playerHistory;
private final Listeners listeners;
private final PlanConfig config;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final RunnableFactory runnableFactory;
private final boolean pingMethodAvailable;
private PingMethod pingMethod;
@Inject
@ -79,14 +80,13 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
Listeners listeners,
PlanConfig config,
DBSystem dbSystem,
ServerInfo serverInfo,
RunnableFactory runnableFactory
ServerInfo serverInfo
) {
this.listeners = listeners;
this.config = config;
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.runnableFactory = runnableFactory;
startRecording = new ConcurrentHashMap<>();
playerHistory = new HashMap<>();
Optional<PingMethod> pingMethod = loadPingMethod();
@ -117,7 +117,7 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
}
Logger.getGlobal().log(
Level.WARNING,
"Plan: No Ping method found - Ping will not be recorded:" + reasonsForUnavailability.toString()
() -> "Plan: No Ping method found - Ping will not be recorded:" + reasonsForUnavailability.toString()
);
return Optional.empty();
@ -138,6 +138,16 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
@Override
public void run() {
long time = System.currentTimeMillis();
Iterator<Map.Entry<UUID, Long>> starts = startRecording.entrySet().iterator();
while (starts.hasNext()) {
Map.Entry<UUID, Long> start = starts.next();
if (time >= start.getValue()) {
addPlayer(start.getKey());
starts.remove();
}
}
Iterator<Map.Entry<UUID, List<DateObj<Integer>>>> iterator = playerHistory.entrySet().iterator();
while (iterator.hasNext()) {
@ -164,11 +174,12 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
}
}
public void addPlayer(Player player) {
playerHistory.put(player.getUniqueId(), new ArrayList<>());
public void addPlayer(UUID uuid) {
playerHistory.put(uuid, new ArrayList<>());
}
public void removePlayer(Player player) {
startRecording.remove(player.getUniqueId());
playerHistory.remove(player.getUniqueId());
}
@ -182,15 +193,11 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();
Long pingDelay = config.get(TimeSettings.PING_PLAYER_LOGIN_DELAY);
if (pingDelay >= TimeUnit.HOURS.toMillis(2L)) {
Long pingDelayMs = config.get(TimeSettings.PING_PLAYER_LOGIN_DELAY);
if (pingDelayMs >= TimeUnit.HOURS.toMillis(2L)) {
return;
}
runnableFactory.create(() -> {
if (player.isOnline()) {
addPlayer(player);
}
}).runTaskLater(TimeAmount.toTicks(pingDelay, TimeUnit.MILLISECONDS));
startRecording.put(player.getUniqueId(), System.currentTimeMillis() + pingDelayMs);
}
@EventHandler

View File

@ -0,0 +1,64 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2016-2018
*
* 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 com.djrapitops.plan.gathering.timed;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
public class PingMethodReflection {
private PingMethodReflection() {
/* Static utility class */
}
/**
* Get methods for getting the ping value.
*
* @param craftPlayerClass Class for player
* @param entityPlayer Class for player entity
* @param methodName getHandle method
* @param fieldName Latency field name
* @return [getHandle method, getPlayer method]
* @throws NoSuchMethodException Method doesn't exist
* @throws IllegalAccessException Method can't be accessed
* @throws NoSuchFieldException Field can't be accessed
* @throws IllegalArgumentException Something else
*/
public static MethodHandle[] getMethods(
Class<?> craftPlayerClass,
Class<?> entityPlayer,
String methodName,
String fieldName
) throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Method handleMethod = craftPlayerClass.getDeclaredMethod(methodName);
return new MethodHandle[]{
lookup.unreflect(handleMethod),
lookup.findGetter(entityPlayer, fieldName, Integer.TYPE)
};
}
}

View File

@ -27,8 +27,6 @@ import com.djrapitops.plan.utilities.java.Reflection;
import org.bukkit.entity.Player;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
public class ReflectiveLatencyFieldMethod implements PingMethod {
@ -37,26 +35,26 @@ public class ReflectiveLatencyFieldMethod implements PingMethod {
private String reasonForUnavailability;
private static void setMethods() throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
MethodHandle[] methodHandles = PingMethodReflection.getMethods(
Reflection.getCraftBukkitClass("entity.CraftPlayer"),
Reflection.getMinecraftClass("EntityPlayer"),
"getHandle",
"latency"
);
getHandleMethod = methodHandles[0];
pingField = methodHandles[1];
}
@Override
public boolean isAvailable() {
MethodHandle localHandle = null;
MethodHandle localPing = null;
try {
Class<?> craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer");
Class<?> entityPlayer = Reflection.getMinecraftClass("EntityPlayer");
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Method getHandleMethod = craftPlayerClass.getDeclaredMethod("getHandle");
localHandle = lookup.unreflect(getHandleMethod);
localPing = lookup.findGetter(entityPlayer, "latency", Integer.TYPE);
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException | IllegalArgumentException reflectiveEx) {
setMethods();
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException |
IllegalArgumentException reflectiveEx) {
reasonForUnavailability = reflectiveEx.toString();
return false;
}
getHandleMethod = localHandle;
pingField = localPing;
return pingField != null;
}

View File

@ -27,8 +27,6 @@ import com.djrapitops.plan.utilities.java.Reflection;
import org.bukkit.entity.Player;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
public class ReflectiveLevelEntityPlayerLatencyFieldMethod implements PingMethod {
@ -37,26 +35,34 @@ public class ReflectiveLevelEntityPlayerLatencyFieldMethod implements PingMethod
private String reasonForUnavailability;
private static void setMethods() throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
MethodHandle[] methodHandles = PingMethodReflection.getMethods(
Reflection.getCraftBukkitClass("entity.CraftPlayer"),
getEntityPlayer(),
"getHandle",
"latency"
);
getHandleMethod = methodHandles[0];
pingField = methodHandles[1];
}
private static Class<?> getEntityPlayer() throws ClassNotFoundException {
try {
return Class.forName("net.minecraft.server.level.EntityPlayer");
} catch (NullPointerException classLoaderError) {
throw new ClassNotFoundException("net.minecraft.server.level.EntityPlayer");
}
}
@Override
public boolean isAvailable() {
MethodHandle localHandle = null;
MethodHandle localPing = null;
try {
Class<?> craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer");
Class<?> entityPlayer = Class.forName("net.minecraft.server.level.EntityPlayer");
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Method getHandleMethod = craftPlayerClass.getDeclaredMethod("getHandle");
localHandle = lookup.unreflect(getHandleMethod);
localPing = lookup.findGetter(entityPlayer, "latency", Integer.TYPE);
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException | ClassNotFoundException | IllegalArgumentException reflectiveEx) {
setMethods();
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException | ClassNotFoundException |
IllegalArgumentException reflectiveEx) {
reasonForUnavailability = reflectiveEx.toString();
return false;
}
getHandleMethod = localHandle;
pingField = localPing;
return pingField != null;
}

View File

@ -27,8 +27,6 @@ import com.djrapitops.plan.utilities.java.Reflection;
import org.bukkit.entity.Player;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
public class ReflectivePingFieldMethod implements PingMethod {
@ -37,26 +35,26 @@ public class ReflectivePingFieldMethod implements PingMethod {
private String reasonForUnavailability;
private static void setMethods() throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
MethodHandle[] methodHandles = PingMethodReflection.getMethods(
Reflection.getCraftBukkitClass("entity.CraftPlayer"),
Reflection.getMinecraftClass("EntityPlayer"),
"getHandle",
"ping"
);
getHandleMethod = methodHandles[0];
pingField = methodHandles[1];
}
@Override
public boolean isAvailable() {
MethodHandle localHandle = null;
MethodHandle localPing = null;
try {
Class<?> craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer");
Class<?> entityPlayer = Reflection.getMinecraftClass("EntityPlayer");
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Method getHandleMethod = craftPlayerClass.getDeclaredMethod("getHandle");
localHandle = lookup.unreflect(getHandleMethod);
localPing = lookup.findGetter(entityPlayer, "ping", Integer.TYPE);
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException | IllegalArgumentException reflectiveEx) {
setMethods();
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException |
IllegalArgumentException reflectiveEx) {
reasonForUnavailability = reflectiveEx.toString();
return false;
}
getHandleMethod = localHandle;
pingField = localPing;
return pingField != null;
}

View File

@ -27,8 +27,6 @@ import com.djrapitops.plan.utilities.java.Reflection;
import org.bukkit.entity.Player;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
public class ReflectiveUnmappedLatencyFieldMethod implements PingMethod {
@ -37,27 +35,34 @@ public class ReflectiveUnmappedLatencyFieldMethod implements PingMethod {
private String reasonForUnavailability;
private static void setMethods() throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
MethodHandle[] methodHandles = PingMethodReflection.getMethods(
Reflection.getCraftBukkitClass("entity.CraftPlayer"),
getEntityPlayer(),
"getHandle",
"e"
);
getHandleMethod = methodHandles[0];
pingField = methodHandles[1];
}
private static Class<?> getEntityPlayer() throws ClassNotFoundException {
try {
return Class.forName("net.minecraft.server.level.EntityPlayer");
} catch (NullPointerException classLoaderError) {
throw new ClassNotFoundException("net.minecraft.server.level.EntityPlayer");
}
}
@Override
public boolean isAvailable() {
MethodHandle localHandle = null;
MethodHandle localPing = null;
try {
Class<?> craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer");
Class<?> entityPlayer = Class.forName("net.minecraft.server.level.EntityPlayer");
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Method getHandleMethod = craftPlayerClass.getDeclaredMethod("getHandle");
localHandle = lookup.unreflect(getHandleMethod);
// e is latency in unmapped jar
localPing = lookup.findGetter(entityPlayer, "e", Integer.TYPE);
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException | ClassNotFoundException | IllegalArgumentException reflectiveEx) {
setMethods();
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException | ClassNotFoundException |
IllegalArgumentException reflectiveEx) {
reasonForUnavailability = reflectiveEx.toString();
return false;
}
getHandleMethod = localHandle;
pingField = localPing;
return pingField != null;
}

View File

@ -17,6 +17,10 @@
package com.djrapitops.plan.modules.bukkit;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.addons.placeholderapi.PlaceholderCacheRefreshTask;
import com.djrapitops.plan.delivery.web.ResourceWriteTask;
import com.djrapitops.plan.delivery.web.WebAssetVersionCheckTask;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask;
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
@ -26,6 +30,7 @@ import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
import com.djrapitops.plan.storage.upkeep.DBCleanTask;
import com.djrapitops.plan.storage.upkeep.ExtensionDisableOnGameServerTask;
import com.djrapitops.plan.storage.upkeep.LogsFolderCleanTask;
import com.djrapitops.plan.storage.upkeep.OldDependencyCacheDeletionTask;
import dagger.Binds;
@ -83,4 +88,24 @@ public interface BukkitTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindOldDependencyCacheDeletion(OldDependencyCacheDeletionTask deletionTask);
@Binds
@IntoSet
TaskSystem.Task bindResourceWriteTask(ResourceWriteTask resourceWriteTask);
@Binds
@IntoSet
TaskSystem.Task bindWebAssetVersionCheckTask(WebAssetVersionCheckTask webAssetVersionCheckTask);
@Binds
@IntoSet
TaskSystem.Task bindActiveCookieStoreExpiryTask(ActiveCookieExpiryCleanupTask activeCookieExpiryCleanupTask);
@Binds
@IntoSet
TaskSystem.Task bindExtensionDisableOnGameServerTask(ExtensionDisableOnGameServerTask extensionDisableOnGameServerTask);
@Binds
@IntoSet
TaskSystem.Task bindPlaceholderWarmupTask(PlaceholderCacheRefreshTask placeholderCacheRefreshTask);
}

View File

@ -32,7 +32,6 @@ import javax.inject.Singleton;
@Singleton
public class BukkitDBSystem extends DBSystem {
private final PlanConfig config;
@Inject
public BukkitDBSystem(
@ -42,8 +41,7 @@ public class BukkitDBSystem extends DBSystem {
PlanConfig config,
PluginLogger logger
) {
super(locale, sqLiteDB, logger);
this.config = config;
super(config, locale, sqLiteDB, logger);
databases.add(mySQLDB);
databases.add(sqLiteDB.usingDefaultFile());

View File

@ -71,7 +71,7 @@ public final class Reflection {
field.setAccessible(true);
// A function for retrieving a specific field value
return new FieldAccessor<T>() {
return new FieldAccessor<>() {
@Override
@SuppressWarnings("unchecked")
@ -140,7 +140,7 @@ public final class Reflection {
private static Class<?> getCanonicalClass(String canonicalName) {
try {
return Class.forName(canonicalName);
} catch (ClassNotFoundException e) {
} catch (ClassNotFoundException | NullPointerException e) {
throw new IllegalArgumentException("Cannot find " + canonicalName, e);
}
}

View File

@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.gathering.listeners;
import com.djrapitops.plan.gathering.cache.SessionCache;
import com.djrapitops.plan.gathering.domain.ActiveSession;
import com.djrapitops.plan.gathering.listeners.bukkit.BukkitAFKListener;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.TimeSettings;
@ -51,11 +53,15 @@ class BukkitAFKListenerTest {
when(config.get(TimeSettings.AFK_THRESHOLD)).thenReturn(TimeUnit.MINUTES.toMillis(3));
errorLogger = Mockito.mock(ErrorLogger.class);
underTest = new BukkitAFKListener(config, errorLogger);
new SessionCache().cacheSession(TestConstants.PLAYER_ONE_UUID, new ActiveSession(null, null, 0, null, null));
new SessionCache().cacheSession(TestConstants.PLAYER_TWO_UUID, new ActiveSession(null, null, 0, null, null));
}
@AfterEach
void ensureNoErrors() {
verifyNoInteractions(errorLogger);
SessionCache.clear();
}
@Test

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.DaggerPlanBukkitComponent;
import com.djrapitops.plan.PlanBukkitComponent;
import com.djrapitops.plan.PlanPlugin;
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.storage.database.SQLDB;
import org.bukkit.Server;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.scheduler.BukkitScheduler;
@ -44,6 +45,7 @@ public class BukkitMockComponent {
public BukkitMockComponent(Path tempDir) {
this.tempDir = tempDir;
SQLDB.setDownloadDriver(false);
}
public PlanPlugin getPlanMock() throws Exception {

View File

@ -1,20 +1,20 @@
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
implementation "net.playeranalytics:platform-abstraction-layer-bungeecord:$palVersion"
implementation "org.bstats:bstats-bungeecord:$bstatsVersion"
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-bungeecord:$palVersion"
shadow "org.bstats:bstats-bungeecord:$bstatsVersion"
compileOnly "net.md-5:bungeecord-api:$bungeeVersion"
compileOnly "com.imaginarycode.minecraft:RedisBungee:$redisBungeeVersion"
testImplementation "net.md-5:bungeecord-api:$bungeeVersion"
testImplementation "com.imaginarycode.minecraft:RedisBungee:$redisBungeeVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}
shadowJar {
relocate 'org.bstats', 'com.djrapitops.plan.utilities.metrics'
relocate 'org.slf4j', 'plan.org.slf4j'
configurations = [project.configurations.shadow]
relocate 'org.bstats', 'net.playeranalytics.bstats.utilities.metrics'
}

View File

@ -23,6 +23,8 @@ import com.djrapitops.plan.exceptions.EnableException;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.settings.theme.PlanColorScheme;
import com.djrapitops.plan.utilities.java.ThreadContextClassLoaderSwap;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.md_5.bungee.api.plugin.Plugin;
import net.playeranalytics.plugin.BungeePlatformLayer;
import net.playeranalytics.plugin.PlatformAbstractionLayer;
@ -46,6 +48,7 @@ public class PlanBungee extends Plugin implements PlanPlugin {
private PluginLogger logger;
private RunnableFactory runnableFactory;
private PlatformAbstractionLayer abstractionLayer;
private ErrorLogger errorLogger;
@Override
public void onLoad() {
@ -61,7 +64,8 @@ public class PlanBungee extends Plugin implements PlanPlugin {
.abstractionLayer(abstractionLayer)
.build();
try {
system = component.system();
system = ThreadContextClassLoaderSwap.performOperation(getClass().getClassLoader(), component::system);
errorLogger = component.errorLogger();
locale = system.getLocaleSystem().getLocale();
system.enable();
@ -107,7 +111,7 @@ public class PlanBungee extends Plugin implements PlanPlugin {
return;
}
for (String name : command.getAliases()) {
getProxy().getPluginManager().registerCommand(this, new BungeeCommand(runnableFactory, system.getErrorLogger(), command, name));
getProxy().getPluginManager().registerCommand(this, new BungeeCommand(runnableFactory, errorLogger, command, name));
}
}

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan;
import com.djrapitops.plan.commands.PlanCommand;
import com.djrapitops.plan.modules.*;
import com.djrapitops.plan.modules.bungee.*;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import dagger.BindsInstance;
import dagger.Component;
import net.playeranalytics.plugin.PlatformAbstractionLayer;
@ -50,6 +51,8 @@ public interface PlanBungeeComponent {
PlanSystem system();
ErrorLogger errorLogger();
@Component.Builder
interface Builder {

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.commands.use.MessageBuilder;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.apache.commons.text.TextStringBuilder;
import java.util.Arrays;
@ -68,7 +69,7 @@ class BungeePartBuilder implements MessageBuilder {
@Override
public MessageBuilder hover(String text) {
part.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(text).create()));
part.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(text)));
return this;
}
@ -81,7 +82,7 @@ class BungeePartBuilder implements MessageBuilder {
public MessageBuilder hover(Collection<String> lines) {
ComponentBuilder hoverMsg = new ComponentBuilder("");
hoverMsg.append(new TextStringBuilder().appendWithSeparators(lines, "\n").build());
part.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverMsg.create()));
part.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(hoverMsg.create())));
return this;
}

View File

@ -0,0 +1,78 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.gathering.domain;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.UUID;
public class BungeePlayerData implements PlatformPlayerData {
private final ProxiedPlayer player;
public BungeePlayerData(ProxiedPlayer player) {this.player = player;}
@Override
public UUID getUUID() {
return player.getUniqueId();
}
@Override
public String getName() {
return player.getName();
}
@Override
public Optional<InetAddress> getIPAddress() {
Optional<InetAddress> ip = getIPFromSocketAddress();
if (ip.isPresent()) return ip;
return getIpFromOldMethod();
}
@SuppressWarnings("deprecation") // ProxiedPlayer#getAddress is deprecated
private Optional<InetAddress> getIpFromOldMethod() {
try {
return Optional.ofNullable(player.getAddress()).map(InetSocketAddress::getAddress);
} catch (NoSuchMethodError e) {
return Optional.empty();
}
}
private Optional<InetAddress> getIPFromSocketAddress() {
try {
SocketAddress socketAddress = player.getSocketAddress();
if (socketAddress instanceof InetSocketAddress) {
return Optional.of(((InetSocketAddress) socketAddress).getAddress());
}
// Unix domain socket address requires Java 16 compatibility.
// These connections come from the same physical machine
Class<?> jdk16SocketAddressType = Class.forName("java.net.UnixDomainSocketAddress");
if (jdk16SocketAddressType.isAssignableFrom(socketAddress.getClass())) {
return Optional.of(InetAddress.getLocalHost());
}
} catch (NoSuchMethodError | ClassNotFoundException | UnknownHostException e) {
// Ignored
}
return Optional.empty();
}
}

View File

@ -16,23 +16,13 @@
*/
package com.djrapitops.plan.gathering.listeners.bungee;
import com.djrapitops.plan.delivery.domain.PlayerName;
import com.djrapitops.plan.delivery.domain.ServerName;
import com.djrapitops.plan.delivery.export.Exporter;
import com.djrapitops.plan.extension.CallEvents;
import com.djrapitops.plan.extension.ExtensionSvc;
import com.djrapitops.plan.gathering.cache.SessionCache;
import com.djrapitops.plan.gathering.domain.ActiveSession;
import com.djrapitops.plan.gathering.geolocation.GeolocationCache;
import com.djrapitops.plan.gathering.domain.BungeePlayerData;
import com.djrapitops.plan.gathering.domain.event.PlayerJoin;
import com.djrapitops.plan.gathering.domain.event.PlayerLeave;
import com.djrapitops.plan.gathering.events.PlayerJoinEventConsumer;
import com.djrapitops.plan.gathering.events.PlayerLeaveEventConsumer;
import com.djrapitops.plan.gathering.events.PlayerSwitchServerEventConsumer;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
import com.djrapitops.plan.settings.config.paths.ExportSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.transactions.events.GeoInfoStoreTransaction;
import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@ -44,8 +34,6 @@ import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import javax.inject.Inject;
import java.net.InetAddress;
import java.util.UUID;
/**
* Player Join listener for Bungee.
@ -54,34 +42,24 @@ import java.util.UUID;
*/
public class PlayerOnlineListener implements Listener {
private final PlanConfig config;
private final Processing processing;
private final DBSystem dbSystem;
private final ExtensionSvc extensionService;
private final Exporter exporter;
private final GeolocationCache geolocationCache;
private final SessionCache sessionCache;
private final PlayerJoinEventConsumer joinEventConsumer;
private final PlayerLeaveEventConsumer leaveEventConsumer;
private final PlayerSwitchServerEventConsumer switchServerEventConsumer;
private final ServerInfo serverInfo;
private final ErrorLogger errorLogger;
@Inject
public PlayerOnlineListener(
PlanConfig config,
Processing processing,
DBSystem dbSystem,
ExtensionSvc extensionService,
Exporter exporter, GeolocationCache geolocationCache,
SessionCache sessionCache,
PlayerJoinEventConsumer joinEventConsumer,
PlayerLeaveEventConsumer leaveEventConsumer,
PlayerSwitchServerEventConsumer switchServerEventConsumer,
ServerInfo serverInfo,
ErrorLogger errorLogger
) {
this.config = config;
this.processing = processing;
this.dbSystem = dbSystem;
this.extensionService = extensionService;
this.exporter = exporter;
this.geolocationCache = geolocationCache;
this.sessionCache = sessionCache;
this.joinEventConsumer = joinEventConsumer;
this.leaveEventConsumer = leaveEventConsumer;
this.switchServerEventConsumer = switchServerEventConsumer;
this.serverInfo = serverInfo;
this.errorLogger = errorLogger;
}
@ -96,82 +74,48 @@ public class PlayerOnlineListener implements Listener {
}
private void actOnLogin(PostLoginEvent event) {
ProxiedPlayer player = event.getPlayer();
UUID playerUUID = player.getUniqueId();
String playerName = player.getName();
InetAddress address = player.getAddress().getAddress();
long time = System.currentTimeMillis();
ProxiedPlayer player = event.getPlayer();
ActiveSession session = new ActiveSession(playerUUID, serverInfo.getServerUUID(), time, null, null);
session.getExtraData().put(PlayerName.class, new PlayerName(playerName));
session.getExtraData().put(ServerName.class, new ServerName("Proxy Server"));
sessionCache.cacheSession(playerUUID, session);
Database database = dbSystem.getDatabase();
boolean gatheringGeolocations = config.isTrue(DataGatheringSettings.GEOLOCATIONS);
if (gatheringGeolocations) {
database.executeTransaction(
new GeoInfoStoreTransaction(playerUUID, address, time, geolocationCache::getCountry)
);
}
database.executeTransaction(new PlayerRegisterTransaction(playerUUID, () -> time, playerName));
processing.submitNonCritical(() -> extensionService.updatePlayerValues(playerUUID, playerName, CallEvents.PLAYER_JOIN));
if (config.isTrue(ExportSettings.EXPORT_ON_ONLINE_STATUS_CHANGE)) {
processing.submitNonCritical(() -> exporter.exportPlayerPage(playerUUID, playerName));
}
joinEventConsumer.onJoinProxyServer(PlayerJoin.builder()
.server(serverInfo.getServer())
.player(new BungeePlayerData(player))
.time(time)
.build());
}
@EventHandler(priority = EventPriority.NORMAL)
public void beforeLogout(PlayerDisconnectEvent event) {
ProxiedPlayer player = event.getPlayer();
UUID playerUUID = player.getUniqueId();
String playerName = player.getName();
processing.submitNonCritical(() -> extensionService.updatePlayerValues(playerUUID, playerName, CallEvents.PLAYER_LEAVE));
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onLogout(PlayerDisconnectEvent event) {
try {
actOnLogout(event);
leaveEventConsumer.beforeLeave(PlayerLeave.builder()
.server(serverInfo.getServer())
.player(new BungeePlayerData(event.getPlayer()))
.time(System.currentTimeMillis())
.build());
} catch (Exception e) {
errorLogger.error(e, ErrorContext.builder().related(event).build());
}
}
private void actOnLogout(PlayerDisconnectEvent event) {
ProxiedPlayer player = event.getPlayer();
String playerName = player.getName();
UUID playerUUID = player.getUniqueId();
sessionCache.endSession(playerUUID, System.currentTimeMillis());
if (config.isTrue(ExportSettings.EXPORT_ON_ONLINE_STATUS_CHANGE)) {
processing.submitNonCritical(() -> exporter.exportPlayerPage(playerUUID, playerName));
@EventHandler(priority = EventPriority.HIGHEST)
public void onLogout(PlayerDisconnectEvent event) {
try {
leaveEventConsumer.onLeaveProxyServer(PlayerLeave.builder()
.server(serverInfo.getServer())
.player(new BungeePlayerData(event.getPlayer()))
.time(System.currentTimeMillis())
.build());
} catch (Exception e) {
errorLogger.error(e, ErrorContext.builder().related(event).build());
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onServerSwitch(ServerSwitchEvent event) {
try {
actOnServerSwitch(event);
switchServerEventConsumer.onServerSwitch(new BungeePlayerData(event.getPlayer()), System.currentTimeMillis());
} catch (Exception e) {
errorLogger.error(e, ErrorContext.builder().related(event).build());
}
}
private void actOnServerSwitch(ServerSwitchEvent event) {
ProxiedPlayer player = event.getPlayer();
String playerName = player.getName();
UUID playerUUID = player.getUniqueId();
long time = System.currentTimeMillis();
// Replaces the current session in the cache.
ActiveSession session = new ActiveSession(playerUUID, serverInfo.getServerUUID(), time, null, null);
session.getExtraData().put(PlayerName.class, new PlayerName(playerName));
session.getExtraData().put(ServerName.class, new ServerName("Proxy Server"));
sessionCache.cacheSession(playerUUID, session);
if (config.isTrue(ExportSettings.EXPORT_ON_ONLINE_STATUS_CHANGE)) {
processing.submitNonCritical(() -> exporter.exportPlayerPage(playerUUID, playerName));
}
}
}

View File

@ -44,6 +44,7 @@ import net.playeranalytics.plugin.server.Listeners;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
@ -54,35 +55,43 @@ import java.util.concurrent.TimeUnit;
@Singleton
public class BungeePingCounter extends TaskSystem.Task implements Listener {
private final Map<UUID, Long> startRecording;
private final Map<UUID, List<DateObj<Integer>>> playerHistory;
private final Listeners listeners;
private final PlanConfig config;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final RunnableFactory runnableFactory;
@Inject
public BungeePingCounter(
Listeners listeners,
PlanConfig config,
DBSystem dbSystem,
ServerInfo serverInfo,
RunnableFactory runnableFactory
ServerInfo serverInfo
) {
this.listeners = listeners;
this.config = config;
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.runnableFactory = runnableFactory;
startRecording = new ConcurrentHashMap<>();
playerHistory = new HashMap<>();
}
@Override
public void run() {
long time = System.currentTimeMillis();
Iterator<Map.Entry<UUID, List<DateObj<Integer>>>> iterator = playerHistory.entrySet().iterator();
Iterator<Map.Entry<UUID, Long>> starts = startRecording.entrySet().iterator();
while (starts.hasNext()) {
Map.Entry<UUID, Long> start = starts.next();
if (time >= start.getValue()) {
addPlayer(start.getKey());
starts.remove();
}
}
Iterator<Map.Entry<UUID, List<DateObj<Integer>>>> iterator = playerHistory.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<UUID, List<DateObj<Integer>>> entry = iterator.next();
UUID uuid = entry.getKey();
@ -119,12 +128,13 @@ public class BungeePingCounter extends TaskSystem.Task implements Listener {
}
}
public void addPlayer(ProxiedPlayer player) {
playerHistory.put(player.getUniqueId(), new ArrayList<>());
public void addPlayer(UUID uuid) {
playerHistory.put(uuid, new ArrayList<>());
}
public void removePlayer(ProxiedPlayer player) {
playerHistory.remove(player.getUniqueId());
startRecording.remove(player.getUniqueId());
}
private int getPing(ProxiedPlayer player) {
@ -134,15 +144,11 @@ public class BungeePingCounter extends TaskSystem.Task implements Listener {
@EventHandler
public void onPlayerJoin(ServerConnectedEvent joinEvent) {
ProxiedPlayer player = joinEvent.getPlayer();
Long pingDelay = config.get(TimeSettings.PING_PLAYER_LOGIN_DELAY);
if (pingDelay >= TimeUnit.HOURS.toMillis(2L)) {
Long pingDelayMs = config.get(TimeSettings.PING_PLAYER_LOGIN_DELAY);
if (pingDelayMs >= TimeUnit.HOURS.toMillis(2L)) {
return;
}
runnableFactory.create(() -> {
if (player.isConnected()) {
addPlayer(player);
}
}).runTaskLater(TimeAmount.toTicks(pingDelay, TimeUnit.MILLISECONDS));
startRecording.put(player.getUniqueId(), System.currentTimeMillis() + pingDelayMs);
}
@EventHandler

View File

@ -28,6 +28,7 @@ import com.djrapitops.plan.settings.locale.lang.PluginLang;
import net.playeranalytics.plugin.server.PluginLogger;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
/**
@ -38,6 +39,7 @@ import javax.inject.Singleton;
@Singleton
public class BungeeServerInfo extends ServerInfo {
private final String currentVersion;
private final ServerLoader fromFile;
private final ServerLoader fromDatabase;
@ -49,6 +51,7 @@ public class BungeeServerInfo extends ServerInfo {
@Inject
public BungeeServerInfo(
@Named("currentVersion") String currentVersion,
ServerProperties serverProperties,
ServerFileLoader fromFile,
ServerDBLoader fromDatabase,
@ -58,6 +61,7 @@ public class BungeeServerInfo extends ServerInfo {
PluginLogger logger
) {
super(serverProperties);
this.currentVersion = currentVersion;
this.fromFile = fromFile;
this.fromDatabase = fromDatabase;
this.processing = processing;
@ -89,7 +93,7 @@ public class BungeeServerInfo extends ServerInfo {
}
/**
* @throws EnableException
* @throws EnableException If IP setting is unset
*/
private void checkIfDefaultIP() {
String ip = serverProperties.getIp();
@ -101,7 +105,7 @@ public class BungeeServerInfo extends ServerInfo {
}
/**
* @throws EnableException
* @throws EnableException If IP setting is unset
*/
private Server registerServer() {
Server proxy = createServerObject();
@ -115,11 +119,11 @@ public class BungeeServerInfo extends ServerInfo {
}
/**
* @throws EnableException
* @throws EnableException If IP setting is unset
*/
private Server createServerObject() {
ServerUUID serverUUID = generateNewUUID();
String accessAddress = addresses.getAccessAddress().orElseThrow(() -> new EnableException("Velocity can not have '0.0.0.0' or '' as an address. Set up 'Server.IP' setting."));
return new Server(-1, serverUUID, "BungeeCord", accessAddress, true);
return new Server(-1, serverUUID, "BungeeCord", accessAddress, true, currentVersion);
}
}

View File

@ -17,6 +17,9 @@
package com.djrapitops.plan.modules.bungee;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.delivery.web.ResourceWriteTask;
import com.djrapitops.plan.delivery.web.WebAssetVersionCheckTask;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask;
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.timed.BungeePingCounter;
@ -72,4 +75,16 @@ public interface BungeeTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindOldDependencyCacheDeletion(OldDependencyCacheDeletionTask deletionTask);
@Binds
@IntoSet
TaskSystem.Task bindResourceWriteTask(ResourceWriteTask resourceWriteTask);
@Binds
@IntoSet
TaskSystem.Task bindWebAssetVersionCheckTask(WebAssetVersionCheckTask webAssetVersionCheckTask);
@Binds
@IntoSet
TaskSystem.Task bindActiveCookieStoreExpiryTask(ActiveCookieExpiryCleanupTask activeCookieExpiryCleanupTask);
}

View File

@ -48,13 +48,13 @@ class BungeeSystemTest {
private DBPreparer dbPreparer;
@BeforeEach
void prepareSystem(@TempDir Path temp) throws Exception {
void prepareSystem(@TempDir Path temp) {
component = new BungeeMockComponent(temp);
dbPreparer = new DBPreparer(component.getPlanSystem(), TEST_PORT_NUMBER);
dbPreparer = new DBPreparer(new BungeeSystemTestDependencies(component.getPlanSystem()), TEST_PORT_NUMBER);
}
@Test
void bungeeEnables() throws Exception {
void bungeeEnables() {
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();
@ -75,45 +75,41 @@ class BungeeSystemTest {
@Test
void bungeeDoesNotEnableWithDefaultIP() {
EnableException thrown = assertThrows(EnableException.class, () -> {
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();
config.set(WebserverSettings.PORT, TEST_PORT_NUMBER);
config.set(ProxySettings.IP, "0.0.0.0");
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();
config.set(WebserverSettings.PORT, TEST_PORT_NUMBER);
config.set(ProxySettings.IP, "0.0.0.0");
DBSystem dbSystem = bungeeSystem.getDatabaseSystem();
SQLiteDB db = dbSystem.getSqLiteFactory().usingDefaultFile();
db.setTransactionExecutorServiceProvider(MoreExecutors::newDirectExecutorService);
dbSystem.setActiveDatabase(db);
DBSystem dbSystem = bungeeSystem.getDatabaseSystem();
SQLiteDB db = dbSystem.getSqLiteFactory().usingDefaultFile();
db.setTransactionExecutorServiceProvider(MoreExecutors::newDirectExecutorService);
dbSystem.setActiveDatabase(db);
bungeeSystem.enable(); // Throws EnableException
} finally {
bungeeSystem.disable();
}
});
EnableException thrown = assertThrows(EnableException.class, bungeeSystem::enable);
assertEquals("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.", thrown.getMessage());
} finally {
bungeeSystem.disable();
}
assertEquals("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.", thrown.getMessage());
}
@Test
void testEnableNoMySQL() {
assertThrows(EnableException.class, () -> {
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();
config.set(WebserverSettings.PORT, TEST_PORT_NUMBER);
config.set(ProxySettings.IP, "8.8.8.8");
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();
config.set(WebserverSettings.PORT, TEST_PORT_NUMBER);
config.set(ProxySettings.IP, "8.8.8.8");
bungeeSystem.enable(); // Throws EnableException
} finally {
bungeeSystem.disable();
}
});
assertThrows(EnableException.class, bungeeSystem::enable);
} finally {
bungeeSystem.disable();
}
}
@Test
void testEnableWithMySQL() throws Exception {
void testEnableWithMySQL() {
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();

View File

@ -0,0 +1,48 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.storage.database.DBSystem;
import utilities.DBPreparer;
public class BungeeSystemTestDependencies implements DBPreparer.Dependencies {
private final PlanSystem system;
public BungeeSystemTestDependencies(PlanSystem system) {
this.system = system;
}
@Override
public PlanConfig config() {
return system.getConfigSystem().getConfig();
}
@Override
public DBSystem dbSystem() {
return system.getDatabaseSystem();
}
@Override
public void enable() {
}
@Override
public void disable() {
}
}

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.DaggerPlanBungeeComponent;
import com.djrapitops.plan.PlanBungee;
import com.djrapitops.plan.PlanBungeeComponent;
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.storage.database.SQLDB;
import java.nio.file.Path;
@ -37,9 +38,10 @@ public class BungeeMockComponent {
public BungeeMockComponent(Path tempDir) {
this.tempDir = tempDir;
SQLDB.setDownloadDriver(false);
}
public PlanBungee getPlanMock() throws Exception {
public PlanBungee getPlanMock() {
if (planMock == null) {
planMock = PlanBungeeMocker.setUp()
.withDataFolder(tempDir.toFile())
@ -51,7 +53,7 @@ public class BungeeMockComponent {
return planMock;
}
public PlanSystem getPlanSystem() throws Exception {
public PlanSystem getPlanSystem() {
if (component == null) {
PlanBungee planMock = getPlanMock();
component = DaggerPlanBungeeComponent.builder()

View File

@ -25,6 +25,7 @@ import net.md_5.bungee.api.plugin.PluginDescription;
import net.md_5.bungee.api.plugin.PluginManager;
import org.mockito.Mockito;
import utilities.TestConstants;
import utilities.TestResources;
import utilities.mocks.objects.TestLogger;
import java.io.File;
@ -38,9 +39,10 @@ import static org.mockito.Mockito.when;
*
* @author AuroraLS3
*/
public class PlanBungeeMocker extends Mocker {
public class PlanBungeeMocker {
private PlanBungee planMock;
private File tempFolder;
private PlanBungeeMocker() {
}
@ -51,7 +53,6 @@ public class PlanBungeeMocker extends Mocker {
private PlanBungeeMocker mockPlugin() {
planMock = Mockito.mock(PlanBungee.class);
super.planMock = planMock;
doReturn(new ColorScheme("§1", "§2", "§3")).when(planMock).getColorScheme();
@ -63,11 +64,12 @@ public class PlanBungeeMocker extends Mocker {
}
PlanBungeeMocker withDataFolder(File tempFolder) {
when(planMock.getDataFolder()).thenReturn(tempFolder);
this.tempFolder = tempFolder;
when(planMock.getDataFolder()).thenReturn(this.tempFolder);
return this;
}
PlanBungeeMocker withResourceFetchingFromJar() throws Exception {
PlanBungeeMocker withResourceFetchingFromJar() {
return this;
}
@ -91,7 +93,8 @@ public class PlanBungeeMocker extends Mocker {
}
PlanBungeeMocker withPluginDescription() {
File pluginYml = getFile("/bungee.yml");
File pluginYml = tempFolder.toPath().resolve("jar").resolve("bungee.yml").toFile();
TestResources.copyResourceIntoFile(pluginYml, "/bungee.yml");
HashSet<String> empty = new HashSet<>();
PluginDescription pluginDescription = new PluginDescription("Plan", "", "9.9.9", "AuroraLS3", empty, empty, pluginYml, "");
when(planMock.getDescription()).thenReturn(pluginDescription);

View File

@ -1,74 +1,193 @@
import dev.vankka.dependencydownload.task.GenerateDependencyDownloadResourceTask
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id "dev.vankka.dependencydownload.plugin" version "$dependencyDownloadVersion"
id "com.github.node-gradle.node" version "3.4.0"
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.2"
}
configurations {
// Runtime downloading scopes
mysqlDriver
sqliteDriver
testImplementation.extendsFrom mysqlDriver, sqliteDriver
compileOnly.extendsFrom mysqlDriver, sqliteDriver
swaggerJson // swagger.json configuration
}
task generateResourceForMySQLDriver(type: GenerateDependencyDownloadResourceTask) {
var conf = configurations.mysqlDriver
configuration = conf
file = "assets/plan/dependencies/" + conf.name + ".txt"
// Not necessary to include in the resource
includeShadowJarRelocations = false
}
task generateResourceForSQLiteDriver(type: GenerateDependencyDownloadResourceTask) {
var conf = configurations.sqliteDriver
configuration = conf
file = "assets/plan/dependencies/" + conf.name + ".txt"
// Not necessary to include in the resource
includeShadowJarRelocations = false
}
dependencies {
implementation "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
implementation project(":api")
compileOnly project(":extensions")
implementation project(path: ":extensions", configuration: 'shadow')
implementation "org.apache.commons:commons-text:$commonsTextVersion"
implementation "org.apache.commons:commons-compress:$commonsCompressVersion"
implementation "com.github.ben-manes.caffeine:caffeine:$caffeineVersion"
implementation "mysql:mysql-connector-java:$mysqlVersion"
implementation "org.xerial:sqlite-jdbc:$sqliteVersion"
implementation "com.zaxxer:HikariCP:$hikariVersion"
implementation "org.slf4j:slf4j-nop:$slf4jVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation "com.maxmind.geoip2:geoip2:$geoIpVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
shadow project(":extensions")
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
compileOnly "net.kyori:adventure-api:4.9.3"
shadow("dev.vankka:dependencydownload-runtime:$dependencyDownloadVersion") {
// Effectively disables relocating
exclude module: "jar-relocator"
}
mysqlDriver "mysql:mysql-connector-java:$mysqlVersion"
sqliteDriver "org.xerial:sqlite-jdbc:$sqliteVersion"
shadow "org.apache.commons:commons-text:$commonsTextVersion"
shadow "org.apache.commons:commons-compress:$commonsCompressVersion"
shadow "commons-codec:commons-codec:$commonsCodecVersion"
shadow "com.github.ben-manes.caffeine:caffeine:$caffeineVersion"
shadow "com.zaxxer:HikariCP:$hikariVersion"
shadow "org.slf4j:slf4j-nop:$slf4jVersion"
shadow "org.slf4j:slf4j-api:$slf4jVersion"
shadow "com.maxmind.geoip2:geoip2:$geoIpVersion"
shadow "com.google.code.gson:gson:$gsonVersion"
shadow "org.eclipse.jetty:jetty-server:$jettyVersion"
shadow "org.eclipse.jetty:jetty-alpn-java-server:$jettyVersion"
shadow "org.eclipse.jetty.http2:http2-server:$jettyVersion"
shadow 'com.googlecode.json-simple:json-simple:1.1.1' // json simple used by UUIDFetcher
// Swagger annotations
implementation "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"
implementation "io.swagger.core.v3:swagger-core-jakarta:$swaggerVersion"
implementation "io.swagger.core.v3:swagger-jaxrs2-jakarta:$swaggerVersion"
testImplementation project(":api")
testImplementation "com.google.code.gson:gson:$gsonVersion"
testImplementation "org.seleniumhq.selenium:selenium-java:4.4.0"
testImplementation "org.testcontainers:testcontainers:$testContainersVersion"
testImplementation "org.testcontainers:junit-jupiter:$testContainersVersion"
testImplementation "org.testcontainers:nginx:$testContainersVersion"
}
import org.apache.tools.ant.filters.ReplaceTokens
task updateVersion(type: Copy) {
from('src/main/resources') {
include 'plugin.yml'
include 'bungee.yml'
include 'nukkit.yml'
include 'fabric.mod.json'
}
into 'build/sources/resources/'
filter(ReplaceTokens, tokens: [version: '' + project.ext.fullVersion])
}
node {
download = true
version = "16.14.2"
nodeProjectDir = file("$rootDir/react/dashboard")
}
task yarnBundle(type: YarnTask) {
inputs.files(fileTree("$rootDir/react/dashboard/src"))
inputs.file("$rootDir/react/dashboard/package.json")
outputs.dir("$rootDir/react/dashboard/build")
dependsOn yarn_install
args = ['run', 'build']
}
task copyYarnBuildResults {
inputs.files(fileTree("$rootDir/react/dashboard/build"))
outputs.dir("$rootDir/common/build/resources/main/assets/plan/web")
dependsOn yarnBundle
doLast {
mkdir "$rootDir/common/build/resources/main/assets/plan/web"
copy {
from "$rootDir/react/dashboard/build"
into "$rootDir/common/build/resources/main/assets/plan/web"
}
}
}
task determineAssetModifications {
inputs.files(fileTree(dir: 'src/main/resources/assets/plan/web'))
inputs.files(fileTree(dir: 'src/main/resources/assets/plan/locale'))
outputs.file("build/resources/main/assets/plan/AssetVersion.yml")
doLast {
mkdir "build/resources/main/assets/plan"
def versionFile = file("build/resources/main/assets/plan/AssetVersion.yml")
versionFile.text = "" // Clear previous build
ConfigurableFileTree tree = fileTree(dir: 'src/main/resources/assets/plan/web')
tree.forEach { File f ->
def gitModified = new ByteArrayOutputStream()
exec {
commandLine 'git', 'log', '-1', '--pretty=%ct', f.toString()
standardOutput = gitModified
}
def gitModifiedAsString = gitModified.toString().strip()
// git returns UNIX time in seconds, but most things in Java use UNIX time in milliseconds
def modified = gitModifiedAsString.isEmpty() ? System.currentTimeMillis() : Long.parseLong(gitModifiedAsString) * 1000
def relativePath = tree.getDir().toPath().relativize(f.toPath()) // File path relative to the tree
versionFile.text += String.format( // writing YAML as raw text probably isn't the best idea
"%s: %s\n", relativePath.toString().replace('.', ','), modified
)
}
tree = fileTree(dir: 'src/main/resources/assets/plan/locale')
tree.forEach { File f ->
def gitModified = new ByteArrayOutputStream()
exec {
commandLine 'git', 'log', '-1', '--pretty=%ct', f.toString()
standardOutput = gitModified
}
def gitModifiedAsString = gitModified.toString().strip()
// git returns UNIX time in seconds, but most things in Java use UNIX time in milliseconds
def modified = gitModifiedAsString.isEmpty() ? System.currentTimeMillis() : Long.parseLong(gitModifiedAsString) * 1000
def relativePath = tree.getDir().toPath().relativize(f.toPath()) // File path relative to the tree
versionFile.text += String.format( // writing YAML as raw text probably isn't the best idea
"%s: %s\n", relativePath.toString().replace('.', ','), modified
)
}
}
}
resolve { // Swagger json generation task
outputFileName = 'swagger'
outputFormat = 'JSON'
prettyPrint = 'TRUE'
classpath = sourceSets.main.runtimeClasspath
buildClasspath = classpath
resourcePackages = [
'com.djrapitops.plan.delivery.webserver',
'com.djrapitops.plan.delivery.webserver.resolver.auth',
'com.djrapitops.plan.delivery.webserver.resolver.json',
]
outputDir = file('build/generated-resources/swagger/assets/plan/web/')
}
task swaggerJsonJar(type: Jar) {
dependsOn resolve
archiveClassifier.set("resolve")
from 'build/generated-resources/swagger'
}
artifacts {
swaggerJson swaggerJsonJar
}
processResources {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
dependsOn copyYarnBuildResults
dependsOn determineAssetModifications
dependsOn generateResourceForMySQLDriver
dependsOn generateResourceForSQLiteDriver
dependsOn updateVersion
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from 'build/sources/resources'
}
shadowJar {
dependsOn processResources
// Exclude these files
exclude "**/*.svg"
exclude "**/*.psd"
exclude "**/module-info.class"
exclude "module-info.class"
exclude 'META-INF/versions/' // Causes Sponge to crash
exclude 'org/apache/http/**/*' // Unnecessary http client depended on by geolite2 implementation
exclude 'mozilla/**/*'
// Exclude unnecessary SQLite drivers
exclude '**/Linux/android-arm/libsqlitejdbc.so'
exclude '**/DragonFlyBSD/**/libsqlitejdbc.so'
relocate 'com.google.protobuf', 'plan.com.mysql.cj.x.google.protobuf'
relocate 'com.maxmind', 'plan.com.maxmind'
relocate 'com.fasterxml', 'plan.com.fasterxml'
relocate 'com.zaxxer', 'plan.com.zaxxer'
relocate 'com.google.gson', 'plan.com.google.gson'
relocate 'com.google.errorprone', 'plan.com.google.errorprone'
relocate 'org.bstats', 'plan.org.bstats'
relocate 'org.slf4j', 'plan.org.slf4j'
// Exclude test dependencies
exclude "org/junit/**/*"
exclude "org/opentest4j/**/*"
exclude "org/checkerframework/**/*"
exclude "org/apiguardian/**/*"
exclude "org/mockito/**/*"
exclude "org/selenium/**/*"
exclude "org/jayway/**/*"
exclude "google/protobuf/**/*"
exclude "jargs/gnu/**/*"
}
configurations = [project.configurations.shadow]
}

View File

@ -17,67 +17,60 @@
package com.djrapitops.plan;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import com.djrapitops.plan.utilities.java.TriConsumer;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Service for sourcing, mapping and consuming data.
* <p>
* The service is in charge of two data flows:
* - push, given to consumers
* - pull, obtained from sources
* <p>
* The mappers facilitate a one way type transformation if needed.
* <p>
* The interface is very abstract about how data is obtained,
* but here are my general ideas of where the abstraction is coming from.
* - push cause one or multiple consumers to modify stored data
* - pull cause one or multiple suppliers to fetch stored data
* - mappers are stateless type transformers in memory
* <p>
* Example use-case:
* - PlayerJoinEvent is mapped to a generic event
* - that generic event is then consumed and mapped until the data is in a database.
* <p>
* - Some kind of data is wanted to place on a web page
* - It is requested and the suppliers and mappers give the wanted type of data.
* <p>
* Further research needed:
* - Can this limited type system represent types of data that need parameters
* (such as differentiate between two servers data)
*/
public interface DataService {
<M> DataService push(Class<M> type, M data);
<S> Optional<S> pull(Class<S> type);
<S, P> Optional<S> pull(Class<S> type, P parameter);
<A, B> B mapTo(Class<B> toType, A from);
default <S, P> Optional<S> pull(Class<S> type, Class<P> parameterType) {
return pull(type, () -> pull(parameterType).orElse(null));
default <K, T> void push(K identifier, T value) {
push(identifier, value, (Class<T>) value.getClass());
}
default <S, P> Optional<S> pull(Class<S> type, Supplier<P> parameter) {
return pull(type, parameter.get());
<K, T> void push(K identifier, T value, Class<T> type);
default <K, A, B> DataService registerOptionalMapper(Class<K> identifierType, Class<A> from, Class<B> to, BiFunction<K, A, Optional<B>> mapper) {
return registerMapper(identifierType, from, to, (id, value) -> mapper.apply(id, value).orElse(null));
}
<A, B> DataService registerMapper(Class<A> typeA, Class<B> typeB, Function<A, B> mapper);
<K, A, B> DataService registerMapper(Class<K> identifierType, Class<A> from, Class<B> to, BiFunction<K, A, B> mapper);
<M> DataService registerConsumer(Class<M> type, Consumer<M> consumer);
<K, A, B> DataService registerMapper(Class<K> identifierType, Class<A> from, Class<B> to, Function<A, B> mapper);
<S> DataService registerSupplier(Class<S> type, Supplier<S> supplier);
default <K, A, B> DataService registerDataServiceMapper(Class<K> identifierType, Class<A> from, Class<B> to, BiFunction<DataService, A, B> mapper) {
return registerMapper(identifierType, from, to, value -> mapper.apply(this, value));
}
<P, S> DataService registerSupplier(Class<S> type, Class<P> parameterType, Function<P, S> supplierWithParameter);
<K, Y, A, B> DataService registerMapper(Class<K> fromIdentifier, Class<A> from, Class<Y> toIdentifier, Class<B> to, TriConsumer<K, A, BiConsumer<Y, B>> mapper);
<P, S> DataService registerDBSupplier(Class<S> type, Class<P> parameterType, Function<P, Query<S>> supplierWithParameter);
<K, T> DataService registerSink(Class<K> identifierType, Class<T> type, BiConsumer<K, T> consumer);
interface Mapping {
<K, T> DataService registerDatabaseSink(Class<K> identifierType, Class<T> type, BiFunction<K, T, Transaction> consumer);
<K, T> Optional<T> pull(Class<T> type, K identifier);
<T> Optional<T> pullWithoutId(Class<T> type);
<K, T> DataService registerPullSource(Class<K> identifierType, Class<T> type, Function<K, T> source);
default <K, T> DataService registerOptionalPullSource(Class<K> identifierType, Class<T> type, Function<K, Optional<T>> source) {
return registerPullSource(identifierType, type, id -> source.apply(id).orElse(null));
}
<K, T> DataService registerDatabasePullSource(Class<K> identifierType, Class<T> type, Function<K, Query<T>> source);
<T> DataService registerPullSource(Class<T> type, Supplier<T> source);
<T> DataService registerDatabasePullSource(Class<T> type, Supplier<Query<T>> source);
<K, A, B> Optional<B> map(K identifier, A value, Class<B> toType);
interface Pipeline {
void register(DataService service);
}

View File

@ -18,121 +18,133 @@ package com.djrapitops.plan;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import com.djrapitops.plan.utilities.java.TriConsumer;
import dagger.Lazy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
@Singleton
public class DataSvc implements DataService {
private final MultiHashMap<Class, Mapper> mappers;
private final MultiHashMap<Class, Mapper> mappersReverse;
private final Map<Class, Supplier> suppliers;
private final Map<ClassPair, Function> suppliersWithParameter;
private final MultiHashMap<Class, Consumer> consumers;
private final Lazy<DBSystem> dbSystem;
private final Map<ClassPair, Function> pullSources;
private final Map<Class, Supplier> noIdentifierPullSources;
private final MultiHashMap<ClassPair, BiConsumer> sinks;
private final MultiHashMap<ClassPair, Mapper> mappers;
@Inject
public DataSvc(
Lazy<DBSystem> dbSystem
) {
this.dbSystem = dbSystem;
pullSources = new HashMap<>();
noIdentifierPullSources = new HashMap<>();
sinks = new MultiHashMap<>();
mappers = new MultiHashMap<>();
mappersReverse = new MultiHashMap<>();
suppliers = new ConcurrentHashMap<>();
suppliersWithParameter = new ConcurrentHashMap<>();
consumers = new MultiHashMap<>();
}
@Override
public <A> DataService push(Class<A> type, A data) {
if (data == null) return this;
List<Mapper> mappers = this.mappers.get(type);
for (Mapper mapper : mappers) {
push(mapper.typeB, mapper.func.apply(data));
public <K, T> void push(K identifier, T value, Class<T> type) {
ClassPair<K, T> classPair = new ClassPair<>((Class<K>) identifier.getClass(), type);
for (BiConsumer<K, T> sink : sinks.get(classPair)) {
sink.accept(identifier, value);
}
List<Consumer> consumers = this.consumers.get(type);
for (Consumer<A> consumer : consumers) {
consumer.accept(data);
}
if (mappers.isEmpty() && consumers.isEmpty()) {
System.out.println("WARN: Nothing consumed " + type);
for (Mapper mapper : mappers.get(classPair)) {
push(identifier, mapper.func.apply(identifier, value), mapper.typeB);
}
}
@Override
public <K, A, B> Optional<B> map(K identifier, A value, Class<B> toType) {
ClassPair<K, A> classPair = new ClassPair<>((Class<K>) identifier.getClass(), (Class<A>) value.getClass());
List<Mapper> candidates = this.mappers.get(classPair);
return candidates
.stream()
.filter(mapper -> Objects.equals(mapper.typeB, toType))
.findAny()
.map(mapper -> toType.cast(mapper.func.apply(identifier, value)));
}
@Override
public <K, A, B> DataService registerMapper(Class<K> identifierType, Class<A> from, Class<B> to, BiFunction<K, A, B> mapper) {
ClassPair<K, A> classPair = new ClassPair<>(identifierType, from);
mappers.putOne(classPair, new Mapper<>(from, to, mapper));
return this;
}
@Override
public <T> Optional<T> pull(Class<T> type) {
Supplier<T> present = this.suppliers.get(type);
if (present != null) return Optional.ofNullable(present.get());
List<Mapper> mappers = this.mappersReverse.get(type);
for (Mapper mapper : mappers) {
Optional<T> found = pull(mapper.typeA).map(mapper.func);
if (found.isPresent()) return found;
}
System.out.println("WARN: Nothing supplied " + type);
return Optional.empty();
public <K, A, B> DataService registerMapper(Class<K> identifierType, Class<A> from, Class<B> to, Function<A, B> mapper) {
return registerMapper(identifierType, from, to, (id, value) -> mapper.apply(value));
}
@Override
public <A, B> B mapTo(Class<B> toType, A from) {
List<Mapper> mappers = this.mappers.get(from.getClass());
for (Mapper mapper : mappers) {
if (mapper.typeB.equals(toType)) {
return toType.cast(mapper.func);
}
}
// TODO Figure out type mapping resolution when it needs more than one mapping
System.out.println("WARN: No mapper for " + from.getClass() + " -> " + toType);
return null;
}
@Override
public <A, B> DataService registerMapper(Class<A> typeA, Class<B> typeB, Function<A, B> mapper) {
Mapper<A, B> asWrapper = new Mapper<>(typeA, typeB, mapper);
// TODO Prevent two mappers for same 2 types with a data structure
mappers.putOne(typeA, asWrapper);
mappersReverse.putOne(typeB, asWrapper);
public <K, Y, A, B> DataService registerMapper(Class<K> fromIdentifier, Class<A> from, Class<Y> toIdentifier, Class<B> to, TriConsumer<K, A, BiConsumer<Y, B>> mapper) {
ClassPair<K, A> classPair = new ClassPair<>(fromIdentifier, from);
sinks.putOne(classPair, (id, value) -> mapper.accept(fromIdentifier.cast(id), from.cast(value), this::push));
return this;
}
@Override
public <A> DataService registerConsumer(Class<A> type, Consumer<A> consumer) {
consumers.putOne(type, consumer);
public <K, T> DataService registerSink(Class<K> identifierType, Class<T> type, BiConsumer<K, T> consumer) {
ClassPair<K, T> classPair = new ClassPair<>(identifierType, type);
sinks.putOne(classPair, consumer);
return this;
}
@Override
public <A> DataService registerSupplier(Class<A> type, Supplier<A> supplier) {
suppliers.put(type, supplier);
public <K, T> DataService registerDatabaseSink(Class<K> identifierType, Class<T> type, BiFunction<K, T, Transaction> consumer) {
return registerSink(identifierType, type, (id, value) -> dbSystem.get().getDatabase().executeTransaction(consumer.apply(id, value)));
}
@Override
public <K, T> Optional<T> pull(Class<T> type, K identifier) {
ClassPair<K, T> classPair = new ClassPair<>((Class<K>) identifier.getClass(), type);
return Optional.ofNullable(pullSources.get(classPair))
.map(source -> source.apply(identifier))
.map(type::cast);
}
@Override
public <T> Optional<T> pullWithoutId(Class<T> type) {
return Optional.ofNullable(noIdentifierPullSources.get(type))
.map(Supplier::get)
.map(type::cast);
}
@Override
public <K, T> DataService registerPullSource(Class<K> identifierType, Class<T> type, Function<K, T> source) {
ClassPair<K, T> classPair = new ClassPair<>(identifierType, type);
pullSources.put(classPair, source);
return this;
}
@Override
public <S, P> Optional<S> pull(Class<S> type, P parameter) {
if (parameter == null) return Optional.empty();
Function<P, S> function = suppliersWithParameter.get(new ClassPair<>(type, parameter.getClass()));
return function != null ? Optional.of(function.apply(parameter)) : Optional.empty();
public <K, T> DataService registerDatabasePullSource(Class<K> identifierType, Class<T> type, Function<K, Query<T>> source) {
return registerPullSource(identifierType, type, identifier -> dbSystem.get().getDatabase().query(source.apply(identifier)));
}
@Override
public <P, S> DataService registerSupplier(Class<S> type, Class<P> parameterType, Function<P, S> supplierWithParameter) {
suppliersWithParameter.put(new ClassPair<>(type, parameterType), supplierWithParameter);
public <T> DataService registerPullSource(Class<T> type, Supplier<T> source) {
noIdentifierPullSources.put(type, source);
return this;
}
@Override
public <P, S> DataService registerDBSupplier(Class<S> type, Class<P> parameterType, Function<P, Query<S>> queryVisitor) {
return registerSupplier(type, parameterType, parameter -> dbSystem.get().getDatabase().query(queryVisitor.apply(parameter)));
public <T> DataService registerDatabasePullSource(Class<T> type, Supplier<Query<T>> source) {
return registerPullSource(type, () -> dbSystem.get().getDatabase().query(source.get()));
}
private static class ClassPair<A, B> {
@ -159,16 +171,6 @@ public class DataSvc implements DataService {
}
}
private static class KeyValuePair<K, V> {
final K key;
final V value;
public KeyValuePair(K key, V value) {
this.key = key;
this.value = value;
}
}
private static class MultiHashMap<A, B> extends ConcurrentHashMap<A, List<B>> {
void putOne(A key, B value) {
@ -176,15 +178,14 @@ public class DataSvc implements DataService {
values.add(value);
put(key, values);
}
}
private static class Mapper<A, B> {
private static class Mapper<K, A, B> {
final Class<A> typeA;
final Class<B> typeB;
final Function<A, B> func;
final BiFunction<K, A, B> func;
public Mapper(Class<A> typeA, Class<B> typeB, Function<A, B> func) {
public Mapper(Class<A> typeA, Class<B> typeB, BiFunction<K, A, B> func) {
this.typeA = typeA;
this.typeB = typeB;
this.func = func;

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan;
import com.djrapitops.plan.commands.use.ColorScheme;
import com.djrapitops.plan.commands.use.Subcommand;
import net.playeranalytics.plugin.PluginInformation;
import java.io.File;
import java.io.InputStream;
@ -45,7 +46,10 @@ public interface PlanPlugin {
void onDisable();
@Deprecated
/**
* @deprecated Use {@code @Named("dataFolder") File}, or {@link PluginInformation#getDataFolder()}
*/
@Deprecated(since = "2021-03-09")
File getDataFolder();
}

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan;
import com.djrapitops.plan.api.PlanAPI;
import com.djrapitops.plan.delivery.DeliveryUtilities;
import com.djrapitops.plan.delivery.export.ExportSystem;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.web.ResolverSvc;
import com.djrapitops.plan.delivery.web.ResourceSvc;
import com.djrapitops.plan.delivery.webserver.NonProxyWebserverDisableChecker;
@ -55,6 +56,8 @@ import javax.inject.Singleton;
@Singleton
public class PlanSystem implements SubSystem {
private static final long SERVER_ENABLE_TIME = System.currentTimeMillis();
private boolean enabled = false;
private final PlanFiles files;
@ -134,21 +137,39 @@ public class PlanSystem implements SubSystem {
this.logger = logger;
this.errorLogger = errorLogger;
logger.info("");
logger.info("§2");
logger.info("§2 ██▌");
logger.info("§2 ██▌ ██▌");
logger.info("§2 ██▌██▌██▌██▌ §2Player Analytics");
logger.info("§2 ██▌██▌██▌██▌ §fv" + versionChecker.getCurrentVersion());
logger.info("");
logger.info("§2");
}
@Deprecated
public static long getServerEnableTime() {
return SERVER_ENABLE_TIME;
}
/**
* @deprecated Use {@link com.djrapitops.plan.delivery.webserver.Addresses} instead.
*/
@Deprecated(since = "Addresses.java")
public String getMainAddress() {
return webServerSystem.getAddresses().getMainAddress().orElse(webServerSystem.getAddresses().getFallbackLocalhostAddress());
}
@Override
public void enable() {
/**
* Enables only the systems that are required for {@link com.djrapitops.plan.commands.PlanCommand}.
*
* @see #enableOtherThanCommands()
*/
public void enableForCommands() {
enableSystems(configSystem);
}
/**
* Enables the rest of the systems that are not enabled in {@link #enableForCommands()}.
*/
public void enableOtherThanCommands() {
extensionService.register();
resolverService.register();
resourceService.register();
@ -159,7 +180,6 @@ public class PlanSystem implements SubSystem {
enableSystems(
files,
configSystem,
localeSystem,
versionChecker,
databaseSystem,
@ -182,6 +202,15 @@ public class PlanSystem implements SubSystem {
extensionService.registerExtensions();
enabled = true;
String javaVersion = System.getProperty("java.specification.version");
if ("1.8".equals(javaVersion) || "9".equals(javaVersion) || "10".equals(javaVersion)
) {
logger.warn("! ------- Deprecation warning ------- !");
logger.warn("Plan version 5.5 will require Java 11 or newer,");
logger.warn("consider updating your JVM as soon as possible.");
logger.warn("! ----------------------------------- !");
}
}
private void enableSystems(SubSystem... systems) {
@ -191,23 +220,9 @@ public class PlanSystem implements SubSystem {
}
@Override
public void disable() {
enabled = false;
disableSystems(
taskSystem,
cacheSystem,
listenerSystem,
importSystem,
exportSystem,
processing,
databaseSystem,
webServerSystem,
serverInfo,
localeSystem,
configSystem,
files,
versionChecker
);
public void enable() {
enableForCommands();
enableOtherThanCommands();
}
private void disableSystems(SubSystem... systems) {
@ -288,14 +303,27 @@ public class PlanSystem implements SubSystem {
return extensionService;
}
/**
* Originally visible for testing purposes.
*
* @return the error logger of the system
* @deprecated A smell, dagger should be used to construct things instead.
*/
@Deprecated
public ErrorLogger getErrorLogger() {
return errorLogger;
@Override
public void disable() {
enabled = false;
Formatters.clearSingleton();
extensionService.disableUpdates();
disableSystems(
taskSystem,
cacheSystem,
listenerSystem,
importSystem,
exportSystem,
processing,
databaseSystem,
webServerSystem,
serverInfo,
localeSystem,
configSystem,
files,
versionChecker
);
}
}
}

View File

@ -46,7 +46,7 @@ import java.util.stream.Collectors;
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
*/
@Singleton
@Deprecated
@Deprecated(forRemoval = true, since = "5.0")
public class CommonAPI implements PlanAPI {
private final DBSystem dbSystem;
@ -107,7 +107,7 @@ public class CommonAPI implements PlanAPI {
@Override
public ServerContainer fetchServerContainer(UUID serverUUID) {
return new ServerContainer(new com.djrapitops.plan.delivery.domain.container.ServerContainer());
return new ServerContainer();
}
@Override

View File

@ -37,7 +37,7 @@ import java.util.UUID;
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
*/
@Deprecated
@Deprecated(since = "5.0")
public interface PlanAPI {
static PlanAPI getInstance() {

View File

@ -31,7 +31,7 @@ import java.util.Optional;
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
*/
@Deprecated
@Deprecated(since = "5.0")
public class PlayerContainer {
private final com.djrapitops.plan.delivery.domain.container.PlayerContainer container;

View File

@ -24,23 +24,18 @@ import java.util.Optional;
* Wrapper for a ServerContainer.
* <p>
* The actual object is wrapped to avoid exposing too much API that might change.
* See {@link com.djrapitops.plan.delivery.domain.keys.ServerKeys} for Key objects.
* <p>
* The Keys might change in the future, but the Optional API should help dealing with those cases.
*
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
*/
@Deprecated
@Deprecated(forRemoval = true, since = "5.0")
public class ServerContainer {
private final com.djrapitops.plan.delivery.domain.container.ServerContainer container;
public ServerContainer(com.djrapitops.plan.delivery.domain.container.ServerContainer container) {
this.container = container;
}
public ServerContainer() {/*Empty constructor, no-op derpecated api class*/}
public <T> Optional<T> getValue(Key<T> key) {
return container.getValue(key);
return Optional.empty();
}
}

View File

@ -332,12 +332,36 @@ public class PlanCommand {
.subcommand(clearCommand())
.subcommand(removeCommand())
.subcommand(uninstalledCommand())
.subcommand(removeJoinAddressesCommand())
.subcommand(onlineUuidMigration())
.requirePermission(Permissions.DATA_BASE)
.description(locale.getString(HelpLang.DB))
.inDepthDescription(locale.getString(DeepHelpLang.DB))
.build();
}
private Subcommand onlineUuidMigration() {
return Subcommand.builder()
.aliases("migrate_to_online_uuids", "migratetoonlineuuids")
.requirePermission(Permissions.DATA_CLEAR)
.optionalArgument("--remove_offline", "Remove offline players if given")
.description(locale.getString(HelpLang.ONLINE_UUID_MIGRATION))
.inDepthDescription("Moves and combines offline uuid data to online uuids where possible. Leaves offline-only players to database.")
.onCommand((sender, arguments) -> databaseCommands.onOnlineConversion(commandName, sender, arguments))
.build();
}
private Subcommand removeJoinAddressesCommand() {
return Subcommand.builder()
.aliases("remove_join_addresses", "removejoinaddresses")
.requirePermission(Permissions.DATA_CLEAR)
.requiredArgument(locale.getString(HelpLang.ARG_SERVER), locale.getString(HelpLang.DESC_ARG_SERVER_IDENTIFIER))
.description(locale.getString(HelpLang.JOIN_ADDRESS_REMOVAL))
.onCommand((sender, arguments) -> databaseCommands.onFixFabricJoinAddresses(commandName, sender, arguments))
.onTabComplete(this::serverNames)
.build();
}
private Subcommand backupCommand() {
return Subcommand.builder()
.aliases("backup")
@ -369,7 +393,7 @@ public class PlanCommand {
return DBType.names();
}
Optional<String> firstArgument = arguments.get(0);
if (!firstArgument.isPresent()) {
if (firstArgument.isEmpty()) {
return tabCompleteCache.getMatchingBackupFilenames(null);
}
String part = firstArgument.get();

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.queries.objects.UserIdentifierQueries;
@ -39,6 +40,7 @@ import java.util.stream.Collectors;
@Singleton
public class TabCompleteCache implements SubSystem {
private final Processing processing;
private final PlanFiles files;
private final DBSystem dbSystem;
@ -49,9 +51,11 @@ public class TabCompleteCache implements SubSystem {
@Inject
public TabCompleteCache(
Processing processing,
PlanFiles files,
DBSystem dbSystem
) {
this.processing = processing;
this.files = files;
this.dbSystem = dbSystem;
playerIdentifiers = new ArrayList<>();
@ -62,15 +66,17 @@ public class TabCompleteCache implements SubSystem {
@Override
public void enable() {
refreshPlayerIdentifiers();
refreshServerIdentifiers();
refreshUserIdentifiers();
refreshBackupFileNames();
processing.submitNonCritical(() -> {
refreshPlayerIdentifiers();
refreshServerIdentifiers();
refreshUserIdentifiers();
refreshBackupFileNames();
Collections.sort(playerIdentifiers);
Collections.sort(serverIdentifiers);
Collections.sort(userIdentifiers);
Collections.sort(backupFileNames);
Collections.sort(playerIdentifiers);
Collections.sort(serverIdentifiers);
Collections.sort(userIdentifiers);
Collections.sort(backupFileNames);
});
}
private void refreshServerIdentifiers() {
@ -109,22 +115,30 @@ public class TabCompleteCache implements SubSystem {
}
public List<String> getMatchingServerIdentifiers(String searchFor) {
if (searchFor == null || searchFor.isEmpty()) return serverIdentifiers;
if (searchFor == null || searchFor.isEmpty()) {
return serverIdentifiers.size() < 100 ? serverIdentifiers : Collections.emptyList();
}
return serverIdentifiers.stream().filter(identifier -> identifier.startsWith(searchFor)).collect(Collectors.toList());
}
public List<String> getMatchingPlayerIdentifiers(String searchFor) {
if (searchFor == null || searchFor.isEmpty()) return playerIdentifiers;
if (searchFor == null || searchFor.isEmpty()) {
return playerIdentifiers.size() < 100 ? playerIdentifiers : Collections.emptyList();
}
return playerIdentifiers.stream().filter(identifier -> identifier.startsWith(searchFor)).collect(Collectors.toList());
}
public List<String> getMatchingUserIdentifiers(String searchFor) {
if (searchFor == null || searchFor.isEmpty()) return userIdentifiers;
if (searchFor == null || searchFor.isEmpty()) {
return userIdentifiers.size() < 100 ? userIdentifiers : Collections.emptyList();
}
return userIdentifiers.stream().filter(identifier -> identifier.startsWith(searchFor)).collect(Collectors.toList());
}
public List<String> getMatchingBackupFilenames(String searchFor) {
if (searchFor == null || searchFor.isEmpty()) return backupFileNames;
if (searchFor == null || searchFor.isEmpty()) {
return backupFileNames.size() < 100 ? backupFileNames : Collections.emptyList();
}
return backupFileNames.stream().filter(identifier -> identifier.startsWith(searchFor)).collect(Collectors.toList());
}
}

View File

@ -39,7 +39,7 @@ public class Confirmation {
) {
this.locale = locale;
awaiting = Caffeine.newBuilder()
.expireAfterWrite(90, TimeUnit.SECONDS)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}

View File

@ -231,7 +231,7 @@ public class DataUtilityCommands {
}
PlayerContainer player = dbSystem.getDatabase().query(ContainerFetchQueries.fetchPlayerContainer(playerUUID));
if (!player.getValue(PlayerKeys.REGISTERED).isPresent()) {
if (player.getValue(PlayerKeys.REGISTERED).isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_PLAYER_NOT_FOUND_REGISTER, identifier));
}

View File

@ -19,12 +19,16 @@ package com.djrapitops.plan.commands.subcommands;
import com.djrapitops.plan.commands.use.Arguments;
import com.djrapitops.plan.commands.use.CMDSender;
import com.djrapitops.plan.commands.use.ColorScheme;
import com.djrapitops.plan.commands.use.MessageBuilder;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.gathering.domain.BaseUser;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.query.QuerySvc;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DatabaseSettings;
@ -35,21 +39,24 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.SQLiteDB;
import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.transactions.BackupCopyTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.RemovePlayerTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.SetServerAsUninstalledTransaction;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import com.djrapitops.plan.storage.database.transactions.commands.*;
import com.djrapitops.plan.storage.database.transactions.patches.BadFabricJoinAddressValuePatch;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.player.UUIDFetcher;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@Singleton
public class DatabaseCommands {
@ -66,8 +73,10 @@ public class DatabaseCommands {
private final Identifiers identifiers;
private final PluginStatusCommands statusCommands;
private final ErrorLogger errorLogger;
private final Processing processing;
private final Formatter<Long> timestamp;
private final Formatter<Long> clock;
@Inject
public DatabaseCommands(
@ -83,7 +92,8 @@ public class DatabaseCommands {
Formatters formatters,
Identifiers identifiers,
PluginStatusCommands statusCommands,
ErrorLogger errorLogger
ErrorLogger errorLogger,
Processing processing
) {
this.locale = locale;
this.confirmation = confirmation;
@ -99,6 +109,8 @@ public class DatabaseCommands {
this.errorLogger = errorLogger;
this.timestamp = formatters.iso8601NoClockLong();
clock = formatters.clockLong();
this.processing = processing;
}
public void onBackup(CMDSender sender, Arguments arguments) {
@ -323,6 +335,56 @@ public class DatabaseCommands {
}
}
public void onFixFabricJoinAddresses(String mainCommand, CMDSender sender, Arguments arguments) {
String identifier = arguments.concatenate(" ");
Optional<ServerUUID> serverUUID = identifiers.getServerUUID(identifier);
if (serverUUID.isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_SERVER_NOT_FOUND, identifier));
}
Database database = dbSystem.getDatabase();
if (sender.supportsChatEvents()) {
sender.buildMessage()
.addPart(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_JOIN_ADDRESS_REMOVAL, identifier, database.getType().getName())).newLine()
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM))
.addPart("§2§l[\u2714]").command("/" + mainCommand + " accept").hover(locale.getString(CommandLang.CONFIRM_ACCEPT))
.addPart(" ")
.addPart("§4§l[\u2718]").command("/" + mainCommand + " cancel").hover(locale.getString(CommandLang.CONFIRM_DENY))
.send();
} else {
sender.buildMessage()
.addPart(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_JOIN_ADDRESS_REMOVAL, identifier, database.getType().getName())).newLine()
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM)).addPart("§a/" + mainCommand + " accept")
.addPart(" ")
.addPart("§c/" + mainCommand + " cancel")
.send();
}
confirmation.confirm(sender, choice -> {
if (Boolean.TRUE.equals(choice)) {
performJoinAddressRemoval(sender, serverUUID.get(), database);
} else {
sender.send(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_CANCELLED_DATA));
}
});
}
private void performJoinAddressRemoval(CMDSender sender, ServerUUID serverUUID, Database database) {
try {
sender.send(locale.getString(CommandLang.DB_WRITE, database.getType().getName()));
database.executeTransaction(new BadFabricJoinAddressValuePatch(serverUUID))
.thenRunAsync(() -> sender.send(locale.getString(CommandLang.PROGRESS_SUCCESS)))
.exceptionally(error -> {
sender.send(locale.getString(CommandLang.PROGRESS_FAIL, error.getMessage()));
return null;
});
} catch (DBOpException e) {
sender.send(locale.getString(CommandLang.PROGRESS_FAIL, e.getMessage()));
errorLogger.error(e, ErrorContext.builder().related(sender, database.getType().getName()).build());
}
}
public void onRemove(String mainCommand, CMDSender sender, Arguments arguments) {
String identifier = arguments.concatenate(" ");
UUID playerUUID = identifiers.getPlayerUUID(identifier);
@ -387,7 +449,6 @@ public class DatabaseCommands {
String identifier = arguments.concatenate(" ");
Server server = dbSystem.getDatabase()
.query(ServerQueries.fetchServerMatchingIdentifier(identifier))
.filter(s -> !s.isProxy())
.orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_SERVER_NOT_FOUND, identifier)));
if (server.getUuid().equals(serverInfo.getServerUUID())) {
@ -420,4 +481,102 @@ public class DatabaseCommands {
}
statusCommands.onReload(sender);
}
public void onOnlineConversion(String mainCommand, CMDSender sender, Arguments arguments) {
boolean removeOfflinePlayers = arguments.get(0)
.map("--remove_offline"::equals)
.orElse(false);
sender.send(locale.getString(CommandLang.PROGRESS_PREPARING));
processing.submitNonCritical(() -> {
Map<UUID, BaseUser> baseUsersByUUID = dbSystem.getDatabase().query(BaseUserQueries.fetchAllBaseUsersByUUID());
List<String> playerNames = baseUsersByUUID.values().stream().map(BaseUser::getName).collect(Collectors.toList());
sender.send("Performing lookup for " + playerNames.size() + " uuids from Mojang..");
sender.send("Preparation estimated complete at: " + clock.apply(System.currentTimeMillis() + playerNames.size() * 100) + " (due to request rate limiting)");
Map<String, UUID> onlineUUIDsOfPlayers = getUUIDViaUUIDFetcher(playerNames);
if (onlineUUIDsOfPlayers.isEmpty()) {
sender.send(locale.getString(CommandLang.PROGRESS_FAIL, "Did not get any UUIDs from Mojang."));
return;
}
int totalProfiles = baseUsersByUUID.size();
int offlineOnlyUsers = 0;
int combine = 0;
int move = 0;
List<Transaction> transactions = new ArrayList<>();
for (BaseUser user : baseUsersByUUID.values()) {
String playerName = user.getName();
UUID recordedUUID = user.getUuid();
UUID actualUUID = onlineUUIDsOfPlayers.get(playerName);
if (actualUUID == null) {
offlineOnlyUsers++;
if (removeOfflinePlayers) transactions.add(new RemovePlayerTransaction(recordedUUID));
continue;
}
if (recordedUUID == actualUUID) {
continue;
}
BaseUser alreadyExistingProfile = baseUsersByUUID.get(actualUUID);
if (alreadyExistingProfile == null) {
move++;
transactions.add(new ChangeUserUUIDTransaction(recordedUUID, actualUUID));
} else {
combine++;
transactions.add(new CombineUserTransaction(recordedUUID, actualUUID));
}
}
MessageBuilder messageBuilder = sender.buildMessage()
.addPart(colors.getMainColor() + "Moving to online-only UUIDs (irreversible):").newLine()
.addPart(colors.getSecondaryColor() + " Total players in database: " + totalProfiles).newLine()
.addPart(colors.getSecondaryColor() + (removeOfflinePlayers ? "Removing (no online UUID): " : " Offline only (no online UUID): ") + offlineOnlyUsers).newLine()
.addPart(colors.getSecondaryColor() + " Moving to new UUID: " + move).newLine()
.addPart(colors.getSecondaryColor() + " Combining offline and online profiles: " + combine).newLine()
.newLine()
.addPart(colors.getSecondaryColor() + " Estimated online UUID players in database after: " + (totalProfiles - combine - offlineOnlyUsers) + (removeOfflinePlayers ? "" : " (+" + offlineOnlyUsers + " offline)")).newLine()
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM));
if (sender.supportsChatEvents()) {
messageBuilder
.addPart("§2§l[\u2714]").command("/" + mainCommand + " accept").hover(locale.getString(CommandLang.CONFIRM_ACCEPT))
.addPart(" ")
.addPart("§4§l[\u2718]").command("/" + mainCommand + " cancel").hover(locale.getString(CommandLang.CONFIRM_DENY))
.send();
} else {
messageBuilder
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM)).addPart("§a/" + mainCommand + " accept")
.addPart(" ")
.addPart("§c/" + mainCommand + " cancel")
.send();
}
confirmation.confirm(sender, choice -> {
if (Boolean.TRUE.equals(choice)) {
transactions.forEach(dbSystem.getDatabase()::executeTransaction);
dbSystem.getDatabase().executeTransaction(new Transaction() {
@Override
protected void performOperations() {
sender.send(locale.getString(CommandLang.PROGRESS_SUCCESS));
}
});
} else {
sender.send(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_CANCELLED_DATA));
}
});
});
}
private Map<String, UUID> getUUIDViaUUIDFetcher(List<String> playerNames) {
try {
return new UUIDFetcher(playerNames).call();
} catch (Exception | NoClassDefFoundError failure) {
errorLogger.error(failure, ErrorContext.builder()
.related("Migrating offline uuids to online uuids")
.build());
return new HashMap<>();
}
}
}

View File

@ -125,7 +125,7 @@ public class LinkCommands {
String serversListed = dbSystem.getDatabase()
.query(ServerQueries.fetchPlanServerInformationCollection())
.stream().sorted()
.map(server -> m + server.getId().orElse(0) + "::" + t + server.getName() + "::" + s + server.getUuid() + "\n")
.map(server -> m + server.getId().orElse(0) + "::" + t + server.getName() + "::" + s + server.getUuid() + "::" + s + server.getPlanVersion() + "\n")
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
sender.buildMessage()
@ -194,7 +194,7 @@ public class LinkCommands {
.addPart(colors.getMainColor() + locale.getString(CommandLang.LINK_NETWORK))
.apply(builder -> linkTo(builder, sender, address))
.send();
if (!dbSystem.getDatabase().query(ServerQueries.fetchProxyServerInformation()).isPresent()) {
if (dbSystem.getDatabase().query(ServerQueries.fetchProxyServerInformation()).isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.NOTIFY_NO_NETWORK));
}
}

View File

@ -31,8 +31,8 @@ import com.djrapitops.plan.settings.locale.lang.HelpLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.storage.database.transactions.commands.RegisterWebUserTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.RemoveWebUserTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
@ -158,7 +158,7 @@ public class RegistrationCommands {
boolean userExists = database.query(WebUserQueries.fetchUser(username)).isPresent();
if (userExists) throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_WEB_USER_EXISTS));
database.executeTransaction(new RegisterWebUserTransaction(user))
database.executeTransaction(new StoreWebUserTransaction(user))
.get(); // Wait for completion
sender.send(locale.getString(CommandLang.WEB_USER_REGISTER_SUCCESS, username));
@ -177,31 +177,26 @@ public class RegistrationCommands {
UUID playerUUID = sender.getUUID().orElse(null);
String username;
if (!givenUsername.isPresent() && playerUUID != null) {
Optional<User> found = database.query(WebUserQueries.fetchUser(playerUUID));
if (!found.isPresent()) {
throw new IllegalArgumentException(locale.getString(CommandLang.USER_NOT_LINKED));
}
username = found.get().getUsername();
} else if (!givenUsername.isPresent()) {
if (givenUsername.isEmpty() && playerUUID != null) {
username = database.query(WebUserQueries.fetchUser(playerUUID))
.map(User::getUsername)
.orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.USER_NOT_LINKED)));
} else if (givenUsername.isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ONE_ARG, "<" + locale.getString(HelpLang.ARG_USERNAME) + ">"));
} else {
username = givenUsername.get();
}
Optional<User> found = database.query(WebUserQueries.fetchUser(username));
if (!found.isPresent()) {
throw new IllegalArgumentException(locale.getString(FailReason.USER_DOES_NOT_EXIST));
}
User presentUser = found.get();
boolean ownsTheUser = Objects.equals(playerUUID, presentUser.getLinkedToUUID());
User user = database.query(WebUserQueries.fetchUser(username))
.orElseThrow(() -> new IllegalArgumentException(locale.getString(FailReason.USER_DOES_NOT_EXIST)));
boolean ownsTheUser = Objects.equals(playerUUID, user.getLinkedToUUID());
if (!(ownsTheUser || sender.hasPermission(Permissions.UNREGISTER_OTHER.getPerm()))) {
throw new IllegalArgumentException(locale.getString(CommandLang.USER_NOT_LINKED));
}
if (sender.supportsChatEvents()) {
sender.buildMessage()
.addPart(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_UNREGISTER, presentUser.getUsername(), presentUser.getLinkedTo())).newLine()
.addPart(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_UNREGISTER, user.getUsername(), user.getLinkedTo())).newLine()
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM))
.addPart("§2§l[\u2714]").command("/" + mainCommand + " accept").hover(locale.getString(CommandLang.CONFIRM_ACCEPT))
.addPart(" ")
@ -209,7 +204,7 @@ public class RegistrationCommands {
.send();
} else {
sender.buildMessage()
.addPart(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_UNREGISTER, presentUser.getUsername(), presentUser.getLinkedTo())).newLine()
.addPart(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_UNREGISTER, user.getUsername(), user.getLinkedTo())).newLine()
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM)).addPart("§a/" + mainCommand + " accept")
.addPart(" ")
.addPart("§c/" + mainCommand + " cancel")
@ -219,7 +214,7 @@ public class RegistrationCommands {
confirmation.confirm(sender, choice -> {
if (Boolean.TRUE.equals(choice)) {
try {
sender.send(colors.getMainColor() + locale.getString(CommandLang.UNREGISTER, presentUser.getUsername()));
sender.send(colors.getMainColor() + locale.getString(CommandLang.UNREGISTER, user.getUsername()));
database.executeTransaction(new RemoveWebUserTransaction(username))
.get(); // Wait for completion
ActiveCookieStore.removeUserCookie(username);
@ -230,19 +225,15 @@ public class RegistrationCommands {
errorLogger.warn(e, ErrorContext.builder().related("unregister command", sender, sender.getPlayerName().orElse("console"), arguments).build());
}
} else {
sender.send(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_CANCELLED_UNREGISTER, presentUser.getUsername()));
sender.send(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_CANCELLED_UNREGISTER, user.getUsername()));
}
});
}
public void onLogoutCommand(CMDSender sender, Arguments arguments) {
Optional<String> username = arguments.get(0);
if (!username.isPresent()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ONE_ARG, locale.getString(HelpLang.ARG_USERNAME) + "/*"));
}
String loggingOut = username.get();
String loggingOut = arguments.get(0)
.orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ONE_ARG, locale.getString(HelpLang.ARG_USERNAME) + "/*")));
if ("*".equals(loggingOut)) {
activeCookieStore.removeAll();

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.commands.use;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
@ -24,16 +25,21 @@ import org.apache.commons.text.TextStringBuilder;
import java.util.Collection;
public class VelocityMessageBuilder implements MessageBuilder {
/**
* Shared Adventure {@link MessageBuilder} shared by Velocity and Sponge.
*/
public class AdventureMessageBuilder implements MessageBuilder {
private final VelocityCMDSender sender;
private final CMDSender sender;
private final Audience audience;
private final TextComponent.Builder builder;
// Store reference to previous component to properly add hover & click events
private Component previousComponent;
public VelocityMessageBuilder(VelocityCMDSender sender) {
AdventureMessageBuilder(CMDSender sender, Audience audience) {
this.sender = sender;
this.audience = audience;
builder = Component.text();
}
@ -112,6 +118,6 @@ public class VelocityMessageBuilder implements MessageBuilder {
if (previousComponent != null) {
builder.append(previousComponent);
}
sender.commandSource.sendMessage(builder.build());
audience.sendMessage(builder.build());
}
}

View File

@ -22,11 +22,11 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
public abstract class ChatFormatter {
public interface ChatFormatter {
public abstract int getWidth(String part);
int getWidth(String part);
public String table(String message, String separator) {
default String table(String message, String separator) {
String[] lines = StringUtils.split(message, '\n');
List<String[]> rows = new ArrayList<>();
Maximum.ForInteger rowWidth = new Maximum.ForInteger(0);
@ -54,7 +54,7 @@ public abstract class ChatFormatter {
return table.toString();
}
public List<String[]> tableAsParts(String message, String separator) {
default List<String[]> tableAsParts(String message, String separator) {
String[] lines = StringUtils.split(message, '\n');
List<String[]> rows = new ArrayList<>();
Maximum.ForInteger rowWidth = new Maximum.ForInteger(0);

View File

@ -50,6 +50,21 @@ public class CommandWithSubcommands extends Subcommand {
return subcommands.stream().filter(sender::hasAllPermissionsFor).collect(Collectors.toList());
}
public List<Subcommand> getSubcommands() {
return subcommands;
}
public Optional<Subcommand> findSubCommand(Arguments arguments) {
return arguments.get(0).flatMap(alias -> {
for (Subcommand subcommand : subcommands) {
if (subcommand.getAliases().contains(alias)) {
return Optional.of(subcommand);
}
}
return Optional.empty();
});
}
public void onHelp(CMDSender sender, Arguments arguments) {
List<Subcommand> hasPermissionFor = getPermittedSubcommands(sender);
sender.buildMessage()

View File

@ -18,7 +18,7 @@ package com.djrapitops.plan.commands.use;
import org.apache.commons.lang3.StringUtils;
public class ConsoleChatFormatter extends ChatFormatter {
public class ConsoleChatFormatter implements ChatFormatter {
@Override
public int getWidth(String part) {

View File

@ -0,0 +1,83 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.commands.use;
import java.util.Collection;
import java.util.function.Consumer;
public class ConsoleMessageBuilder implements MessageBuilder {
private final StringBuilder stringBuilder;
private final Consumer<String> messageBus;
public ConsoleMessageBuilder(Consumer<String> messageBus) {
this.messageBus = messageBus;
this.stringBuilder = new StringBuilder();
}
@Override
public MessageBuilder addPart(String msg) {
stringBuilder.append(msg);
return this;
}
@Override
public MessageBuilder newLine() {
return addPart("\n");
}
@Override
public MessageBuilder link(String address) {
return addPart(address);
}
@Override
public MessageBuilder command(String command) {
return this;
}
@Override
public MessageBuilder hover(String text) {
return this;
}
@Override
public MessageBuilder hover(String... text) {
return this;
}
@Override
public MessageBuilder hover(Collection<String> text) {
return this;
}
@Override
public MessageBuilder indent(int spaces) {
stringBuilder.append(" ".repeat(Math.max(0, spaces)));
return this;
}
@Override
public MessageBuilder tabular(CharSequence columnSeparator) {
return this;
}
@Override
public void send() {
messageBus.accept(stringBuilder.toString());
}
}

View File

@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.commands.use;
public class PlayerChatFormatter extends ChatFormatter {
public class PlayerChatFormatter implements ChatFormatter {
@Override
public int getWidth(String part) {

View File

@ -29,7 +29,7 @@ import java.util.UUID;
* @see InspectContainer
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
*/
@Deprecated
@Deprecated(since = "5.0")
public final class AnalysisContainer extends InspectContainer {
private final Map<String, Map<UUID, ? extends Serializable>> playerTableValues;

View File

@ -29,7 +29,7 @@ import java.util.TreeMap;
* @see TableContainer
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
*/
@Deprecated
@Deprecated(since = "5.0")
public class InspectContainer {
protected final List<String> values;

View File

@ -28,7 +28,7 @@ import java.util.List;
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
*/
@Deprecated
@Deprecated(since = "5.0")
public class TableContainer {
protected final String[] header;

View File

@ -25,7 +25,7 @@ import java.util.UUID;
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
*/
@Deprecated
@Deprecated(since = "5.0")
public interface BanData {
boolean isBanned(UUID uuid);

View File

@ -35,7 +35,7 @@ import java.util.UUID;
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
*/
@Deprecated
@Deprecated(since = "5.0")
public abstract class PluginData {
private final ContainerSize size;

View File

@ -24,6 +24,6 @@ import com.djrapitops.plan.delivery.domain.container.DynamicDataContainer;
* @author AuroraLS3
* @deprecated AnalysisContainer is no longer used.
*/
@Deprecated
@Deprecated(since = "5.0")
public class AnalysisContainer extends DynamicDataContainer {
}

View File

@ -0,0 +1,72 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Objects;
/**
* Represents a single join address - number pair.
*
* @author AuroraLS3
*/
public class JoinAddressCount implements Comparable<JoinAddressCount> {
private final int count;
private String joinAddress;
public JoinAddressCount(Map.Entry<String, Integer> entry) {
this(entry.getKey(), entry.getValue());
}
public JoinAddressCount(String joinAddress, int count) {
this.joinAddress = joinAddress;
this.count = count;
}
public String getJoinAddress() {
return joinAddress;
}
public void setJoinAddress(String joinAddress) {
this.joinAddress = joinAddress;
}
public int getCount() {
return count;
}
@Override
public int compareTo(@NotNull JoinAddressCount other) {
return String.CASE_INSENSITIVE_ORDER.compare(this.joinAddress, other.joinAddress);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JoinAddressCount that = (JoinAddressCount) o;
return getCount() == that.getCount() && Objects.equals(getJoinAddress(), that.getJoinAddress());
}
@Override
public int hashCode() {
return Objects.hash(getJoinAddress(), getCount());
}
}

Some files were not shown because too many files have changed in this diff Show More