# Plugin

Plugins werden als Unterprojekte angelegt. In einem Git-Repo kann es also problemlos mehrere Plugins geben. Inwiefern das organisatorisch sinnvoll ist, muss individuell geklärt werden. Sachen wie Projektzugehörigkeit, Issue-Management und Abhängigkeiten (Dependencies) spielen dabei eine Rolle.

Die Programmiersprache, mit der Plugins entwickelt werden, ist frei wählbar. Sie muss allerdings JVM-kompatibel sein. Es wären also z. B. Java, Groovy, Scala oder Kotlin möglich. Diese Doku beschränkt sich auf Kotlin, das ist auch unsere Empfehlung.

Beim Builden wird das Plugin in ein ZIP-Archiv verpackt, das nebst dem Code und den Dependencies auch Metadaten zum Plugin wie Titel und Version enthält.

## Struktur

Eine Konvention, die bei uns oft verwendet wird, fasst Plugin-Unterprojekte im Verzeichnis `plugins` zusammen. Wenn das Git-Repo wächst, hat das den Vorteil, dass man nicht alle Verzeichnisse abklappern muss, um alle Plugins zu finden. Außerdem könnten so auch allgemeine Plugin-Tasks im Unterprojekt `:plugins` angelegt werden.

Ein Plugin besteht im Minimum aus einer kleinen YAML-Datei mit Metainformationen sowie der Plugin-Hauptklasse. Die Metainformationen, der eigene Code, sowie alle Dependencies (externe .jar Dateien) werden durch den `build` oder `packageBatixPlugin` Task in eine .zip Datei geschrieben, die dann im Framework hochgeladen werden kann.

Um beim Beispiel des Import-Plugins zu bleiben, wird also die Unterordner-Struktur `plugins/mitarbeiter-import` angelegt. Dort werden dann zwei neue Dateien angelegt: `build.gradle.kts` und `plugin.yaml`.

