| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |

This tutorial attempts to guide you through using Query API in your plugin, for more in-depth documentation about different parts of the API, see Query API.
These icons are used to aid understanding
💭 Question about possible issues (Someone has had these before)
💡 Extra stuff
Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have
💭 What is this API for?
Query API is for accessing the Plan database from within your plugin. This can be used to store data in the database, or to write custom queries against the database.
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>maven {
url "https://jitpack.io"
}<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'softdepend:
- Plan
# nukkit
softdepend: ["Plan"]
# bungee
softDepends:
- Plan@Plugin(
id = ...,
dependencies = {
@Dependency(id = "plan", optional = true)
}
)✔️ Your project now includes Plan API as a dependency!
In order to keep Plan as an optional dependency, all access to the Plan API should be made from a separate class. In this tutorial this will be called PlanHook, but you can call it whatever you want.
In this case we're creating QueryAPIAccessor in order to write all queries in a separate class from PlanHook.
Let's take a look at this example class:
import com.djrapitops.plan.capability.CapabilityService;
import com.djrapitops.plan.query.QueryService;
public class PlanHook {
public PlanHook() {
}
public Optional<QueryAPIAccessor> hookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return Optional.empty();
return Optional.ofNullable(createQueryAPIAccessor());
}
private boolean areAllCapabilitiesAvailable() {
CapabilityService capabilities = CapabilityService.getInstance();
return capabilities.hasCapability("QUERY_API");
}
private QueryAPIAccessor createQueryAPIAccessor() {
try {
return new QueryAPIAccessor(QueryService.getInstance());
} catch (IllegalStateException planIsNotEnabled) {
// Plan is not enabled, handle exception
return null;
}
}
}Creating a separate class is necessary to keep NoClassDefFoundError away from loading your plugin when Plan is not enabled!
Here is some more explanation for each section of the code in case you need more information.
hookIntoPlan() public Optional<QueryAPIAccessor> hookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return Optional.empty();
return Optional.ofNullable(createQueryAPIAccessor());
} private boolean areAllCapabilitiesAvailable() {
CapabilityService capabilities = CapabilityService.getInstance();
return capabilities.hasCapability("QUERY_API");
} private QueryAPIAccessor createQueryAPIAccessor() {
try {
return new QueryAPIAccessor(QueryService.getInstance());
} catch (IllegalStateException planIsNotEnabled) {
// Plan is not enabled, handle exception
return null;
}
}In this example the Spigot JavaPlugin#onEnable is used, but you can add these methods to wherever you wish, as long as it is called after Plan has been loaded & enabled.
💭 When does Plan enable?
- Spigot & Nukkit: After dependencies have enabled & worlds have been loaded
- Sponge: After dependencies on GameStartedServerEvent
- BungeeCord: After dependencies
- Velocity: After dependencies on ProxyInitializeEvent
In the next step: Creating QueryAPIAccessor
public void onEnable() {
... // The example plugin enables itself
try {
Optional<QueryAPIAccessor> = new PlanHook().hookIntoPlan();
} catch (NoClassDefFoundError planIsNotInstalled) {
// Plan is not installed
}
}✔️ You can now access Plan API from somewhere!
In order to keep code maintainable, a second class called QueryAPIAccessor is created. This is then used to access Plan API's QueryService.
In this example data is stored in a new table inside the Plan database. The example is from ViaVersion Extension
Let's take a look at the class:
import com.djrapitops.plan.query.QueryService;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class QueryAPIAccessor {
private final QueryService queryService;
public QueryAPIAccessor(QueryService queryService) {
this.queryService = queryService;
createTable();
queryService.subscribeDataClearEvent(this::recreateTable);
queryService.subscribeToPlayerRemoveEvent(this::removePlayer);
}
private void createTable() {
String dbType = queryService.getDBType();
boolean sqlite = dbType.equalsIgnoreCase("SQLITE");
String sql = "CREATE TABLE IF NOT EXISTS plan_version_protocol (" +
"id int " + (sqlite ? "PRIMARY KEY" : "NOT NULL AUTO_INCREMENT") + ',' +
"uuid varchar(36) NOT NULL UNIQUE," +
"protocol_version int NOT NULL" +
(sqlite ? "" : ",PRIMARY KEY (id)") +
')';
queryService.execute(sql, PreparedStatement::execute);
}
private void dropTable() {
queryService.execute("DROP TABLE IF EXISTS plan_version_protocol", PreparedStatement::execute);
}
private void recreateTable() {
dropTable();
createTable();
}
private void removePlayer(UUID playerUUID) {
queryService.execute(
"DELETE FROM plan_version_protocol WHERE uuid=?",
statement -> {
statement.setString(1, playerUUID.toString());
statement.execute();
}
);
}
public void storeProtocolVersion(UUID uuid, int version) throws ExecutionException {
String update = "UPDATE plan_version_protocol SET protocol_version=? WHERE uuid=?";
String insert = "INSERT INTO plan_version_protocol (protocol_version, uuid) VALUES (?, ?)";
AtomicBoolean updated = new AtomicBoolean(false);
try {
queryService.execute(update, statement -> {
statement.setInt(1, version);
statement.setString(2, uuid.toString());
updated.set(statement.executeUpdate() > 0);
}).get(); // Wait
if (!updated.get()) {
queryService.execute(insert, statement -> {
statement.setInt(1, version);
statement.setString(2, uuid.toString());
statement.execute();
});
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public int getProtocolVersion(UUID uuid) {
String sql = "SELECT protocol_version FROM plan_version_protocol WHERE uuid=?";
return queryService.query(sql, statement -> {
statement.setString(1, uuid.toString());
try (ResultSet set = statement.executeQuery()) {
return set.next() ? set.getInt("protocol_version") : -1;
}
});
}
public Map<Integer, Integer> getProtocolVersionCounts() {
UUID serverUUID = queryService.getServerUUID()
.orElseThrow(NotReadyException::new);
final String sql = "SELECT protocol_version, COUNT(1) as count" +
" FROM plan_version_protocol" +
" INNER JOIN plan_user_info on plan_version_protocol.uuid=plan_user_info.uuid" +
" WHERE plan_user_info.server_uuid=?" +
" GROUP BY protocol_version";
return queryService.query(sql, statement -> {
statement.setString(1, serverUUID.toString());
try (ResultSet set = statement.executeQuery()) {
Map<Integer, Integer> versions = new HashMap<>();
while (set.next()) {
versions.put(set.getInt("protocol_version"), set.getInt("count"));
}
return versions;
}
});
}
}More information about each method
Construction private final QueryService queryService;
public QueryAPIAccessor(QueryService queryService) {
this.queryService = queryService;
createTable();
queryService.subscribeDataClearEvent(this::recreateTable);
queryService.subscribeToPlayerRemoveEvent(this::removePlayer);
} private void createTable() {
String dbType = queryService.getDBType();
boolean sqlite = dbType.equalsIgnoreCase("SQLITE");
String sql = "CREATE TABLE IF NOT EXISTS plan_version_protocol (" +
"id int " + (sqlite ? "PRIMARY KEY" : "NOT NULL AUTO_INCREMENT") + ',' +
"uuid varchar(36) NOT NULL UNIQUE," +
"protocol_version int NOT NULL" +
(sqlite ? "" : ",PRIMARY KEY (id)") +
')';
queryService.execute(sql, PreparedStatement::execute);
} private void dropTable() {
queryService.execute("DROP TABLE IF EXISTS plan_version_protocol", PreparedStatement::execute);
} private void recreateTable() {
dropTable();
createTable();
} private void removePlayer(UUID playerUUID) {
queryService.execute(
"DELETE FROM plan_version_protocol WHERE uuid=?",
statement -> {
statement.setString(1, playerUUID.toString());
statement.execute();
}
);
} public void storeProtocolVersion(UUID uuid, int version) throws ExecutionException {
String update = "UPDATE plan_version_protocol SET protocol_version=? WHERE uuid=?";
String insert = "INSERT INTO plan_version_protocol (protocol_version, uuid) VALUES (?, ?)";
AtomicBoolean updated = new AtomicBoolean(false);
try {
queryService.execute(update, statement -> {
statement.setInt(1, version);
statement.setString(2, uuid.toString());
updated.set(statement.executeUpdate() > 0);
}).get(); // Wait
if (!updated.get()) {
queryService.execute(insert, statement -> {
statement.setInt(1, version);
statement.setString(2, uuid.toString());
statement.execute();
});
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}getProtocolVersion💡 Batch execution
It is possible to execute batches with PreparedStatements. Set the parameters inside a for-loop, call PreparedStatement#addBatch and then call PreparedStatement#executeBatch at the end of the for-loop
public int getProtocolVersion(UUID uuid) {
String sql = "SELECT protocol_version FROM plan_version_protocol WHERE uuid=?";
return queryService.query(sql, statement -> {
statement.setString(1, uuid.toString());
try (ResultSet set = statement.executeQuery()) {
return set.next() ? set.getInt("protocol_version") : -1;
}
});
} public Map<Integer, Integer> getProtocolVersionCounts() {
UUID serverUUID = queryService.getServerUUID()
.orElseThrow(NotReadyException::new);
final String sql = "SELECT protocol_version, COUNT(1) as count" +
" FROM plan_version_protocol" +
" INNER JOIN plan_user_info on plan_version_protocol.uuid=plan_user_info.uuid" +
" WHERE plan_user_info.server_uuid=?" +
" GROUP BY protocol_version";
return queryService.query(sql, statement -> {
statement.setString(1, serverUUID.toString());
try (ResultSet set = statement.executeQuery()) {
Map<Integer, Integer> versions = new HashMap<>();
while (set.next()) {
versions.put(set.getInt("protocol_version"), set.getInt("count"));
}
return versions;
}
});
}✔️ You can now use Plan API to store and query your own data
This goal is for a different kind of use of Query API, so we'll create another version of QueryAPIAccessor class.
Let's take a look:
import com.djrapitops.plan.query.QueryService;
import com.djrapitops.plan.query.CommonQueries;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class QueryAPIAccessor {
private final QueryService queryService;
public QueryAPIAccessor(QueryService queryService) {
this.queryService = queryService;
ensureDBSchemaMatch();
}
private void ensureDBSchemaMatch() {
CommonQueries queries = queryService.getCommonQueries();
if (
!queries.doesDBHaveTable("plan_sessions")
|| !queries.doesDBHaveTableColumn("plan_sessions", "uuid")
) {
throw new IllegalStateException("Different table schema");
}
}
public long getPlaytimeLast30d(UUID playerUUID) {
long now = System.currentTimeMillis();
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
UUID serverUUID = queryService.getServerUUID()
.orElseThrow(IllegalStateException::new);
return queryService.getCommonQueries().fetchPlaytime(
playerUUID, serverUUID, monthAgo, now
);
}
public long getPlaytimeLast30dOnAllServers(UUID playerUUID) {
long now = System.currentTimeMillis();
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
Set<UUID> serverUUIDs = queryService.getCommonQueries()
.fetchServerUUIDs();
long playtime = 0;
for (UUID serverUUID : serverUUIDs) {
playtime += queryService.getCommonQueries().fetchPlaytime(
playerUUID, serverUUID, monthAgo, now
);
}
return playtime;
}
public long getSessionCount(UUID playerUUID) {
UUID serverUUID = queryService.getServerUUID()
.orElseThrow(IllegalStateException::new);
String sql = "SELECT COUNT(1) as session_count FROM plan_sessions WHERE uuid=?";
return queryService.query(sql, statement -> {
statement.setString(1, playerUUID.toString());
try (ResultSet set = statement.executeQuery()) {
return set.next() ? set.getLong("session_count") : -1L;
}
});
}✔️ You can now use Plan API to query your Plan data
For more in-depth details about Query API, see Query API documentation
| Back | FazBrowse Home | New Git URL |