# OpenKM Plugins — Developer Reference This document covers the OpenKM plugin system: how to register a plugin and how to implement each of the supported plugin types. Plugins extend OpenKM functionality without modifying the core application. They are deployed as standalone JAR files and discovered at runtime via the JSPF framework (Java Simple Plugin Framework). No application restart is required for most plugin types — reloading from the administration panel is sufficient. ## Related references When implementing plugins you will regularly use the OpenKM API and the core utility classes. Those are documented in the companion reference files listed below. Load them alongside this file for complete context: - `llms-full-api-*.txt` — OpenKM Java API (OKMDocument, OKMFolder, OKMSearch, etc.) - `llms-full-core-utils-*.txt` — Core utility classes (FileUtils, PathUtils, PDFUtils, PrincipalUtils, AutomationUtils, etc.) - `llms-full-metadata-*.txt` — Property groups, form element types, metadata validators, and form-related plugin types The wildcard notation is intentional: each file evolves independently and the plugin interfaces documented here are designed to remain stable across patch releases. --- ## Plugin fundamentals ### BasePlugin Every plugin class must extend `com.openkm.plugin.BasePlugin`. This ensures that Spring `@Autowired` fields are resolved automatically when the plugin is instantiated outside the Spring context (i.e., when loaded from the external plugins folder). ```java import com.openkm.plugin.BasePlugin; import net.xeoh.plugins.base.annotations.PluginImplementation; @PluginImplementation public class MyPlugin extends BasePlugin implements SomePluginInterface { // @Autowired fields resolved automatically via BasePlugin constructor } ``` ### @PluginImplementation The `@PluginImplementation` annotation (from `net.xeoh.plugins.base.annotations`) marks a class as a JSPF plugin. The OpenKM plugin loader scans for this annotation in both the application classpath and the external plugins folder. Every plugin class must carry this annotation. ### Activation behaviour When a plugin is first detected it is registered in the database: - **Enabled by default**: Action, Validation, CronAdapter, AIPrompt, AutocompleteFormValues, DbAccessManager, FieldValidator, FormDefaultValues, FormInterceptor, FormValidator, NodePaginator, NodeProperties, NodeSearch, OCRTemplateParser, OCRTemplateControlParser, OptionSelectValues, Report, RestPlugin, Suggestion, SuggestBoxValues, VersionNumerationAdapter, and the standard built-in converters and text extractors. - **Disabled by default**: Antivirus analyzers and any converter or text extractor that is not in the built-in list. Plugin activation state can be toggled at any time from **Administration > Utilities > Plugins**. --- ## Register a new plugin ### Steps **1. Build a JAR** Package the plugin class (and any required dependencies not already on the application classpath) into a JAR file using Maven or your build tool of choice. **2. Deploy the JAR** Copy the JAR into the Tomcat plugins directory: ``` $CATALINA_HOME/plugins/ ``` **3. Reload plugins** In the OpenKM administration panel navigate to **Administration > Utilities > Plugins** and click the **Reload plugins** button in the top-right corner. The new plugin will appear in the plugins table. **4. Activate (if necessary)** Some plugin types (e.g., antivirus analyzers) are registered as disabled on first detection. Enable them individually from the plugins table. ### Configuration notes Certain plugin types — such as Principal Adapter or Security Access Manager — may require additional `openkm.properties` configuration entries and an application restart to take effect. Check the specific plugin type section for details. --- ## Variables by automation event Automation actions and validations receive a `Map env` parameter populated by OpenKM before firing the event. The available variables depend on which event triggered the rule. All variables are accessed and mutated through `AutomationUtils` (see `llms-full-core-utils-*.txt`). ### Always-present variables | Key constant | Accessor | Type | Description | |---|---|---|---| | `EVENT` | `getEvent(env)` | `AutomationRule.EnumEvents` | The event that fired the rule | | `EVENT_AT` | `getEventAt(env)` | `String` | `AutomationRule.AT_PRE` or `AT_POST` | | `TENANT` | `getTenant(env)` | `long` | Current tenant ID | | `USER` | `getUser(env)` | `String` | Username of the user who triggered the event | ### Node variables | Key constant | Accessor | Type | Present in | |---|---|---|---| | `NODE_BASE` | `getNodeBase(env)` | `NodeBase` (cast to `NodeDocument`, `NodeFolder`, `NodeMail`, `NodeRecord`) | All node events (POST or PRE where node exists) | | `DESTINATION_NODE` | `getDestinationNode(env)` | `NodeParent` | Create and move events (PRE) | | `SOURCE_PATH` | `getSourcePath(env)` | `String` | Move events | | `NODE_CLASS` | `getNodeClass(env)` | `Long` | Set node class events | | `NEW_NODE_CLASS` | `getNewNodeClass(env)` | `Long` | Set node class events | | `OLD_NODE_CLASS` | `getOldNodeClass(env)` | `Long` | Set node class events | | `NODE_BASE_LIST` | `getNodeBaseList(env)` | `List` | Events returning multiple nodes | ### Document / general node variables | Key constant | Accessor | Type | Writable at PRE | |---|---|---|---| | `NAME` | `getName(env)` | `String` | Yes — rename events | | `MIME_TYPE` | `getMimeType(env)` | `String` | Yes — create events | | `CATEGORIES` | `getCategories(env)` | `Set` (category UUIDs) | Yes | | `KEYWORDS` | `getKeywords(env)` | `Set` | Yes | | `NOTES` | `getNotes(env)` | `List` | No | | `TITLE` | `getTitle(env)` | `String` | Yes | | `VERSION` | `getVersion(env)` | `String` | No | | `CREATION_DATE` | `getCreationDate(env)` | `Calendar` | Yes — create events | | `DOCUMENT_INPUT_STREAM` | `getDocumentInputStream(env)` | `InputStream` | Yes — create/update PRE | | `DOCUMENT_FILE` | `getDocumentFile(env)` | `File` | Yes — create/update PRE | | `DOCUMENT_SIZE` | `getDocumentSize(env)` | `Long` | No | | `TEXT_EXTRACTED` | `getTextExtracted(env)` | `String` | No | | `LANGUAGE_DETECTED` | `getLanguageDetected(env)` | `String` | No | | `TEXT_EXTRACTOR` | `getTextExtractor(env)` | `TextExtractor` | Yes | | `VIRUS_INFO` | `getVirusInfo(env)` | `String` | No — virus events only | ### Property group variables | Key constant | Accessor | Type | Description | |---|---|---|---| | `PROPERTY_GROUPS` | `getPropertyGroups(env)` | `Map` | Active property group values (key → value) | | `ALL_PROPERTY_GROUPS` | `getAllPropertyGroups(env)` | `List` | All property group definitions | | `PROPERTY_GROUP_NAME` | `getPropertyGroupName(env)` | `String` | Name of the affected property group | | `PROPERTY_GROUP_PROPERTIES` | `getPropertyGroupProperties(env)` | `Map` | Properties of the affected group | ### Mail variables (mail events only) | Key constant | Accessor | Type | |---|---|---| | `MAIL_FROM` | `getMailFrom(env)` | `String` | | `MAIL_TO` | `getMailTo(env)` | `String[]` | | `MAIL_CC` | `getMailCc(env)` | `String[]` | | `MAIL_BCC` | `getMailBcc(env)` | `String[]` | | `MAIL_REPLY` | `getMailReply(env)` | `String[]` | | `MAIL_SUBJECT` | `getMailSubject(env)` | `String` | | `MAIL_CONTENT` | `getMailContent(env)` | `String` | | `MAIL_SENT_DATE` | `getMailSentDate(env)` | `Calendar` | | `MAIL_RECEIVED_DATE` | `getMailReceivedDate(env)` | `Calendar` | ### Task variables (task events only) | Key constant | Accessor | Type | |---|---|---| | `TASK_ID` | `getTaskId(env)` | `Long` | | `TASK_OBJECT` | `getTaskObject(env)` | `Object` | ### Note variables (note events only) | Key constant | Accessor | Type | |---|---|---| | `NOTE_TEXT` | `getNoteText(env)` | `String` | | `NOTE_UUID` | `getNoteUuid(env)` | `String` | ### Web variables (HTTP context events) | Key constant | Accessor | Type | |---|---|---| | `HTTP_SERVLET_REQUEST` | `getHttpServletRequest(env)` | `HttpServletRequest` | | `HTTP_SERVLET_RESPONSE` | `getHttpServletResponse(env)` | `HttpServletResponse` | | `AUTHENTICATION` | `getAuthentication(env)` | `Authentication` | ### Full list of events (`AutomationRule.EnumEvents`) | Enum constant | Value | Category | |---|---|---| | `EVENT_DOCUMENT_CREATE` | `doc_create` | Document | | `EVENT_DOCUMENT_UPDATE` | `doc_update` | Document | | `EVENT_DOCUMENT_DELETE` | `doc_delete` | Document | | `EVENT_DOCUMENT_RENAME` | `doc_rename` | Document | | `EVENT_DOCUMENT_PURGE` | `doc_purge` | Document | | `EVENT_DOCUMENT_MOVE` | `doc_move` | Document | | `EVENT_DOCUMENT_RESTORE_VERSION` | `doc_restore_version` | Document | | `EVENT_DOCUMENT_DOWNLOAD_FROM_UI` | `doc_download_from_ui` | Document | | `EVENT_DOCUMENT_DOWNLOAD_FROM_UI_FOR_PREVIEW` | `doc_download_from_ui_for_preview` | Document | | `EVENT_DOCUMENT_STAMP` | `doc_stamp` | Document | | `EVENT_DOCUMENT_VIRUS_DETECTED` | `doc_virus_detected` | Document | | `EVENT_DOCUMENT_SET_NODE_CLASS` | `doc_set_node_class` | Document | | `EVENT_RECORD_CREATE` | `rec_create` | Record | | `EVENT_RECORD_DELETE` | `rec_delete` | Record | | `EVENT_RECORD_PURGE` | `rec_purge` | Record | | `EVENT_RECORD_MOVE` | `rec_move` | Record | | `EVENT_RECORD_RENAME` | `rec_rename` | Record | | `EVENT_RECORD_SET_NODE_CLASS` | `rec_set_node_class` | Record | | `EVENT_FOLDER_CREATE` | `fld_create` | Folder | | `EVENT_FOLDER_DELETE` | `fld_delete` | Folder | | `EVENT_FOLDER_RENAME` | `fld_rename` | Folder | | `EVENT_FOLDER_PURGE` | `fld_purge` | Folder | | `EVENT_FOLDER_MOVE` | `fld_move` | Folder | | `EVENT_FOLDER_EXPORT_AS_JAR` | `fld_export_jar` | Folder | | `EVENT_MAIL_CREATE` | `mail_create` | Mail | | `EVENT_MAIL_DELETE` | `mail_delete` | Mail | | `EVENT_MAIL_PURGE` | `mail_purge` | Mail | | `EVENT_MAIL_MOVE` | `mail_move` | Mail | | `EVENT_MAIL_DOWNLOAD_FROM_UI` | `mail_download_from_ui` | Mail | | `EVENT_MAIL_DOWNLOAD_FROM_UI_FOR_PREVIEW` | `doc_mail_from_ui_for_preview` | Mail | | `EVENT_MAIL_SET_NODE_CLASS` | `mail_set_node_class` | Mail | | `EVENT_MAIL_TEXT_EXTRACTOR` | `mail_text_extractor` | Mail | | `EVENT_TEXT_EXTRACTOR` | `text_extractor` | General | | `EVENT_PROPERTY_GROUP_GET_ALL` | `prop_group_get_all` | Property group | | `EVENT_PROPERTY_GROUP_ADD` | `prop_group_add` | Property group | | `EVENT_PROPERTY_GROUP_SET` | `prop_group_set` | Property group | | `EVENT_PROPERTY_GROUP_REMOVE` | `prop_group_remove` | Property group | | `EVENT_NODE_EXPORT_AS_ZIP` | `node_export_zip` | General | | `EVENT_TASK_CREATE` | `task_create` | Task | | `EVENT_TASK_UPDATE` | `task_update` | Task | | `EVENT_TASK_DELETE` | `task_delete` | Task | | `EVENT_USER_CREATE` | `user_create` | User | | `EVENT_USER_LOGIN` | `user_login` | User | | `EVENT_USER_LOGOUT` | `user_logout` | User | | `EVENT_NOTE_CREATE` | `note_create` | Note | | `EVENT_NOTE_UPDATE` | `note_update` | Note | | `EVENT_NOTE_DELETE` | `note_delete` | Note | --- ## Creating your own Automation Action An Automation Action is a piece of logic that OpenKM executes automatically in response to repository events (document created, moved, renamed, etc.). Actions can run before the event commits (`executePre`) or after it completes (`executePost`). ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.automation.Action` | | Package | `com.openkm.plugin.automation.action` | | Annotation | `@PluginImplementation` | | Base class | `BasePlugin` | ### Interface ```java public interface Action extends Plugin { void executePre(Map env, Object... params) throws AutomationException; void executePost(Map env, Object... params) throws AutomationException; String getName(); String getParamType00(); String getParamSrc00(); String getParamDesc00(); String getParamType01(); String getParamSrc01(); String getParamDesc01(); String getParamType02(); String getParamSrc02(); String getParamDesc02(); List getValidEventsAtPre(); List getValidEventsAtPost(); } ``` ### Parameter constants (`com.openkm.db.bean.Automation`) **Types** (`getParamTypeNN`): | Constant | Value | UI widget | |---|---|---| | `PARAM_TYPE_EMPTY` | `""` | Hidden — parameter not used | | `PARAM_TYPE_TEXT` | `"text"` | Single-line text input | | `PARAM_TYPE_INTEGER` | `"integer"` | Integer input | | `PARAM_TYPE_LONG` | `"long"` | Long integer input | | `PARAM_TYPE_BOOLEAN` | `"boolean"` | Checkbox | | `PARAM_TYPE_TEXTAREA` | `"textarea"` | Multi-line text | | `PARAM_TYPE_CODE` | `"code"` | Code editor | | `PARAM_TYPE_USER` | `"user"` | User picker | | `PARAM_TYPE_ROLE` | `"role"` | Role picker | **Sources** (`getParamSrcNN`): | Constant | Value | Description | |---|---|---| | `PARAM_SOURCE_EMPTY` | `""` | No special source | | `PARAM_SOURCE_FOLDER` | `"okm:folder"` | Folder picker (UUID stored as text) | Set `getParamDescNN` to a short human-readable label. Use `PARAM_DESCRIPTION_EMPTY` (`""`) for unused slots. ### Accessing env and params ```java // Read the node that triggered the event (document, folder, mail, or record) NodeBase node = automationUtils.getNodeToEvaluate(env); String uuid = node.getUuid(); // Read user-configured parameters (index matches param slot 00, 01, 02) String text = automationUtils.getString(0, params); Boolean flag = automationUtils.getBoolean(1, params); Integer number = automationUtils.getInteger(0, params); ``` ### Recursion prevention Actions that modify nodes (rename, update, etc.) can trigger the same event again, causing an infinite loop. Guard against this with `StackTraceUtils`: ```java if (StackTraceUtils.isCallingMe(this.getClass().getName())) { return; // already executing — skip to avoid infinite loop } ``` ### Example — AddTag (adds a keyword to the triggering node) ```java package com.openkm.plugin.automation.action; import com.openkm.db.bean.Automation; import com.openkm.db.bean.AutomationRule.EnumEvents; import com.openkm.db.service.NodeBaseSrv; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.automation.Action; import com.openkm.plugin.automation.AutomationException; import com.openkm.plugin.automation.AutomationUtils; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @PluginImplementation public class AddTag extends BasePlugin implements Action { private static final ArrayList EVENTS_AT_PRE = new ArrayList<>(); private static final ArrayList EVENTS_AT_POST = Stream.of(EnumEvents.EVENT_DOCUMENT_CREATE, EnumEvents.EVENT_DOCUMENT_UPDATE) .collect(Collectors.toCollection(ArrayList::new)); @Autowired private AutomationUtils automationUtils; @Autowired private NodeBaseSrv nodeBaseSrv; @Override public void executePre(Map env, Object... params) throws AutomationException { // not used } @Override public void executePost(Map env, Object... params) throws AutomationException { try { String keyword = automationUtils.getString(0, params); String uuid = automationUtils.getNodeToEvaluate(env).getUuid(); if (uuid != null && keyword != null && !keyword.isEmpty()) { nodeBaseSrv.addKeyword(uuid, keyword); } } catch (Exception e) { throw new AutomationException("AddTag exception", e); } } @Override public String getName() { return "AddTag"; } @Override public String getParamType00() { return Automation.PARAM_TYPE_TEXT; } @Override public String getParamSrc00() { return Automation.PARAM_SOURCE_EMPTY; } @Override public String getParamDesc00() { return "Keyword"; } @Override public String getParamType01() { return Automation.PARAM_TYPE_EMPTY; } @Override public String getParamSrc01() { return Automation.PARAM_SOURCE_EMPTY; } @Override public String getParamDesc01() { return Automation.PARAM_DESCRIPTION_EMPTY; } @Override public String getParamType02() { return Automation.PARAM_TYPE_EMPTY; } @Override public String getParamSrc02() { return Automation.PARAM_SOURCE_EMPTY; } @Override public String getParamDesc02() { return Automation.PARAM_DESCRIPTION_EMPTY; } @Override public List getValidEventsAtPre() { return EVENTS_AT_PRE; } @Override public List getValidEventsAtPost() { return EVENTS_AT_POST; } } ``` --- ## Creating your own Automation Validation An Automation Validation evaluates a condition and returns `true` or `false`. When a validation returns `false`, the automation rule's associated actions are skipped. ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.automation.Validation` | | Package | `com.openkm.plugin.automation.validation` | | Annotation | `@PluginImplementation` | | Base class | `BasePlugin` | ### Interface ```java public interface Validation extends Plugin { boolean isValid(Map env, Object... params) throws AutomationException; String getName(); String getParamType00(); String getParamSrc00(); String getParamDesc00(); String getParamType01(); String getParamSrc01(); String getParamDesc01(); String getParamType02(); String getParamSrc02(); String getParamDesc02(); List getValidEventsAtPre(); List getValidEventsAtPost(); } ``` The parameter slots and `env`/`params` access patterns are identical to those in Automation Action (see above). ### Example — HasKeyword (checks whether the node has a specific keyword) ```java package com.openkm.plugin.automation.validation; import com.openkm.db.bean.Automation; import com.openkm.db.bean.AutomationRule.EnumEvents; import com.openkm.db.service.NodeBaseSrv; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.automation.AutomationException; import com.openkm.plugin.automation.AutomationUtils; import com.openkm.plugin.automation.Validation; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @PluginImplementation public class HasKeyword extends BasePlugin implements Validation { private static final ArrayList EVENTS_AT_PRE = new ArrayList<>(); private static final ArrayList EVENTS_AT_POST = Stream.of(EnumEvents.EVENT_DOCUMENT_CREATE, EnumEvents.EVENT_DOCUMENT_UPDATE, EnumEvents.EVENT_DOCUMENT_MOVE) .collect(Collectors.toCollection(ArrayList::new)); @Autowired private AutomationUtils automationUtils; @Autowired private NodeBaseSrv nodeBaseSrv; @Override public boolean isValid(Map env, Object... params) throws AutomationException { try { String keyword = automationUtils.getString(0, params); String uuid = automationUtils.getNodeToEvaluate(env).getUuid(); return uuid != null && !uuid.isEmpty() && nodeBaseSrv.hasKeyword(uuid, keyword); } catch (Exception e) { throw new AutomationException("HasKeyword exception", e); } } @Override public String getName() { return "HasKeyword"; } @Override public String getParamType00() { return Automation.PARAM_TYPE_TEXT; } @Override public String getParamSrc00() { return Automation.PARAM_SOURCE_EMPTY; } @Override public String getParamDesc00() { return "Keyword"; } @Override public String getParamType01() { return Automation.PARAM_TYPE_EMPTY; } @Override public String getParamSrc01() { return Automation.PARAM_SOURCE_EMPTY; } @Override public String getParamDesc01() { return Automation.PARAM_DESCRIPTION_EMPTY; } @Override public String getParamType02() { return Automation.PARAM_TYPE_EMPTY; } @Override public String getParamSrc02() { return Automation.PARAM_SOURCE_EMPTY; } @Override public String getParamDesc02() { return Automation.PARAM_DESCRIPTION_EMPTY; } @Override public List getValidEventsAtPre() { return EVENTS_AT_PRE; } @Override public List getValidEventsAtPost() { return EVENTS_AT_POST; } } ``` --- ## Creating your own Crontab plugin A Crontab plugin is a scheduled task that OpenKM runs periodically according to a cron expression. The task executes in a background thread with no interactive user session, so all API calls must use a system token. ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.cron.CronAdapter` | | Package | `com.openkm.plugin.cron` | | Annotation | `@PluginImplementation` | | Base class | `BaseCronPlugin` (extends `BasePlugin`, implements `CronAdapter`) | ### Interface ```java public interface CronAdapter extends Runnable, Plugin { String getName(); // display name shown in the admin panel String getCronExpression(); // default cron expression registered on first detection boolean isRunning(); // whether the task is currently executing void execute() throws Throwable; // the task logic } ``` `BaseCronPlugin` provides a complete `run()` implementation that: - Guards against concurrent execution (`isRunning()` check) - Records start/end times in the database via `CronTabSrv` - On error: saves the exception to the database, stores an HTML error report in the repository, and sends an admin e-mail notification You only need to implement `getName()`, `getCronExpression()`, and `execute()`. ### Authentication — system token Cron tasks have no interactive user session. To call OpenKM API or service methods that require a token, obtain the system token: ```java String systemToken = DbSessionManager.getInstance().getSystemToken(); ``` Pass this token to API methods that accept a `token` parameter (see `llms-full-api-*.txt`). ### Cron expression format Standard six-field Spring cron format: `seconds minutes hours day-of-month month day-of-week`. | Example | Meaning | |---|---| | `0 0 0 * * *` | Daily at midnight | | `0 0 2 * * *` | Daily at 02:00 | | `0 */30 * * * *` | Every 30 minutes | | `0 0 8 * * MON-FRI` | Weekdays at 08:00 | The expression returned by `getCronExpression()` is the **default** used when the plugin is first registered. It can be overridden from **Administration > Crontab** without redeploying the JAR. ### Example — CheckDiskSpace ```java package com.openkm.plugin.cron; import com.openkm.module.db.stuff.DbSessionManager; import com.openkm.util.FileUtils; import com.openkm.util.MailUtils; import com.openkm.util.RepositoryReportUtils; import lombok.extern.slf4j.Slf4j; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @Slf4j @PluginImplementation public class CheckDiskSpace extends BaseCronPlugin implements CronAdapter { @Autowired private MailUtils mailUtils; @Autowired private RepositoryReportUtils repositoryReportUtils; @Override public String getName() { return "Check Disk Space"; } @Override public String getCronExpression() { return "0 0 0 * * *"; // daily at midnight } @Override public void execute() throws Throwable { String msg = FileUtils.checkDiskSpace(); if (StringUtils.isNotEmpty(msg)) { // Send alert to admin mailUtils.sendAdminMessage(getName(), msg); // Store report in the repository String systemToken = DbSessionManager.getInstance().getSystemToken(); repositoryReportUtils.storeReport(systemToken, getName(), msg); } } } ``` --- ## Creating your own Text Extractor A Text Extractor plugin teaches OpenKM how to extract plain text from a document format it does not support natively. The extracted text is indexed by the search engine (Lucene / Elasticsearch). ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.extractor.TextExtractor` | | Package | `com.openkm.plugin.extractor` | | Annotation | `@PluginImplementation` | | Base class | `AbstractTextExtractor` (extends `BasePlugin`, implements `TextExtractor`) | ### Interface ```java public interface TextExtractor extends Plugin { String[] getContentTypes(); String extractText(InputStream stream, String type, String encoding) throws IOException; } ``` `AbstractTextExtractor` implements `getContentTypes()` by storing the array passed to its constructor. You only need to call `super(new String[]{"mime/type"})` and implement `extractText`. ### Method contracts | Method | Notes | |---|---| | `getContentTypes()` | MIME types must be **lower-case**. The array must not be empty. | | `extractText(stream, type, encoding)` | Read from `stream` and return the extracted text. `encoding` may be `null`. Close `stream` when done (or use try-with-resources). | ### Example — PlainTextExtractor ```java package com.openkm.plugin.extractor; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; @PluginImplementation public class PlainTextExtractor extends AbstractTextExtractor { private static final Logger log = LoggerFactory.getLogger(PlainTextExtractor.class); public PlainTextExtractor() { super(new String[]{"text/plain"}); } @Override public String extractText(InputStream stream, String type, String encoding) throws IOException { log.debug("extractText({}, {}, {})", stream, type, encoding); try { if (encoding != null) { return IOUtils.toString(stream, encoding); } } catch (UnsupportedEncodingException e) { log.warn("Unsupported encoding '{}', falling back to UTF-8.", encoding); } return IOUtils.toString(stream, StandardCharsets.UTF_8); } } ``` --- ## Creating your own Document Converter A Document Converter plugin allows OpenKM to convert documents of specific MIME types to PDF, PDF/A, SVG, or image format. Converters are used during document preview, printing, and explicit conversion requests. ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.conversion.Converter` | | Package | `com.openkm.plugin.conversion` | | Annotation | `@PluginImplementation` | | Base class | `BasePlugin` | ### Interface ```java public interface Converter extends Plugin { int TO_PDF = 0; int TO_IMAGE = 1; // output is PNG int TO_SVG = 2; int TO_PDFA = 3; void makeConversion(File input, String mimeType, File output) throws IOException, ConversionException; List getSourceMimeTypes(); int getConversionType(); } ``` ### Method contracts | Method | Notes | |---|---| | `getSourceMimeTypes()` | Returns the list of input MIME types this converter handles. | | `getConversionType()` | Returns one of the `Converter.TO_*` constants indicating the output format. | | `makeConversion(input, mimeType, output)` | Reads `input`, converts it, and writes the result to `output`. Both files already exist on disk; the caller manages their lifecycle. | ### Example — SqlToPdfConversion ```java package com.openkm.plugin.conversion; import com.openkm.core.ConversionException; import com.openkm.core.MimeTypeConfig; import com.openkm.plugin.BasePlugin; import com.openkm.util.DocConverter; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.List; @PluginImplementation public class SqlToPdfConversion extends BasePlugin implements Converter { @Autowired private DocConverter docConverter; @Override public void makeConversion(File input, String mimeType, File output) throws IOException, ConversionException { docConverter.src2pdf(input, output, "sql"); } @Override public List getSourceMimeTypes() { return Arrays.asList(MimeTypeConfig.MIME_SQL); } @Override public int getConversionType() { return Converter.TO_PDF; } } ``` --- ## Creating your own Report plugin A Report plugin adds a custom report to the OpenKM Reports section. It defines the input form that the user fills in and the logic that queries the repository and returns rows for display. ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.report.Report` | | Package | `com.openkm.plugin.report` | | Annotation | `@PluginImplementation` | | Base class | `BasePlugin` | ### Interface ```java public interface Report extends Plugin { String getName(); List getFormElements(); PageInfo execute(Map params) throws AccessDeniedException, PathNotFoundException, RepositoryException, DatabaseException; } ``` ### Method contracts | Method | Notes | |---|---| | `getName()` | Display name shown in the Reports section. | | `getFormElements()` | Returns the input fields rendered above the report. Use `Input`, `Select`, etc. from `com.openkm.bean.form`. Add `Validator` objects to enforce required/numeric constraints. | | `execute(params)` | Receives values from the form as a `Map`. Returns a `PageInfo` whose `objects` list contains `SimpleRowReport` items (up to 3 labelled columns each). | ### Key types - `PageInfo` (`com.openkm.bean.PageInfo`) — wraps the result list and total element count. - `SimpleRowReport` (`com.openkm.ws.rest.util.SimpleRowReport`) — one result row: `uuid`, `name`, `reportColumnLabel1/2/3`, `reportColumnValue1/2/3`. - `SimpleNodeBase` (`com.openkm.ws.rest.util.SimpleNodeBase`) — lightweight node projection returned by `NodeBaseSrv.getCommonChildrenPaginated`. ### Example — SampleReport ```java package com.openkm.plugin.report; import com.openkm.bean.PageInfo; import com.openkm.bean.form.FormElement; import com.openkm.bean.form.Input; import com.openkm.bean.form.Validator; import com.openkm.core.*; import com.openkm.db.bean.NodeBase; import com.openkm.db.bean.NodeDocument; import com.openkm.db.service.NodeBaseSrv; import com.openkm.module.db.DbRepositoryModule; import com.openkm.plugin.BasePlugin; import com.openkm.util.FormatUtil; import com.openkm.ws.rest.util.SimpleNodeBase; import com.openkm.ws.rest.util.SimpleRowReport; import lombok.extern.slf4j.Slf4j; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import java.util.*; @Slf4j @PluginImplementation public class SampleReport extends BasePlugin implements Report { @Autowired private DbRepositoryModule dbRepositoryModule; @Autowired private NodeBaseSrv nodeBaseSrv; @Override public String getName() { return "Sample report plugin"; } @Override public List getFormElements() { Input filter = new Input(); filter.setName("filter"); filter.setLabel("Filter"); Input limit = new Input(); limit.setName("limit"); limit.setLabel("Limit"); limit.setValue("100"); limit.setValidators(List.of( new Validator(Validator.TYPE_REQUIRED, ""), new Validator(Validator.TYPE_NUMERIC, "") )); return List.of(filter, limit); } @Override public PageInfo execute(Map params) throws AccessDeniedException, PathNotFoundException, RepositoryException, DatabaseException { String filter = params.get("filter"); int limit = Integer.parseInt(StringUtils.defaultIfEmpty(params.get("limit"), "100")); PageInfo src = nodeBaseSrv.getCommonChildrenPaginated( dbRepositoryModule.getRootFolder(null).getUuid(), 0, limit, filter, "name", true, List.of(NodeDocument.class) ); List rows = new ArrayList<>(); for (SimpleNodeBase snb : (List) src.getObjects()) { SimpleRowReport row = new SimpleRowReport(); row.setUuid(snb.getUuid()); row.setName(snb.getName()); row.setReportColumnLabel1("Author"); row.setReportColumnValue1(snb.getAuthor()); row.setReportColumnLabel2("Path"); row.setReportColumnValue2(snb.getPath()); row.setReportColumnLabel3("Date"); row.setReportColumnValue3(FormatUtil.formatDate(snb.getVersionCreated())); rows.add(row); } PageInfo result = new PageInfo(); result.setTotalElements(rows.size()); result.setObjects(rows); return result; } } ``` --- ## Creating your own REST plugin (extending the REST API) A REST plugin adds a custom endpoint to the OpenKM REST API. It is invoked via the existing plugin REST endpoint and identified by the plugin class name. ### Requirements | Requirement | Value | |---|---| | Interface | `com.openkm.plugin.rest.RestPlugin` | | Package | `com.openkm.plugin.rest` | | Annotation | `@PluginImplementation` | | Base class | `BasePlugin` | ### Interface ```java public interface RestPlugin extends Plugin { Object executePlugin(Map parameters, InputStream is) throws Exception; } ``` `parameters` contains all query/form parameters passed by the caller. `is` is the request body input stream (may be empty). The returned `Object` is automatically marshalled by OpenKM. ### Return value options | Scenario | What to return | |---|---| | JSON string | Build manually or use `new Gson().toJson(obj)` and return the `String` | | OpenKM bean (Document, Folder, etc.) | Return the object directly — XML/JSON annotations already present | | Custom structured object | Annotate your class with `@XmlRootElement` and return an instance | | Binary file / download | Return a `ResponseEntity` with appropriate `Content-Type` and `Content-Disposition` headers | ### Example 1 — Simple JSON response ```java package com.openkm.plugin.rest; import com.openkm.plugin.BasePlugin; import lombok.extern.slf4j.Slf4j; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.io.InputStream; import java.util.Map; @Slf4j @PluginImplementation public class TestRestPlugin extends BasePlugin implements RestPlugin { @Override public Object executePlugin(Map parameters, InputStream is) { log.debug("executePlugin({})", parameters); StringBuilder sb = new StringBuilder("{"); sb.append("\"class\":\"").append(this.getClass().getCanonicalName()).append("\""); for (Map.Entry e : parameters.entrySet()) { sb.append(", \"").append(e.getKey()).append("\":\"").append(e.getValue()).append("\""); } sb.append("}"); return sb.toString(); } } ``` ### Example 2 — Binary file download ```java package com.openkm.plugin.rest; import com.openkm.bean.Document; import com.openkm.module.db.DbDocumentModule; import com.openkm.plugin.BasePlugin; import com.openkm.util.PathUtils; import lombok.extern.slf4j.Slf4j; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.http.*; import java.io.InputStream; import java.util.Map; @Slf4j @PluginImplementation public class TestGetDocumentRestPlugin extends BasePlugin implements RestPlugin { @Autowired private DbDocumentModule dbDocumentModule; @Override public Object executePlugin(Map parameters, InputStream is) throws Exception { String docId = parameters.get("docId"); boolean inline = parameters.containsKey("inline"); Document doc = dbDocumentModule.getProperties(null, docId); InputStream content = dbDocumentModule.getContent(null, docId, false); String fileName = PathUtils.getName(doc.getPath()); HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", doc.getMimeType()); headers.add("Content-Disposition", (inline ? "inline" : "attachment") + "; filename=\"" + fileName + "\""); headers.setContentLength(doc.getActualVersion().getSize()); return new ResponseEntity<>(new InputStreamResource(content), headers, HttpStatus.OK); } } ``` --- ## NodeProperties plugin ### Interface ``` com.openkm.plugin.properties.NodeProperties ``` A `NodeProperties` plugin customises what properties are returned when OpenKM fetches node metadata. For each node type the plugin receives the raw JPA entity (`NodeDocument`, `NodeFolder`, `NodeMail`, `NodeRecord`) and must return the corresponding API bean (`Document`, `Folder`, `Mail`, `Record`) populated with whichever fields the implementation requires. ```java package com.openkm.plugin.properties; import com.openkm.bean.Document; import com.openkm.bean.Folder; import com.openkm.bean.Mail; import com.openkm.bean.Record; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.core.RepositoryException; import com.openkm.db.bean.NodeDocument; import com.openkm.db.bean.NodeFolder; import com.openkm.db.bean.NodeMail; import com.openkm.db.bean.NodeRecord; import net.xeoh.plugins.base.Plugin; public interface NodeProperties extends Plugin { Document getProperties(String user, NodeDocument nDocument) throws PathNotFoundException, DatabaseException, RepositoryException; Folder getProperties(String user, NodeFolder nFolder) throws PathNotFoundException, DatabaseException, RepositoryException; Mail getProperties(String user, NodeMail nMail) throws PathNotFoundException, DatabaseException, RepositoryException; Record getProperties(String user, NodeRecord nRecord) throws PathNotFoundException, DatabaseException, RepositoryException; } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.properties.NodeProperties` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.properties` | ### Methods | Method | Description | |---|---| | `getProperties(String user, NodeDocument nDocument)` | Returns a `Document` bean for the given document node. Populate only the fields the plugin needs to expose. | | `getProperties(String user, NodeFolder nFolder)` | Returns a `Folder` bean for the given folder node. | | `getProperties(String user, NodeMail nMail)` | Returns a `Mail` bean for the given mail node. | | `getProperties(String user, NodeRecord nRecord)` | Returns a `Record` bean for the given record node. | ### Example — UuidNodeProperties (returns only the UUID for each node type) ```java package com.openkm.plugin.properties; import com.openkm.bean.Document; import com.openkm.bean.Folder; import com.openkm.bean.Mail; import com.openkm.bean.Record; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.core.RepositoryException; import com.openkm.db.bean.NodeDocument; import com.openkm.db.bean.NodeFolder; import com.openkm.db.bean.NodeMail; import com.openkm.db.bean.NodeRecord; import com.openkm.plugin.BasePlugin; import net.xeoh.plugins.base.annotations.PluginImplementation; @PluginImplementation public class UuidNodeProperties extends BasePlugin implements NodeProperties { @Override public Document getProperties(String user, NodeDocument nDocument) throws PathNotFoundException, DatabaseException, RepositoryException { Document node = new Document(); node.setUuid(nDocument.getUuid()); return node; } @Override public Folder getProperties(String user, NodeFolder nFolder) throws PathNotFoundException, DatabaseException, RepositoryException { Folder node = new Folder(); node.setUuid(nFolder.getUuid()); return node; } @Override public Mail getProperties(String user, NodeMail nMail) throws PathNotFoundException, DatabaseException, RepositoryException { Mail node = new Mail(); node.setUuid(nMail.getUuid()); return node; } @Override public Record getProperties(String user, NodeRecord nRecord) throws PathNotFoundException, DatabaseException, RepositoryException { Record node = new Record(); node.setUuid(nRecord.getUuid()); return node; } } ``` --- ## NodePaginator plugin ### Interface ``` com.openkm.plugin.paginate.NodePaginator ``` A `NodePaginator` plugin controls how the file browser lists the children of a folder or category. It returns a `PageInfo` object that drives the UI table, including which columns are shown and how many custom metadata columns are displayed. ```java package com.openkm.plugin.paginate; import com.openkm.bean.NodePaginatorConfig; import com.openkm.bean.PageInfo; import com.openkm.core.AccessDeniedException; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.db.bean.NodeBase; import net.xeoh.plugins.base.Plugin; import java.util.List; public interface NodePaginator extends Plugin { PageInfo getChildrenPaginated(String uuid, int offset, int limit, String filter, String orderByField, boolean orderAsc, List> filteredByNodeTypeList) throws AccessDeniedException, PathNotFoundException, DatabaseException; NodePaginatorConfig getConfig() throws DatabaseException; PageInfo getChildrenByCategoryPaginated(String uuid, int offset, int limit, String filter, String orderByField, boolean orderAsc, List> filteredByNodeTypeList) throws AccessDeniedException, PathNotFoundException, DatabaseException; String getName(); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.paginate.NodePaginator` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.paginate` | | Activation | Profile > General > Pagination > Pagination plugins | ### Methods | Method | Description | |---|---| | `getChildrenPaginated(...)` | Returns a paginated list of children for the folder identified by `uuid`. Use `offset` and `limit` for pagination. `filter` is a text filter string. `filteredByNodeTypeList` restricts results to specific node types. | | `getConfig()` | Returns a `NodePaginatorConfig` that defines which standard columns are visible and the labels for up to 10 custom metadata columns. A column label that is empty means the column is hidden. | | `getChildrenByCategoryPaginated(...)` | Same as `getChildrenPaginated` but the `uuid` identifies a category node rather than a folder. | | `getName()` | Returns the display name shown in the paginator selector in the file browser. | ### NodePaginatorConfig fields `NodePaginatorConfig` (`com.openkm.bean.NodePaginatorConfig`) controls column visibility in the file browser. **Standard column visibility** (all `Boolean`, default `true`): | Field | Description | |---|---| | `columnStatusVisible` | Checked-out / locked status icon column | | `columnMassiveVisible` | Mass-action checkbox column | | `columnIconVisible` | Node type icon column | | `columnNameVisible` | Name column | | `columnTitleVisible` | Title column | | `columnLanguageVisible` | Language column | | `columnSizeVisible` | Size column | | `columnLastModifiedVisible` | Last modified date column | | `columnAuthorVisible` | Author column | | `columnVersionVisible` | Version number column | **Custom metadata columns** (up to 10, all default empty / not sortable): | Fields | Description | |---|---| | `pgrpColumnLabel01` … `pgrpColumnLabel10` | Label text for each custom column. An empty string hides the column. | | `pgrpColumnSortable01` … `pgrpColumnSortable10` | Whether the custom column is sortable (`Boolean`, default `false`). | ### Example — CustomMetadataPaginator (shows 2 metadata columns) ```java package com.openkm.plugin.paginate; import com.openkm.bean.NodePaginatorConfig; import com.openkm.bean.PageInfo; import com.openkm.core.AccessDeniedException; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.db.bean.NodeBase; import com.openkm.db.service.NodeBaseSrv; import com.openkm.plugin.BasePlugin; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @PluginImplementation public class CustomMetadataPaginator extends BasePlugin implements NodePaginator { @Autowired private NodeBaseSrv nodeBaseSrv; @Override public NodePaginatorConfig getConfig() throws DatabaseException { NodePaginatorConfig cfg = new NodePaginatorConfig(); cfg.setPgrpColumnLabel01("Department"); cfg.setPgrpColumnSortable01(true); cfg.setPgrpColumnLabel02("Project code"); // columns 03–10 remain empty → hidden return cfg; } @Override public PageInfo getChildrenPaginated(String uuid, int offset, int limit, String filter, String orderByField, boolean orderAsc, List> filteredByNodeTypeList) throws AccessDeniedException, PathNotFoundException, DatabaseException { // Delegate to the default service and enrich with custom column values. // Use nodeBaseSrv or LegacySrv to query metadata and call // row.setPgrpColumnValue01("...") on each PageInfo row as required. return nodeBaseSrv.getChildrenPaginated(uuid, offset, limit, filter, orderByField, orderAsc, filteredByNodeTypeList); } @Override public PageInfo getChildrenByCategoryPaginated(String uuid, int offset, int limit, String filter, String orderByField, boolean orderAsc, List> filteredByNodeTypeList) throws AccessDeniedException, PathNotFoundException, DatabaseException { return nodeBaseSrv.getChildrenByCategoryPaginated(uuid, offset, limit, filter, orderByField, orderAsc, filteredByNodeTypeList); } @Override public String getName() { return "Custom Metadata Paginator"; } } ``` --- ## Search plugin ### Interface ``` com.openkm.plugin.search.NodeSearch ``` A `NodeSearch` plugin customises the search functionality. It receives `QueryParams` and returns a paginated result set. The `NodeSearchConfig` returned by `getConfig()` controls which search fields are shown in the UI. ```java package com.openkm.plugin.search; import com.openkm.bean.NodeSearchConfig; import com.openkm.core.AccessDeniedException; import com.openkm.core.DatabaseException; import com.openkm.core.ParseException; import com.openkm.core.RepositoryException; import com.openkm.db.bean.QueryParams; import com.openkm.ws.rest.util.SimpleNodeBaseResultSet; import net.xeoh.plugins.base.Plugin; import java.io.IOException; public interface NodeSearch extends Plugin { SimpleNodeBaseResultSet findSimpleNodeBasePaginated(QueryParams params, int offset, int limit) throws AccessDeniedException, RepositoryException, IOException, ParseException, DatabaseException; NodeSearchConfig getConfig() throws DatabaseException; String getName(); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.search.NodeSearch` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.search` | ### Methods | Method | Description | |---|---| | `findSimpleNodeBasePaginated(QueryParams params, int offset, int limit)` | Executes the search and returns a paginated result set. `offset` is the zero-based start index and `limit` is the maximum number of results to return. | | `getConfig()` | Returns a `NodeSearchConfig` that controls which search tabs and fields are visible in the UI. All fields default to hidden. | | `getName()` | Returns the display name shown in the search plugin selector. | ### NodeSearchConfig fields `NodeSearchConfig` (`com.openkm.bean.NodeSearchConfig`) controls search UI visibility. All fields are `Boolean`, defaulting to `false` (hidden). | Field | Description | |---|---| | `tabBasicVisible` | Show the Basic search tab | | `tabAdvancedVisible` | Show the Advanced search tab | | `tabMetadataVisible` | Show the Metadata search tab | | `tabLuceneQueryVisible` | Show the Lucene query tab | | `exportCsvVisible` | Show the Export CSV button | | `exportZipVisible` | Show the Export ZIP button | | `saveSearchVisible` | Show the Save search button | | `contextVisible` | Show the Context (search path) field | | `filePlanVisible` | Show the File plan field | | `contentVisible` | Show the Full-text content field | | `nameVisible` | Show the Name field | | `titleVisible` | Show the Title field | | `descriptionVisible` | Show the Description field | | `keywordsVisible` | Show the Keywords field | | `languageVisible` | Show the Language field | | `userVisible` | Show the Author field | | `dateVisible` | Show the Date range fields | | `folderVisible` | Show the Folder field | | `categoryVisible` | Show the Category field | | `typeVisible` | Show the Node type filter | | `docTypeVisible` | Show the Document type filter | | `mailFieldsVisible` | Show the Mail-specific fields | | `notesVisible` | Show the Notes field | ### Example — CustomSearch (delegates to the default search module with custom field config) ```java package com.openkm.plugin.search; import com.openkm.bean.NodeSearchConfig; import com.openkm.core.AccessDeniedException; import com.openkm.core.DatabaseException; import com.openkm.core.ParseException; import com.openkm.core.RepositoryException; import com.openkm.db.bean.QueryParams; import com.openkm.module.db.DbSearchModule; import com.openkm.plugin.BasePlugin; import com.openkm.ws.rest.util.SimpleNodeBaseResultSet; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @PluginImplementation public class CustomSearch extends BasePlugin implements NodeSearch { @Autowired private DbSearchModule dbSearchModule; @Override public SimpleNodeBaseResultSet findSimpleNodeBasePaginated(QueryParams params, int offset, int limit) throws AccessDeniedException, RepositoryException, IOException, ParseException, DatabaseException { return dbSearchModule.findSimpleNodeBasePaginated(params, offset, limit); } @Override public NodeSearchConfig getConfig() throws DatabaseException { NodeSearchConfig cfg = new NodeSearchConfig(); cfg.setTabBasicVisible(true); cfg.setSaveSearchVisible(true); cfg.setContextVisible(true); cfg.setContentVisible(true); cfg.setNameVisible(true); cfg.setDateVisible(true); return cfg; } @Override public String getName() { return "Custom Search"; } } ``` --- ## Suggestion plugin ### Interface ``` com.openkm.plugin.form.suggestion.Suggestion ``` A `Suggestion` plugin provides server-side autocomplete suggestions for a `Select` metadata form field. When a user types in a `Select` field that is configured to use this plugin, OpenKM calls `getSuggestions` and displays the returned list as selectable options. The `Select` form element type is documented in `llms-full-metadata-*.txt`. ```java package com.openkm.plugin.form.suggestion; import com.openkm.bean.form.Select; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import net.xeoh.plugins.base.Plugin; import java.util.List; public interface Suggestion extends Plugin { List getSuggestions(String nodeUuid, String nodePath, Select sel) throws PathNotFoundException, SuggestionException, DatabaseException; } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.form.suggestion.Suggestion` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.form.suggestion` | ### Methods | Method | Description | |---|---| | `getSuggestions(String nodeUuid, String nodePath, Select sel)` | Returns the list of suggested values to display. `nodeUuid` and `nodePath` identify the node being edited. `sel` is the `Select` form element, which may carry a value already typed by the user. Throw `SuggestionException` for domain errors; `PathNotFoundException` or `DatabaseException` for infrastructure errors. | ### Example — KeywordSuggestion (returns existing keywords from the repository) ```java package com.openkm.plugin.form.suggestion; import com.openkm.bean.form.Select; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.db.service.NodeBaseSrv; import com.openkm.plugin.BasePlugin; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @PluginImplementation public class KeywordSuggestion extends BasePlugin implements Suggestion { @Autowired private NodeBaseSrv nodeBaseSrv; @Override public List getSuggestions(String nodeUuid, String nodePath, Select sel) throws PathNotFoundException, SuggestionException, DatabaseException { // Return the top 10 most-used keywords that start with the current input value. String prefix = sel.getValue() != null ? sel.getValue().trim() : ""; return nodeBaseSrv.findTopKeywords(prefix, 10); } } ``` --- ## Security Access Manager plugin ### Interface ``` com.openkm.plugin.access.DbAccessManager ``` A `DbAccessManager` plugin implements the permission-checking logic for repository nodes. It controls whether a given user (identified by username and roles) is granted a specific permission on a node. ```java package com.openkm.plugin.access; import com.openkm.core.AccessDeniedException; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.db.bean.NodeBase; import com.openkm.principal.AuthException; import net.xeoh.plugins.base.Plugin; import java.util.Set; public interface DbAccessManager extends Plugin { void checkPermission(NodeBase node, int permissions) throws AccessDeniedException, PathNotFoundException, DatabaseException; boolean isGranted(NodeBase node, int permissions) throws DatabaseException, PathNotFoundException; boolean isGranted(NodeBase node, String user, int permissions) throws AuthException, DatabaseException, PathNotFoundException; boolean isGranted(NodeBase node, String user, Set roles, int permissions) throws AuthException, DatabaseException, PathNotFoundException; boolean isGranted(NodeBase node, long nodeClass, int permissions) throws DatabaseException; } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.access.DbAccessManager` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.access` | ### Methods | Method | Description | |---|---| | `checkPermission(NodeBase node, int permissions)` | Checks the permission for the currently authenticated user and throws `AccessDeniedException` if it is not granted. | | `isGranted(NodeBase node, int permissions)` | Returns `true` if the currently authenticated user has the requested permission on the node. | | `isGranted(NodeBase node, String user, int permissions)` | Returns `true` if the specified user has the requested permission. | | `isGranted(NodeBase node, String user, Set roles, int permissions)` | Returns `true` if the specified user with the given role set has the requested permission. | | `isGranted(NodeBase node, long nodeClass, int permissions)` | Returns `true` if the current user has the requested permission considering the file-plan node class access manager for the given `nodeClass`. | ### Built-in implementations | Class | Description | |---|---| | `DbSimpleAccessManager` | Checks permissions directly on the node's user/role permission maps. Admin user and admin role always have full access. | | `DbReadRecursiveAccessManager` | For read operations, walks up the node tree recursively until a granting permission is found. | | `DbRecursiveAccessManager` | Recursively evaluates permissions up the node hierarchy for all operation types. | ### Example — AllowAllAccessManager (grants all permissions — for testing only) ```java package com.openkm.plugin.access; import com.openkm.core.AccessDeniedException; import com.openkm.core.DatabaseException; import com.openkm.core.PathNotFoundException; import com.openkm.db.bean.NodeBase; import com.openkm.plugin.BasePlugin; import com.openkm.principal.AuthException; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.util.Set; @PluginImplementation public class AllowAllAccessManager extends BasePlugin implements DbAccessManager { @Override public void checkPermission(NodeBase node, int permissions) throws AccessDeniedException, PathNotFoundException, DatabaseException { // always granted — do nothing } @Override public boolean isGranted(NodeBase node, int permissions) throws DatabaseException, PathNotFoundException { return true; } @Override public boolean isGranted(NodeBase node, String user, int permissions) throws AuthException, DatabaseException, PathNotFoundException { return true; } @Override public boolean isGranted(NodeBase node, String user, Set roles, int permissions) throws AuthException, DatabaseException, PathNotFoundException { return true; } @Override public boolean isGranted(NodeBase node, long nodeClass, int permissions) throws DatabaseException { return true; } } ``` --- ## Version Number Adapter plugin ### Interface ``` com.openkm.plugin.vernum.VersionNumerationAdapter ``` A `VersionNumerationAdapter` plugin defines the version numbering scheme used when creating or updating document versions. It controls the initial version number, which UI increment options are shown to the user, and how the next version number is calculated. ```java package com.openkm.plugin.vernum; import com.openkm.db.bean.NodeVersion; import net.xeoh.plugins.base.Plugin; public interface VersionNumerationAdapter extends Plugin { int INCREASE_DEFAULT = 0; int INCREASE_MINOR = 1; int INCREASE_MAJOR = 2; int INCREASE_MAJOR_MINOR = 3; String getInitialVersionNumber(); int getVersionNumberIncrement(); String getNextVersionNumber(NodeVersion nVer, int increment); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.vernum.VersionNumerationAdapter` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.vernum` | ### Constants | Constant | Value | Description | |---|---|---| | `INCREASE_DEFAULT` | `0` | No UI option shown; always increment by the default step. | | `INCREASE_MINOR` | `1` | Show a "minor version" increment option in the UI. | | `INCREASE_MAJOR` | `2` | Show a "major version" increment option in the UI. | | `INCREASE_MAJOR_MINOR` | `3` | Show both major and minor increment options in the UI. | ### Methods | Method | Description | |---|---| | `getInitialVersionNumber()` | Returns the version number string to assign to the first version of a new document (e.g., `"1"`, `"1.0"`, `"1.0.0"`). | | `getVersionNumberIncrement()` | Returns one of the `INCREASE_*` constants. Controls which increment options the UI displays when the user creates a new version. | | `getNextVersionNumber(NodeVersion nVer, int increment)` | Given the current version (`nVer`) and the chosen `increment` type, calculates and returns the next version number string. Must skip any version numbers already in use. | ### Built-in implementations | Class | Initial version | Increment style | |---|---|---| | `PlainVersionNumerationAdapter` | `"1"` | Sequential integers (`1`, `2`, `3`, …) | | `MajorMinorVersionNumerationAdapter` | `"1.0"` | Major.Minor (`1.0`, `1.1`, `2.0`, …) | | `MajorMinorReleaseVersionNumerationAdapter` | `"1.0.0"` | Major.Minor.Release (`1.0.0`, `1.0.1`, `1.1.0`, …) | | `BranchVersionNumerationAdapter` | `"1.0"` | Branch-aware major.minor | ### Example — MajorMinorVersionNumerationAdapter ```java package com.openkm.plugin.vernum; import com.openkm.db.bean.NodeVersion; import com.openkm.db.repository.NodeVersionRepo; import com.openkm.plugin.BasePlugin; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; @PluginImplementation public class MajorMinorVersionNumerationAdapter extends BasePlugin implements VersionNumerationAdapter { @Autowired private NodeVersionRepo nodeVersionRepo; @Override public String getInitialVersionNumber() { return "1.0"; } @Override public int getVersionNumberIncrement() { return INCREASE_MAJOR; } @Override public String getNextVersionNumber(NodeVersion nVer, int increment) { String[] ver = nVer.getName().split("\\."); int major = Integer.parseInt(ver[0]); int minor = Integer.parseInt(ver[1]); NodeVersion nv; do { if (increment == INCREASE_MAJOR) { major++; minor = 0; } else { minor++; } nv = nodeVersionRepo.findByParentAndName(nVer.getParent(), major + "." + minor); } while (nv != null); return major + "." + minor; } } ``` --- ## AI Prompt plugin ### Interface ``` com.openkm.plugin.ai.AIPrompt ``` An `AIPrompt` plugin wraps a Large Language Model (LLM) interaction. It can process a repository node (by UUID) or a plain text string, and returns an `AIResult` containing the LLM response. The plugin's content type filter and mode flags let each implementation declare what it supports. ```java package com.openkm.plugin.ai; import com.openkm.ws.common.util.AIResult; import net.xeoh.plugins.base.Plugin; public interface AIPrompt extends Plugin { String getName(); AIResult executeNodePrompt(String uuid) throws AIPromptException; AIResult executeTextPrompt(String text) throws AIPromptException; String[] getContentTypes(); boolean isManageNodePrompt(); boolean isManageTextPrompt(); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.ai.AIPrompt` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.ai` | ### Methods | Method | Description | |---|---| | `getName()` | Returns the display name shown in the AI prompt selector. | | `executeNodePrompt(String uuid)` | Executes the prompt against the repository node identified by `uuid`. Fetch the node content or metadata inside this method as needed. | | `executeTextPrompt(String text)` | Executes the prompt against the provided text string. | | `getContentTypes()` | Returns an array of MIME types this plugin supports (e.g., `new String[]{"application/pdf", "image/jpeg"}`). Return an empty array to accept all MIME types. | | `isManageNodePrompt()` | Return `true` to enable the node-based prompt entry point in the UI. | | `isManageTextPrompt()` | Return `true` to enable the text-based prompt entry point in the UI. | ### AIResult fields `AIResult` (`com.openkm.ws.common.util.AIResult`) carries the response from the LLM. | Setter | Description | |---|---| | `setText(String)` | Plain-text output from the LLM. | | `setHtml(String)` | HTML-formatted output. | | `setError(String)` | Human-readable error message if the prompt failed. | | `setErrorCode(String)` | Machine-readable error code: `TEXT_EMPTY` (no content to process), `OTHER` (unexpected failure). | ### Example — TextSummarizer (summarises a node's extracted text) ```java package com.openkm.plugin.ai; import com.openkm.db.service.NodeDocumentSrv; import com.openkm.plugin.BasePlugin; import com.openkm.ws.common.util.AIResult; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; @PluginImplementation public class TextSummarizer extends BasePlugin implements AIPrompt { @Autowired private NodeDocumentSrv nodeDocumentSrv; // @Autowired private OpenAIUtils openAIUtils; // inject your LLM utility @Override public String getName() { return "Text Summarizer"; } @Override public boolean isManageNodePrompt() { return true; } @Override public boolean isManageTextPrompt() { return true; } @Override public String[] getContentTypes() { return new String[]{}; // accept all MIME types } @Override public AIResult executeNodePrompt(String uuid) throws AIPromptException { try { String extractedText = nodeDocumentSrv.getExtractedText(uuid); if (extractedText == null || extractedText.isBlank()) { AIResult result = new AIResult(); result.setErrorCode("TEXT_EMPTY"); result.setError("No extracted text available for this document."); return result; } return executeTextPrompt(extractedText); } catch (Exception e) { throw new AIPromptException("executeNodePrompt failed: " + e.getMessage(), e); } } @Override public AIResult executeTextPrompt(String text) throws AIPromptException { try { // String summary = openAIUtils.processText("Summarise the following text:\n" + text); String summary = "Summary placeholder for: " + text.substring(0, Math.min(50, text.length())); AIResult result = new AIResult(); result.setText(summary); return result; } catch (Exception e) { throw new AIPromptException("executeTextPrompt failed: " + e.getMessage(), e); } } } ``` --- ## Antivirus plugin ### Interface ``` com.openkm.plugin.antivirus.Antivirus ``` An `Antivirus` plugin scans a file for viruses before it is stored in the repository. If a virus is detected, return a non-empty string describing the infection; return `null` or an empty string if the file is clean. OpenKM rejects the upload when the return value is non-empty. This plugin type is **disabled by default**. Enable it from **Administration > Utilities > Plugins** after deployment, and configure the antivirus command in **Administration > Configuration > System** (`system.antivir`). ```java package com.openkm.plugin.antivirus; import net.xeoh.plugins.base.Plugin; import java.io.File; public interface Antivirus extends Plugin { String detect(File file); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.antivirus.Antivirus` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.antivirus` | | Enabled by default | No — must be activated manually | ### Methods | Method | Description | |---|---| | `detect(File file)` | Scans `file` for viruses. Return `null` or an empty string if the file is clean. Return a non-empty description string if a virus is found. Returning `"Failed to check for viruses"` (or any non-empty string) also blocks the upload. | ### Built-in implementation — ClamavAntivirus `ClamavAntivirus` invokes the `clamscan` command-line tool configured in `Config.SYSTEM_ANTIVIR`. It captures the scanner output and returns the virus description when `clamscan` exits with code `1`. ### Example — ClamavAntivirus ```java package com.openkm.plugin.antivirus; import com.openkm.core.Config; import com.openkm.plugin.BasePlugin; import lombok.extern.slf4j.Slf4j; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.apache.commons.io.IOUtils; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; @Slf4j @PluginImplementation public class ClamavAntivirus extends BasePlugin implements Antivirus { @Override public String detect(File file) { try { log.debug("CMD: {} {}", Config.SYSTEM_ANTIVIR, file.getPath()); ProcessBuilder pb = new ProcessBuilder(Config.SYSTEM_ANTIVIR, "--no-summary", file.getPath()); Process process = pb.start(); process.waitFor(); String info = IOUtils.toString(process.getInputStream(), Charset.defaultCharset()); process.destroy(); if (process.exitValue() == 1) { log.warn(info); return info.substring(info.indexOf(':') + 1); } else { return null; } } catch (InterruptedException | IOException e) { log.warn("Failed to check for viruses", e); } return "Failed to check for viruses"; } } ``` --- ## OCR Data Capture plugin ### Interface ``` com.openkm.plugin.ocr.template.OCRTemplateParser ``` An `OCRTemplateParser` plugin extracts a typed value from raw OCR text for a single `OCRTemplateField`. It is used in the OCR Zone feature to populate metadata fields automatically. The plugin receives the field configuration (including an optional regex pattern) and the OCR text, and returns the extracted value. ```java package com.openkm.plugin.ocr.template; import com.openkm.db.bean.OCRTemplateField; import net.xeoh.plugins.base.Plugin; public interface OCRTemplateParser extends Plugin { Object parse(OCRTemplateField otf, String text) throws OCRTemplateException, OCRParserEmptyValueException; String getName(); boolean isPatternRequired(); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.ocr.template.OCRTemplateParser` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.ocr.template.parser` | ### Methods | Method | Description | |---|---| | `parse(OCRTemplateField otf, String text)` | Extracts and returns the captured value. The return type may be `String`, `Calendar` (for ISO 8601 dates), or another type that will be converted to a string. Throw `OCRParserEmptyValueException` if the input text is empty. Throw `OCRTemplateException` if the text does not match the expected format. | | `getName()` | Returns the display name shown in the parser selector in the OCR template editor. | | `isPatternRequired()` | Return `true` if the regex pattern field is mandatory for this parser. | ### OCRTemplateField fields | Field | Description | |---|---| | `getPattern()` | Optional regex pattern string. Empty or null means no pattern is applied. | | `getValue()` | An expected or reference value, used by some parsers for comparison. | ### Built-in implementations | Class | Description | |---|---| | `StringParser` | Returns the raw trimmed text, or the first capture group if a pattern is provided. | | `NumberParser` | Parses and returns a numeric value. | | `DateParser` | Parses a date string and returns a `Calendar`. | | `BarcodeParser` | Extracts a barcode value from the text. | ### Example — StringParser ```java package com.openkm.plugin.ocr.template.parser; import com.openkm.db.bean.OCRTemplateField; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.ocr.template.OCRParserEmptyValueException; import com.openkm.plugin.ocr.template.OCRTemplateException; import com.openkm.plugin.ocr.template.OCRTemplateParser; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.util.regex.Matcher; import java.util.regex.Pattern; @PluginImplementation public class StringParser extends BasePlugin implements OCRTemplateParser { @Override public Object parse(OCRTemplateField otf, String text) throws OCRTemplateException, OCRParserEmptyValueException { if (text == null || text.isEmpty()) { throw new OCRParserEmptyValueException("Empty value"); } if (otf.getPattern() == null || otf.getPattern().isEmpty()) { return text.trim(); } else { Pattern pattern = Pattern.compile("(" + otf.getPattern() + ")", Pattern.UNICODE_CASE); Matcher matcher = pattern.matcher(text); if (matcher.find() && matcher.groupCount() == 1) { return matcher.group(); } else { throw new OCRTemplateException("Bad format, parse exception"); } } } @Override public String getName() { return "String"; } @Override public boolean isPatternRequired() { return false; } } ``` --- ## OCR Validation Control plugin ### Interface ``` com.openkm.plugin.ocr.template.OCRTemplateControlParser ``` An `OCRTemplateControlParser` plugin validates the raw OCR text for an `OCRTemplateControlField`. It acts as a document identification checkpoint: if `parse` returns `false` for the control field, the OCR template is considered non-matching and the document is not processed with that template. ```java package com.openkm.plugin.ocr.template; import com.openkm.db.bean.OCRTemplateControlField; import net.xeoh.plugins.base.Plugin; public interface OCRTemplateControlParser extends Plugin { boolean parse(OCRTemplateControlField otcf, String text) throws OCRTemplateException, OCRParserEmptyValueException; String getName(); boolean isPatternRequired(); String info(); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.ocr.template.OCRTemplateControlParser` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.ocr.template.controlparser` | ### Methods | Method | Description | |---|---| | `parse(OCRTemplateControlField otcf, String text)` | Returns `true` if the OCR text satisfies the validation condition for the control field. Return `false` to reject this template for the document. | | `getName()` | Returns the display name shown in the validator selector. | | `isPatternRequired()` | Return `true` if the regex pattern field is mandatory. | | `info()` | Returns a multi-line description of the validation logic shown in the administration UI. | ### OCRTemplateControlField fields | Field | Description | |---|---| | `getPattern()` | Optional regex pattern. | | `getValue()` | Expected reference value used for equality or similarity checks. | ### Built-in implementations | Class | Description | |---|---| | `StringEqualParser` | Returns `true` if the text equals the field value. With a pattern: true if the pattern is found (and equals `value` if value is non-empty). | | `StringContainsParser` | Returns `true` if the text contains the field value or the pattern match. | | `StringSimilarParser` | Returns `true` if the Levenshtein distance between the extracted text and the field value is within a configurable threshold. | ### Example — StringEqualParser ```java package com.openkm.plugin.ocr.template.controlparser; import com.openkm.db.bean.OCRTemplateControlField; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.ocr.template.OCRParserEmptyValueException; import com.openkm.plugin.ocr.template.OCRTemplateControlParser; import com.openkm.plugin.ocr.template.OCRTemplateException; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.util.regex.Matcher; import java.util.regex.Pattern; @PluginImplementation public class StringEqualParser extends BasePlugin implements OCRTemplateControlParser { @Override public boolean parse(OCRTemplateControlField otf, String text) throws OCRTemplateException, OCRParserEmptyValueException { if (text == null || text.isEmpty()) { return false; } if (otf.getPattern() == null || otf.getPattern().isEmpty()) { return text.equals(otf.getValue()); } else { Pattern pattern = Pattern.compile("(" + otf.getPattern() + ")", Pattern.UNICODE_CASE); Matcher matcher = pattern.matcher(text); if (matcher.find() && matcher.groupCount() == 1) { return otf.getValue().isEmpty() || otf.getValue().equals(matcher.group()); } else { return false; } } } @Override public String getName() { return "String equals"; } @Override public boolean isPatternRequired() { return false; } @Override public String info() { return "Empty pattern case: true if extracted text equals field value.\n" + "Pattern with value field empty: true if pattern found.\n" + "Pattern with value field: true if pattern found equals value.\n"; } } ``` --- ## Form plugin group The form plugin group customises the behaviour of metadata property group forms: how fields are validated, how default values are populated, how options and suggestions are sourced, and how the read/write lifecycle is intercepted. Each plugin type is referenced directly from the metadata XML definition (see `llms-full-metadata-*.txt`). | Plugin interface | XML reference point | Purpose | |---|---|---| | `FieldValidator` | `` inside a field | Validates a single field value | | `FormValidator` | `validatorClassName` on `` | Validates the entire group on submit | | `FormInterceptor` | `interceptorClassName` on `` | Intercepts add / get / set lifecycle calls | | `FormDefaultValues` | `defaultValueClassName` on `` | Supplies initial field values on form open | | `AutocompleteFormValues` | `autocompleteValueClassName` on `` | Autocompletes multiple fields at once (e.g. via AI) | | `OptionSelectValues` | `className` on ` ``` ### Example — UniqueFieldValidator (rejects duplicate values across nodes) ```java package com.example.plugin; import com.openkm.db.service.LegacySrv; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.form.validator.FieldValidator; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @PluginImplementation public class UniqueFieldValidator extends BasePlugin implements FieldValidator { @Autowired private LegacySrv legacySrv; @Override public String getName() { return "Unique field validator"; } @Override public String validate(String value, List uuids) { if (value == null || value.isBlank()) { return null; // let handle empty check } try { List> rows = legacySrv.executeSQL( "SELECT COUNT(*) FROM OKM_PGRP_CUR_COMPANY WHERE RGT_TAX_ID = ?", value); int count = Integer.parseInt(rows.get(0).get(0)); if (count > 0) { return "This Tax ID is already registered."; } } catch (Exception e) { return "Validation error: " + e.getMessage(); } return null; } } ``` --- ## FormValidator plugin ### Interface ``` com.openkm.plugin.form.validator.FormValidator ``` A `FormValidator` plugin validates the entire property group when the user submits the form. It receives all current field values as a map and can enforce cross-field rules. Referenced via the `validatorClassName` attribute of ``. ```java package com.openkm.plugin.form.validator; import com.openkm.core.ValidationFormException; import com.openkm.db.bean.NodeBase; import net.xeoh.plugins.base.Plugin; import java.util.Map; public interface FormValidator extends Plugin { void validate(NodeBase node, String grpName, Map properties, boolean fullValidation) throws ValidationFormException; String getName(); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.form.validator.FormValidator` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.form.validator` | ### Methods | Method | Description | |---|---| | `getName()` | Returns the display name shown in the plugin list. | | `validate(NodeBase node, String grpName, Map properties, boolean fullValidation)` | Called when the group form is submitted. `node` is the repository node being edited. `grpName` is the property group name (e.g. `okg:consulting`). `properties` is the map of field names to their submitted string values. `fullValidation` is `true` on a final submit and `false` during intermediate saves. Throw `ValidationFormException` with an error message to reject the submission. | ### XML reference ```xml ``` ### Example — ContractValidator (end date must be after start date) ```java package com.example.plugin; import com.openkm.core.ValidationFormException; import com.openkm.db.bean.NodeBase; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.form.validator.FormValidator; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Map; @PluginImplementation public class ContractValidator extends BasePlugin implements FormValidator { private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); @Override public String getName() { return "Contract date validator"; } @Override public void validate(NodeBase node, String grpName, Map properties, boolean fullValidation) throws ValidationFormException { String start = properties.get("okp:contract.start_date"); String end = properties.get("okp:contract.end_date"); if (start != null && !start.isBlank() && end != null && !end.isBlank()) { LocalDate startDate = LocalDate.parse(start, FMT); LocalDate endDate = LocalDate.parse(end, FMT); if (!endDate.isAfter(startDate)) { throw new ValidationFormException("End date must be after start date."); } } } } ``` --- ## FormInterceptor plugin ### Interface ``` com.openkm.plugin.form.interceptor.FormInterceptor ``` A `FormInterceptor` plugin intercepts the three lifecycle operations of a property group: adding metadata to a node (`add`), reading metadata for display (`get`), and saving metadata changes (`set`). Referenced via the `interceptorClassName` attribute of ``. ```java package com.openkm.plugin.form.interceptor; import com.openkm.bean.form.FormElement; import com.openkm.core.ValidationFormException; import net.xeoh.plugins.base.Plugin; import java.util.List; import java.util.Map; public interface FormInterceptor extends Plugin { String getName(); void add(String uuid, String grpName, Map properties) throws ValidationFormException; void get(String uuid, String grpName, Map properties, List formElements, boolean useDefaultValue, boolean autocomplete); void set(String uuid, String grpName, Map properties) throws ValidationFormException; } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.form.interceptor.FormInterceptor` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.form.interceptor` | ### Methods | Method | Description | |---|---| | `getName()` | Returns the display name shown in the plugin list. | | `add(String uuid, String grpName, Map properties)` | Called when a property group is first assigned to a node. `uuid` is the node UUID. `properties` contains the initial field values. Modify `properties` in-place to adjust values before they are persisted. Throw `ValidationFormException` to abort. | | `get(String uuid, String grpName, Map properties, List formElements, boolean useDefaultValue, boolean autocomplete)` | Called when a property group is read for display. `properties` holds the stored values. `formElements` is the live list of form element beans — modify them (e.g. set visibility, inject computed values) to alter what the UI renders. | | `set(String uuid, String grpName, Map properties)` | Called when the user saves changes to the property group. `properties` contains the submitted field values. Modify `properties` in-place or throw `ValidationFormException` to abort the save. | ### XML reference ```xml ``` ### Example — AuditInterceptor (logs every save with user and timestamp) ```java package com.example.plugin; import com.openkm.bean.form.FormElement; import com.openkm.core.ValidationFormException; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.form.interceptor.FormInterceptor; import com.openkm.principal.PrincipalUtils; import lombok.extern.slf4j.Slf4j; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.time.Instant; import java.util.List; import java.util.Map; @Slf4j @PluginImplementation public class AuditInterceptor extends BasePlugin implements FormInterceptor { @Override public String getName() { return "Audit interceptor"; } @Override public void add(String uuid, String grpName, Map properties) throws ValidationFormException { log.info("Group {} added to node {} by {} at {}", grpName, uuid, PrincipalUtils.getUser(), Instant.now()); } @Override public void get(String uuid, String grpName, Map properties, List formElements, boolean useDefaultValue, boolean autocomplete) { // read-only: nothing to modify } @Override public void set(String uuid, String grpName, Map properties) throws ValidationFormException { log.info("Group {} saved on node {} by {} at {}", grpName, uuid, PrincipalUtils.getUser(), Instant.now()); } } ``` --- ## FormDefaultValues plugin ### Interface ``` com.openkm.plugin.form.values.FormDefaultValues ``` A `FormDefaultValues` plugin populates the initial values of a property group's form elements when the group is first opened or assigned to a node. Referenced via the `defaultValueClassName` attribute of ``. ```java package com.openkm.plugin.form.values; import com.openkm.bean.form.FormElement; import net.xeoh.plugins.base.Plugin; import java.util.List; public interface FormDefaultValues extends Plugin { String getName(); void setDefaultValues(String uuid, String grpName, List formElementList); } ``` ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.form.values.FormDefaultValues` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.form.values` | ### Methods | Method | Description | |---|---| | `getName()` | Returns the display name shown in the plugin list. | | `setDefaultValues(String uuid, String grpName, List formElementList)` | Called when the form is loaded for a new or unset group. `uuid` is the node UUID. `formElementList` is the list of live form element beans. Cast each element to its concrete type (`Input`, `Select`, `CheckBox`, `TextArea`, `SuggestBox`) and call the appropriate setter to inject a default value. | ### FormElement types and value setters | Bean class | Value setter | Notes | |---|---|---| | `Input` | `setValue(String)` | Date values use `yyyyMMddHHmmss` format. | | `CheckBox` | `setValue(boolean)` | | | `TextArea` | `setValue(String)` | | | `SuggestBox` | `setValue(String)` | Sets the stored key value. | | `Select` | `setValue(String)` | For `TYPE_MULTIPLE`, separate values with `;`. | | `HorizontalPanel` | `getElements()` → recurse | | | `VerticalPanel` | `getElements()` → recurse | | ### XML reference ```xml ``` ### Example — InvoiceDefaults (sets currency to EUR and date to today) ```java package com.example.plugin; import com.openkm.bean.form.FormElement; import com.openkm.bean.form.Input; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.form.values.FormDefaultValues; import net.xeoh.plugins.base.annotations.PluginImplementation; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; @PluginImplementation public class InvoiceDefaults extends BasePlugin implements FormDefaultValues { private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); @Override public String getName() { return "Invoice default values"; } @Override public void setDefaultValues(String uuid, String grpName, List formElementList) { for (FormElement element : formElementList) { if (element instanceof Input input) { if ("okp:invoice.currency".equals(input.getName())) { input.setValue("EUR"); } else if ("okp:invoice.date".equals(input.getName())) { input.setValue(LocalDateTime.now().format(FMT)); } } } } } ``` --- ## AutocompleteFormValues plugin ### Interface ``` com.openkm.plugin.form.values.AutocompleteFormValues ``` An `AutocompleteFormValues` plugin populates multiple form fields at once, typically using an external service or AI. It is triggered programmatically (e.g. when a user clicks an "Autocomplete" button) rather than on every field interaction. Referenced via the `autocompleteValueClassName` attribute of ``. ```java package com.openkm.plugin.form.values; import com.openkm.bean.form.FormElement; import net.xeoh.plugins.base.Plugin; import java.util.List; import java.util.Map; public interface AutocompleteFormValues extends Plugin { String getName(); void autocomplete(String uuid, String grpName, List formElementList); } ``` The interface also provides a built-in `default` helper method: ```java default void autocompleteHelper(List formElementList, Map props) ``` This helper iterates `formElementList` (recursing into `HorizontalPanel` and `VerticalPanel`) and sets each element's value from the `props` map, handling the correct type casting and multi-value serialisation for `Select` fields. ### Requirements | Requirement | Value | |---|---| | Implement interface | `com.openkm.plugin.form.values.AutocompleteFormValues` | | Extend | `BasePlugin` | | Annotation | `@PluginImplementation` | | Package (recommended) | `com.openkm.plugin.form.values` | ### Methods | Method | Description | |---|---| | `getName()` | Returns the display name shown in the plugin list. | | `autocomplete(String uuid, String grpName, List formElementList)` | Called to fill in form fields automatically. `uuid` is the node UUID. `formElementList` is the list of live form element beans. Call `autocompleteHelper(formElementList, props)` with a `Map` of field names to values to apply them in bulk, or set individual elements manually. | ### XML reference ```xml ``` ### Example — ContractAutocomplete (extracts fields from document text using AI) ```java package com.example.plugin; import com.openkm.bean.form.FormElement; import com.openkm.db.service.NodeDocumentSrv; import com.openkm.plugin.BasePlugin; import com.openkm.plugin.form.values.AutocompleteFormValues; import net.xeoh.plugins.base.annotations.PluginImplementation; import org.springframework.beans.factory.annotation.Autowired; import java.util.HashMap; import java.util.List; import java.util.Map; @PluginImplementation public class ContractAutocomplete extends BasePlugin implements AutocompleteFormValues { @Autowired private NodeDocumentSrv nodeDocumentSrv; // @Autowired private OpenAIUtils openAIUtils; @Override public String getName() { return "Contract autocomplete"; } @Override public void autocomplete(String uuid, String grpName, List formElementList) { try { String text = nodeDocumentSrv.getExtractedText(uuid); // Map extracted = openAIUtils.extractFields(text, formElementList); // Placeholder: populate a fixed map for illustration Map extracted = new HashMap<>(); extracted.put("okp:contract.customer", "Acme Corp"); extracted.put("okp:contract.amount", "12500.00"); autocompleteHelper(formElementList, extracted); } catch (Exception e) { // log and leave fields unchanged } } } ``` --- ## OptionSelectValues plugin ### Interface ``` com.openkm.plugin.form.values.OptionSelectValues ``` An `OptionSelectValues` plugin supplies the option list for a ``. ```java package com.openkm.plugin.form.values; import com.openkm.bean.form.Option; import net.xeoh.plugins.base.Plugin; import java.util.List; public interface OptionSelectValues extends Plugin { String getName(); List