DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

DataExtension API Getting started

Aurora Lahtela edited this page Jan 3, 2026 · 17 revisions

Plan Header

DataExtension API - Getting started

This tutorial attempts to guide you through adding a DataExtension to your plugin, for more in-depth documentation about different parts of the API, see DataExtension API.

These icons are used to aid understanding

💭 Question about possible issues (Someone has had these before)
💡 Extra stuff

✔️ Requirements

  • A java plugin project for a minecraft server

🚩 Tutorial Goals

Here are the goals the tutorial aims to guide you through.
At the end of this tutorial you will have

  • .. Added Plan API as a dependency to your project
    • (.. added Plan as soft-dependency to your plugin)
  • .. Created 2 new classes to use the API
  • .. Created 1 new test class to test use of the API
  • .. Defined a DataExtension that adds a number to a player's page

💭 What is this API for?

DataExtension API is for displaying data from a plugin on the Plan website.


Goal #1: Adding Plan API as a dependency

1.1: Add Plan repository to your project

Maven

  • Add the repository to your <repositories>-block in pom.xml of your project
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>

Gradle

  • Add the repository to your repositories-block in build.gradle of your project
maven {
url "https://jitpack.io" }

Other build tools

1.2: Add Plan API as a dependency

Maven

  • Add Plan API as a dependency to your <dependencies>-block in in pom.xml of your project
<dependency>
<groupId>com.github.plan-player-analytics</groupId>
<artifactId>Plan</artifactId>
<version>{jitpack version}</version> <!-- Add the version number here -->
<scope>provided</scope>
</dependency>

Gradle

  • Add Plan API as a compile & test compile time dependency to your dependencies-block in build.gradle of your project.
compileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'
testCompileOnly 'com.github.plan-player-analytics:Plan:{jitpack version}'

Other

1.3: Add Plan as a soft-dependency in your plugin

Spigot, Nukkit & Bungeecord (plugin.yml)

  • Add Plan in softdepend in plugin.yml of your project
softdepend:
- Plan# nukkitsoftdepend: ["Plan"]# bungeesoftDepends:
- Plan

Sponge & Velocity (Plugin annotation)

  • Add Plan as an optional dependency to the @Plugin annotation
@Plugin(
id = ...,
dependencies = {
@Dependency(id ="plan", optional =true)
}
)

✔️ Your project now includes Plan API as a dependency!

Goal #2: Access Plan API from your plugin

2.1: Create a class to separate Plan imports from your main class

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.

Let's take a look at this example class:

importcom.djrapitops.plan.capability.CapabilityService;
importcom.djrapitops.plan.extension.ExtensionService;
publicclassPlanHook {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
);
}
}

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.

Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicPlanHook(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display later.
hookIntoPlan()
publicvoidhookIntoPlan() {
if (!areAllCapabilitiesAvailable()) return;
registerDataExtension();
listenForPlanReloads();
}
  • This method checks if Plan has the capabilities you need, the check is similar to how some plugins ask you to check the version number.
  • If the capabilities are available, the extension is registered.
  • Additionally a listener for Plan reloads is registered to re-register the data extension.
areAllCapabilitiesAvailable()
privatebooleanareAllCapabilitiesAvailable() {
CapabilityServicecapabilities = CapabilityService.getInstance();
returncapabilities.hasCapability("DATA_EXTENSION_VALUES");
}
  • Checks that DATA_EXTENSION_VALUES capability is available. Some features might need more capabilities, and when they do it is mentioned in the documentation. Those capabilities can then be added here.
registerDataExtension()
privatevoidregisterDataExtension() {
try {
ExtensionService.getInstance().register(newMyPluginDataExtension(database));
} catch (IllegalStateExceptionplanIsNotEnabled) {
// Plan is not enabled, handle exception
} catch (IllegalArgumentExceptiondataExtensionImplementationIsInvalid) {
// The DataExtension implementation has an implementation error, handle exception
}
}
  • Creates MyPluginDataExtension with the database of the plugin, and registers it to Plan
  • IllegalStateException might be thrown if Plan has not enabled properly
  • IllegalArgumentException might be thrown if the MyPluginDataExtension has an implementation error. Later a test class is created to avoid this issue.
listenForPlanReloads()
privatevoidlistenForPlanReloads() {
CapabilityService.getInstance().registerEnableListener(
isPlanEnabled -> {
// Register DataExtension againif (isPlanEnabled) registerDataExtension();
}
)
}
  • Registers a listener to Plan that registers the extension again in case Plan is reloaded.

2.2: Construct and call the PlanHook in your plugin enable.

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
MyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicvoidonEnable() {
... // The example plugin enables itselftry {
newPlanHook(database).hookIntoPlan();
} catch (NoClassDefFoundErrorplanIsNotInstalled) {
// Plan is not installed
}
}