![new-plugin-files.png](https://batix.help/uploads/images/gallery/2025-05/scaled-1680-/mBNkiCI36eXsNqXJ-new-plugin-files-png.png)

In der Datei `settings.gradle.kts` im Hauptverzeichnis wird dieses neue Unterprojekt nun noch Gradle mittels `include` bekannt gegeben:

```kotlin{3}
// ...

include(":plugins:mitarbeiter-import")
```

## plugin.yaml

In dieser Datei stehen Informationen wie der Titel des Plugins. Außerdem ist ein Verweis auf die Plugin-Hauptklasse enthalten, die den Einstiegspunkt des Plugins darstellt. Ein Plugin ist eindeutig durch seine `id` identifiziert, welche automatisch anhand der `group` und des Projektnamens in Gradle in die finale Datei geschrieben wird. Es können nicht mehrere Plugins mit derselben `id` geladen werden. Die Version des Plugins wird ebenso automatisch in diese Datei geschrieben.

```yaml
id: "$id"
name: "Mitarbeiter Import"
pluginClass: "com.batix.website.mitarbeiter.ImportPlugin"
version: "$version"
```

Die Werte für `id` und `version` sind Platzhalter und dürfen nicht geändert werden. `name` sollte einen kurzen Titel enthalten, dieser wird im Framework an diversen Stellen angezeigt. Der Wert für `pluginClass` muss dem vollqualifizierten Namen der Plugin-Hauptklasse entsprechen (d. h. inklusive Package).

<p class="callout danger">**Konstante id**  
Der Wert von `id` sollte sich nach der ersten veröffentlichten Version nicht mehr ändern, da sonst Zuordnungen, die im Framework getroffen wurden (wie z. B. [Plugin-Preferences](https://batix.help/books/batix-plugins/page/plugin#bkmrk-preferences-%2F-storag)), verloren gehen!  
  
`group` in [build.gradle.kts](https://batix.help/books/batix-plugins/page/ide-setup#bkmrk-build.gradle.kts) sowie die Verzeichnisstruktur zum Plugin-Unterprojekt sollten also final sein.</p>

Es kann auch eine minimale System-Version festgelegt werden, unter der das Plugin laufen muss.

```yaml
minSystemVersion: "2.7.1"
```

Diese Datei wird im Build-Prozess mit den passenden Werten gefüllt und am Ende in das Plugin-ZIP gepackt, wo sie vom Framework als einer der ersten Vorgänge beim Plugin-Laden ausgelesen wird.

## build.gradle.kts

Auch ein Gradle-Unterprojekt wird über seine `build.gradle.kts` Datei konfiguriert. Diese Datei im Unterprojekt wird, je nachdem welche **Framework Version** im Einsatz ist, mit den entsprechenden Zeilen gefüllt:

<details id="bkmrk-ab-v3.0-plugins-%7B-ko"><summary>ab v3.0</summary>

```kotlin
plugins {
  kotlin("jvm")
}

//version = "1.0.0"

repositories {
  mavenCentral()

  val batixUrls = listOf(
    "https://git.batix.gmbh/api/v4/projects/477/packages/maven", // CMS-API
    "https://git.batix.gmbh/api/v4/projects/322/packages/maven", // Plugin-API
    "https://git.batix.gmbh/api/v4/projects/324/packages/maven",
  )

  batixUrls.forEach { url ->
    maven {
      this.url = uri(url)
      authentication {
        create<HttpHeaderAuthentication>("header")
      }

      if (!System.getenv("CI_JOB_TOKEN").isNullOrEmpty()) {
        credentials(HttpHeaderCredentials::class) {
          name = "Job-Token"
          value = System.getenv("CI_JOB_TOKEN")
        }
      } else {
        credentials(HttpHeaderCredentials::class) {
          name = "Private-Token"
          value = property("batix.gitlab.pat").toString()
        }
      }
    }
  }
}

dependencies {
  implementation("org.jetbrains.kotlin:kotlin-stdlib")

  // https://git.batix.gmbh/maven-packages/cms
  // https://git.batix.gmbh/maven-packages/cms/-/packages
  compileOnly("com.batix:batix-cm:3.0.0")

  // via Tomcat 10.1
  compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0")

  // via Tomcat 10.1
  compileOnly("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.1")

  // via Tomcat 10.1
  compileOnly("jakarta.websocket:jakarta.websocket-api:2.1.1")

  // via Tomcat 10.1
  compileOnly("jakarta.websocket:jakarta.websocket-client-api:2.1.1")

  // via CMS
  compileOnly("org.apache.groovy:groovy:4.0.9")
  compileOnly("org.apache.groovy:groovy-dateutil:4.0.9")
  compileOnly("org.apache.groovy:groovy-json:4.0.9")
  compileOnly("org.apache.groovy:groovy-sql:4.0.9")
  compileOnly("org.apache.groovy:groovy-xml:4.0.9")

  // via CMS
  //compileOnly("com.google.code.gson:gson:2.7")
}

java {
  toolchain {
    languageVersion.set(JavaLanguageVersion.of(21))
  }
}

//
// -- packaging --
//

val packageBatixPlugin by tasks.registering(Zip::class) {
  group = "build"
  dependsOn("cleanPackageBatixPlugin")

  val distDir = layout.buildDirectory.dir("dist")
  destinationDirectory.set(distDir)
  outputs.dir(distDir)

  from("plugin.yaml") {
    expand(
      mapOf(
        "id" to "${project.group}:${project.name}",
        "version" to project.version
      )
    )
  }

  from(tasks.named("jar")) {
    into("lib")
  }

  from(configurations.runtimeClasspath) {
    into("lib")
    exclude("slf4j-api-*.jar")
  }

  from("static") {
    into("static")
  }
}

tasks.named("assemble") {
  dependsOn(packageBatixPlugin)
}
```

</details><details id="bkmrk-ab-v2.9-plugins-%7B-ko"><summary>ab v2.9</summary>

```kotlin
plugins {
  kotlin("jvm")
}

//version = "1.0.0"

repositories {
  mavenCentral()

  val batixUrls = listOf(
    "https://git.batix.gmbh/api/v4/projects/477/packages/maven", // CMS-API
    "https://git.batix.gmbh/api/v4/projects/322/packages/maven", // Plugin-API
    "https://git.batix.gmbh/api/v4/projects/324/packages/maven",
  )

  batixUrls.forEach { url ->
    maven {
      this.url = uri(url)
      authentication {
        create<HttpHeaderAuthentication>("header")
      }

      if (!System.getenv("CI_JOB_TOKEN").isNullOrEmpty()) {
        credentials(HttpHeaderCredentials::class) {
          name = "Job-Token"
          value = System.getenv("CI_JOB_TOKEN")
        }
      } else {
        credentials(HttpHeaderCredentials::class) {
          name = "Private-Token"
          value = property("batix.gitlab.pat").toString()
        }
      }
    }
  }
}

dependencies {
  implementation("org.jetbrains.kotlin:kotlin-stdlib")

  // https://git.batix.gmbh/maven-packages/cms
  // https://git.batix.gmbh/maven-packages/cms/-/packages
  compileOnly("com.batix:batix-cm:2.9.0.3")

  // via Tomcat 9
  compileOnly("javax.servlet:javax.servlet-api:4.0.0")

  // via Tomcat 9
  compileOnly("javax.websocket:javax.websocket-api:1.1")

  // via CMS
  compileOnly("org.apache.groovy:groovy:4.0.9")
  compileOnly("org.apache.groovy:groovy-dateutil:4.0.9")
  compileOnly("org.apache.groovy:groovy-json:4.0.9")
  compileOnly("org.apache.groovy:groovy-sql:4.0.9")
  compileOnly("org.apache.groovy:groovy-xml:4.0.9")

  // via CMS
  //compileOnly("com.google.code.gson:gson:2.7")
}

java {
  toolchain {
    languageVersion.set(JavaLanguageVersion.of(17))
  }
}

//
// -- packaging --
//

val packageBatixPlugin by tasks.registering(Zip::class) {
  group = "build"
  dependsOn("cleanPackageBatixPlugin")

  val distDir = layout.buildDirectory.dir("dist")
  destinationDirectory.set(distDir)
  outputs.dir(distDir)

  from("plugin.yaml") {
    expand(
      mapOf(
        "id" to "${project.group}:${project.name}",
        "version" to project.version
      )
    )
  }

  from(tasks.named("jar")) {
    into("lib")
  }

  from(configurations.runtimeClasspath) {
    into("lib")
    exclude("slf4j-api-*.jar")
  }

  from("static") {
    into("static")
  }
}

tasks.named("assemble") {
  dependsOn(packageBatixPlugin)
}
```

</details><details id="bkmrk-vor-v2.9-plugins-%7B-k"><summary>vor v2.9</summary>

```kotlin
plugins {
  kotlin("jvm")
}

//version = "1.0.0"

repositories {
  mavenCentral()

  // https://git.batix.gmbh/pub/maven/-/packages
  maven("https://git.batix.gmbh/api/v4/projects/324/packages/maven")
}

dependencies {
  implementation("org.jetbrains.kotlin:kotlin-stdlib")

  // https://git.batix.gmbh/pub/maven/-/packages/?orderBy=created_at&sort=desc&search%5B%5D=com%2Fbatix%2Fcms-api
  compileOnly("com.batix:cms-api:2.8.1.2")

  // via Tomcat 8.5
  compileOnly("javax.servlet:javax.servlet-api:3.1.0")

  // via Tomcat 8.5
  compileOnly("javax.websocket:javax.websocket-api:1.1")

  // via CMS
  compileOnly("org.codehaus.groovy:groovy-all:2.4.10")

  // via CMS
  //compileOnly("com.google.code.gson:gson:2.7")
}

java {
  toolchain {
    languageVersion.set(JavaLanguageVersion.of(8))
  }
}

//
// -- packaging --
//

val packageBatixPlugin by tasks.registering(Zip::class) {
  group = "build"
  dependsOn("cleanPackageBatixPlugin")

  val distDir = layout.buildDirectory.dir("dist")
  destinationDirectory.set(distDir)
  outputs.dir(distDir)

  from("plugin.yaml") {
    expand(
      mapOf(
        "id" to "${project.group}:${project.name}",
        "version" to project.version
      )
    )
  }

  from(tasks.named("jar")) {
    into("lib")
  }

  from(configurations.runtimeClasspath) {
    into("lib")
    exclude("slf4j-api-*.jar")
  }

  from("static") {
    into("static")
  }
}

tasks.named("assemble") {
  dependsOn(packageBatixPlugin)
}
```

</details>Gehen wir diese Datei (in der Version für v2.9) mal auszugsweise durch. Da im Grunde alle Gradle-Projekte nach diesem Schema aufgebaut sind, ist dieser kleine Exkurs hoffentlich auch für andere Projekte hilfreich.

### plugins

```kotlin
plugins {
  kotlin("jvm")
}
```

Da wir das Plugin in Kotlin schreiben, brauchen wir auch das Kotlin Gradle-Plugin, damit sich der Kotlin-Compiler in den Build-Prozess einklinkt. Die Version des Kotlin-Plugins wird hier nicht angegeben, diese wurde ja schon im Hauptprojekt definiert.

### version

```kotlin
//version = "1.0.0"
```

Wie schon erwähnt wird die Version eigentlich anhand der Git-Tags vergeben. Hat man aber völlig verschiedenartige Unterprojekte, kann man hier auch die Version je Unterprojekt überschreiben. Da in unserem Fall die automatische Git-Version benutzt werden soll, ist diese Zeile auskommentiert.

### repositories

```kotlin
repositories {
  mavenCentral()

  // ...
}
```

Hier werden alle Maven-Repositories angegeben, in denen nach Dependencies gesucht werden soll. `mavenCentral()` registriert dabei das weltweite Standard-Maven-Repo. Weitere `maven` Repos können hinzugefügt werden, falls Dependencies geladen werden, die nicht öffentlich sind - wie es z. B. bei der Framework-API (ab v2.9) der Fall ist, gegen die Plugins entwickelt werden.

<p class="callout info">**Note**  
Der Zugriff auf das private Maven-Repo ist bei Batix zu erfragen.</p>

### dependencies

```kotlin
dependencies {
  implementation("org.jetbrains.kotlin:kotlin-stdlib")

  // ...
}
```

Jedes Unterprojekt deklariert hier die Dependencies, die es zur Kompilation oder zur Ausführung benötigt. Die Koordinaten der Dependencies (Gruppe, Artefakt, Version - durch Doppelpunkt getrennt) sind auf den jeweiligen Projektseiten oder z. B. via [mvnrepository.com](https://mvnrepository.com/) zu ermitteln. Ein Link als Kommentar über der Dependency-Zeile, unter dem man die Versionen der Dependency sehen kann, ist hilfreich und sollte immer eingefügt werden (mvnrepository macht das automatisch, wenn man aus dem "Gradle (Kotlin)" Tab kopiert).

Die erste Dependency (`org.jetbrains.kotlin:kotlin-stdlib`) enthält die Kotlin Runtime, welche immer für Kotlin-Projekte benötigt wird. Hier ist keine Version angegeben. Diese wird automatisch vom Kotlin-Gradle-Plugin vorgegeben.

Die nächste Dependency (`com.batix:batix-cm:2.9.0.3`) ist die Framework-API. **Hier sollte die Version verwendet werden, unter der auch das Framework läuft, in dem das Plugin dann benutzt wird.**

Die anderen 3 Dependencies (`servlet-api`, `websocket-api` und `groovy-all`) sind Sachen, die zur Laufzeit zur Verfügung stehen, da die Runtime bzw. das Framework diese mitbringt.

<p class="callout success">**Tip**  
Da die GSON-Bibliothek auch beim Framework mitgeliefert wird, kann diese `compileOnly` Dependency noch aktiviert werden (so muss diese Dependency nicht im Plugin mitgeliefert werden).</p>

Danach können dann eigene Dependencies deklariert werden. Um beispielweise Jackson zu nutzen, fügt man folgende Zeilen im `dependencies` Block hinzu:

```kotlin
// https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.3")
```

Ein Gradle-Sync macht die neuen Klassen der IDE bekannt und diese werden dann auch von der Autovervollständigung vorgeschlagen.

<p class="callout success">**Tip**  
Bei Gradle gibt es das Konzept von *Configurations*. Die [Gradle Hilfe dazu](https://docs.gradle.org/current/userguide/declaring_dependencies.html) geht ins Detail, hier sei nur das Wichtigste erwähnt.  
In einer Configuration werden mehrere Dependencies gesammelt. Es gibt verschiedene Configurations, in denen Dependencies gesammelt werden, welche zum Compilen und zur Laufzeit gebraucht werden, andere Configurations beschreiben Dependencies, die nur zum Compilen gebraucht werden und nicht mit ausgeliefert werden sollen. Je nach verwendeten Gradle-Plugins sind außerdem andere Configurations verfügbar ([Beispiel Java Gradle Plugin](https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_plugin_and_dependency_management)). Die wichtigsten Configurations und deren Eigenschaften sind:  
- `implementation` - zum Compilen verfügbar, landet auch in Distributionen (z. B. Plugin ZIP)  
- `compileOnly` - nur zum Compilen verfügbar, wird nicht mit ausgeliefert  
- `runtimeOnly` - beim Compilen nicht verfügbar, wird aber mit ausgeliefert  
- `testImplementation` - zum Compilen von Tests verfügbar  
- `compile` (deprecated) - findet man noch in einigen alten Tutorials, ist meistens durch `implementation` zu ersetzen  
  
Baut man kein Framework-Plugin, sondern eine Library (also Code, der in anderen Projekten nachgenutzt werden kann) bringt das [Java Library Gradle Plugin](https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_separation) noch `api` mit, was `implementation` ähnelt. Der Unterschied ist, dass die Dependencies aus `api` auch im Library-Consumer sichtbar sind, die aus `implementation` allerdings nicht. Beide werden in Distributionen ausgeliefert.</p>

### JVM Version

```kotlin
java {
  toolchain {
    languageVersion.set(JavaLanguageVersion.of(21))
  }
}
```

Hier wird die JDK-Version festgelegt, mit der die Quellcodes kompiliert werden. Es entsteht Bytecode, der mit dieser Java-Version (und neueren, aber nicht älteren) kompatibel ist.

### packageBatixPlugin

```kotlin
//
// -- packaging --
//

val packageBatixPlugin by tasks.registering(Zip::class) {
  //...
}

tasks.named("assemble") {
  dependsOn(packageBatixPlugin)
}
```

Dieser Block definiert einen eigenen Task namens "packageBatixPlugin". Er ist vom Typ `Zip`, erstellt also ein Archiv. `group = "build"` gibt an, unter welcher Gruppe der Task im IntelliJ Gradle Panel auftauchen soll.

Mittels `dependsOn("cleanPackageBatixPlugin")` wird gesagt, dass immer das Outputverzeichnis (welches mit `destinationDirectory` und `outputs` festgelegt wird) geleert werden soll, bevor die Plugin-ZIP dort abgelegt werden soll. Das ist hilfreich, wenn sich beim Entwickeln die Version ändert, damit nicht mehrere ZIPs mit unterschiedlichen Versionen im Outputverzeichnis liegen (und man die falsche Datei erwischt).

Die `from` Blöcke beschreiben, welche Dateien in der ZIP landen sollen. Nebst der `plugin.yaml` (in der hier auch die Platzhalter ersetzt werden), wird noch die kompilierte `jar` Datei des Plugin-Projektes sowie die Dependencies, die zur Laufzeit nötig sind (`configurations.runtimeClasspath`) hinzugefügt. Falls es im Unterprojekt ein Verzeichnis `static` gibt, wird auch dieses in die ZIP gepackt.

## Hauptklasse

Die Plugin-Hauptklasse muss von `com.batix.plugins.Plugin` abgeleitet werden. In ihr kann man die Methoden `load()` und (falls nötig) `unload()` überschreiben.

In `load()` können externe Dependencies oder interne Sachen initialisiert werden. Außerdem werden hier dem Application Framework die einzelnen Erweiterungen mitgeteilt, die das Plugin mitbringt.

In `unload()` müssen benutzte Ressourcen wieder aufgeräumt werden, das sind z. B. angelegte Threads, Listener oder Worker. Es ist die Anleitung der externen Dependencies zurate zu ziehen, wie man diese korrekt herunterfährt / aufräumt, falls nötig. Benutzt man beispielsweise Kotlin Coroutines, so kann hier `Dispatchers.shutdown()` aufgerufen werden.

<p class="callout success">**Tip**  
Framework-Erweiterungen, welche in `load()` bekannt gemacht wurden ([Service](https://bookstack.bxintern.batix.net/extensions/service), [Action](https://bookstack.bxintern.batix.net/extensions/action), [Tag](https://bookstack.bxintern.batix.net/extensions/tag), etc.), müssen nicht manuell unregistriert werden, dies erfolgt automatisch beim Entladen des Plugins.</p>

<p class="callout danger">**Qualifizierter Name**  
Der qualifizierte Name der Klasse (also inklusive Package) muss dem entsprechen, was in der [plugin.yaml](#plugin-yaml) unter `pluginClass` angegeben wurde. Ansonsten kann das Plugin vom Framework nicht geladen werden.</p>

Im Falle des Beispiels wird also unter dem entsprechenden Pfad (hier ist das `src/main/kotlin/com/batix/website/mitarbeiter`) die Klasse `ImportPlugin` angelegt und von `com.batix.plugins.Plugin` abgeleitet.

![plugin-class.png](https://batix.help/uploads/images/gallery/2025-05/scaled-1680-/bryOkgK7Fs7x3Mer-plugin-class-png.png)

## Extensions

Framework-Erweiterungen wie z. B. Actions und Tags registriert man mit den entsprechenden `register*()` Methoden, also beispielsweise `registerService(serviceName, service)`. Diese werden auf den nächsten Seiten näher beschrieben. Passend dazu gibt es `unregister*()` Methoden, z. B. `unregisterService(serviceName)` und `unregisterAllServices()`, um die Extensions dynamisch entfernen zu können (ansonsten werden sie automatisch beim Plugin-Unload entfernt).

<p class="callout success">**Tip**  
Extensions können, genau wie das gesamte Plugin, jederzeit im laufenden Betrieb aktiviert und deaktiviert werden. So kann man bestimmte Funktionen in Abhängigkeit von anderen Sachen (z. B. Konfigurationen) zur Verfügung stellen, oder auch nicht.</p>

## Logging

In der Hauptklasse sind ein paar Hilfsmethoden verfügbar, die zum Loggen benutzt werden können. Dabei wird automatisch der Plugin-Titel vorangestellt, sodass man die Nachrichten dem entsprechenden Plugin zuordnen kann.

```kotlin
fun logD(msg: String)
fun logI(msg: String)
fun logN(msg: String)
fun logW(msg: String)
fun logE(msg: String)
fun logE(msg: String, ex: Exception)
```

Diese können innerhalb der Hauptklasse einfach benutzt werden.

```kotlin
logI("Kotlin version: ${KotlinVersion.CURRENT}")
```

Möchte man diese Methoden auch an anderen Stellen wie den Extension-Klassen verwenden, so sollte die Plugin-Instanz an diese Klassen weitergereicht werden.

## Preferences / Storage

Mittels `storage` (`getStorage()`) bekommt man eine für das aktuelle Plugin gültige Instanz von `PluginStorage`. Die darin abgelegten Sachen überstehen einen Reload des Plugins sowie einen Neustart des Frameworks. Ein Plugin hat nur Zugriff auf seine eigenen Preferences (identifiziert anhand der `id` des Plugins).

<p class="callout warning">**Benutzung**  
`storage` ist erst initialisiert, sobald die `load()` Methode aufgerufen wird. Vorher (also z. B. bei der Initialisierung von Feldern in der Plugin-Klasse) darf `storage` noch nicht benutzt werden.</p>

Für alle primitiven Typen gibt es jeweils eine `set*` und `get*` Methode, über die Einstellungen anhand eines String-Keys abgelegt und wieder geholt werden können.

```kotlin
fun getStringPref(key: String): String
fun setStringPref(key: String, value: String)

fun getBooleanPref(key: String): Boolean
fun setBooleanPref(key: String, value: Boolean)

// usw. für die anderen primitiven Typen
```

Diese Methoden können unter `storage` benutzt werden.

```kotlin
var startCount = storage.getIntegerPref("startCount") ?: 0
storage.setIntegerPref("startCount", ++startCount)
logI("This is start #$startCount")
```

Falls komplexere Objekte wie Maps oder eigene Datenklassen abgelegt werden sollen, können die `*ObjectPref` Methoden benutzt werden. Zu beachten ist, dass diese Objekte nicht zu komplex sein dürfen, da sie im JSON-Format abgelegt werden (wenige MB verfügbar je Preference).

```kotlin
data class StartupInfo(val count: Int)

val prevStartupInfo = storage.getObjectPref("startupInfo", StartupInfo::class.java)
logI("prevStartupInfo: $prevStartupInfo")
storage.setObjectPref("startupInfo", StartupInfo(startCount))
```

Um Einstellungen zu löschen, gibt es `clear*` Methoden.

```kotlin
fun clearPref(key: String)
fun clearAllPrefs()
```

Falls größere Mengen oder Binärdaten gespeichert werden müssen, oder ein temporäres Verzeichnis gebraucht wird, kann `storage.dataDir` (`getStorage().getDataDir()`) benutzt werden. Hier erhält man einen `Path`, der auf ein (für das aktuelle Plugin gültige) Verzeichnis auf der Festplatte zeigt, in dem das Plugin Dateien / Ordner ablegen kann. Dieses Verzeichnis überdauert auch einen Neustart.

<p class="callout danger">**Plugin id Abhängigkeit**  
Wenn sich die `id` eines Plugins ändert, sind die zuvor gesetzten Preferences und abgelegten Dateien nicht mehr zugänglich! Eine Änderung der Version des Plugins ist davon nicht betroffen.</p>