✔️ You can now access Plan API from somewhere!

Goal #3: Creating a DataExtension

You might have noticed that there was a MyPluginDataExtension in the previous section, this is what is going to be implemented next.

3.1: Create your DataExtension class

Let's look at this example class:

@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
} 

Individual sections explained:

PluginInfo annotation
@PluginInfo(
name = "MyPlugin", iconName = "vial", iconFamily = Family.SOLID, color = Color.LIGHT_BLUE
)
publicclassMyPluginDataExtensionimplementsDataExtension {...}
  • PluginInfo annotation provides meta-data to Plan on how to display your plugin on the page.
  • It is a class annotation, the class needs to implement DataExtension
  • Documentation (middle-click to open to new tab)
Construction
privatefinalMyPluginDatabasedatabase; // Replace with whatever you use to store your plugin's datapublicMyPluginDataExtension(MyPluginDatabasedatabase) {
this.database = database;
}
  • MyPluginDatabase is the database of the example plugin, and is given so that the DataExtension has some data to display.
Overriding callExtensionMethodsOn
@OverridepublicCallEvents[] callExtensionMethodsOn() {
returnnewCallEvents[]{
CallEvents.PLAYER_JOIN,
CallEvents.PLAYER_LEAVE
};
}
NumberProvider and challengesCompleted method
@NumberProvider(
text = "Completed Challenges",
description = "How many challenges has the player completed",
iconName = "bookmark",
iconColor = Color.GREEN,
priority = 100,
showInPlayerTable = true
)
publiclongchallengesCompleted(UUIDplayerUUID) {
returndatabase.getCompletedChallengeCount(playerUUID);
}
  • Provider annotations give meta-data to Plan on how to display the data you are about to give, such as name, description, where to show it on the page etc.
  • Each provider annotation needs specific return type, NumberProvider requires long to be returned.
  • The method parameters determine if the method is a "Player method" or "Server method", in this case UUID -> This is a player method
  • Documentation for NumberProvider (middle-click to open to new tab)
  • Documentation for all Provider annotations (middle-click to open to new tab)

✔️ You now have a DataExtension!

Goal #4: Test your DataExtension implementation

Place a test method to a test class.

@TestpublicvoidnoImplementationErrors() {
DataExtensionyourExtension = newMyPluginExtension();
// Throws IllegalArgumentException if there is an implementation error or warning.newExtensionExtractor(yourExtension).validateAnnotations();
}

✔️ Less errors at runtime!


There is still a lot of additional annotations to further control the meta-data and for different kinds of data. I'll list some here for you to check out from the DataExtension API documentation

  • Providers
    • BooleanProvider: Yes/No answers and possibility for conditional data
    • NumberProvider: Numbers, time amounts and dates.
    • DoubleProvider: Floating point numbers
    • PercentageProvider: Percentages between 0% and 100%
    • StringProvider: Strings and player names that link to the player's page
    • GroupProvider: Names of different groups the player belongs in, eg. Permission groups
    • TableProvider: Data for displaying a table
  • Extra annotations
    • Tab, TabInfo and TabOrder: Place provided data to different sections on the page (For cases like where your plugin provides lots of different kinds of data, such as an Essentials or punishment plugin)
    • Conditional: Controls conditional execution of the provider method
    • InvalidateMethod: If you remove a extension method later this annotation is used to remove the old data

Clone this wiki locally