# SDK for Java (sdk4j) - OpenKM

> This document is intended for LLM consumption. It consolidates the complete documentation for sdk4j, the official Java SDK for the OpenKM REST API.
> Source: https://docs.openkm.com/kcenter/view/sdk4j-5.4/
> Compatibility: sdk4j 5.4 requires OpenKM Professional 8.2.4 or newer.

---

## Overview

The OpenKM SDK for Java (sdk4j) is a comprehensive toolkit enabling developers to build applications integrated with OpenKM. It wraps the full OpenKM REST API, providing type-safe Java objects, authentication handling, and broad compatibility across OpenKM REST versions while minimising code modifications.

**License**: EULA - OpenKM SDK End User License Agreement, published by OpenKM Knowledge Management System S.L.

---

## Maven Setup

Add the OpenKM Maven repository and the sdk4j dependency to your `pom.xml`:

```xml
<repositories>
  <repository>
    <id>openkm.com</id>
    <name>OpenKM Maven Repository</name>
    <url>https://maven.openkm.com</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>com.openkm</groupId>
    <artifactId>sdk4j</artifactId>
    <version>5.4</version>
  </dependency>
</dependencies>
```

### Packaging: maven-shade-plugin

When building an executable JAR, use `maven-shade-plugin` (not `maven-assembly-plugin`) to avoid a "missing MessageBodyWriter" error. Include the following transformer:

```xml
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
  <resource>META-INF/services/javax.ws.rs.ext.MessageBodyWriter</resource>
</transformer>
```

---

## Package Structure

| Package | Description |
|---------|-------------|
| `com.openkm.sdk4j` | Factory class `OKMWebservicesFactory` that returns an `OKMWebservices` instance |
| `com.openkm.sdk4j.bean` | Data transfer objects (POJOs) — unmarshalled REST response objects |
| `com.openkm.sdk4j.definition` | Interface definitions (`BaseActivity`, `BaseAuth`, etc.) |
| `com.openkm.sdk4j.impl` | Concrete implementations (`ActivityImpl`, `AuthImpl`, etc.) |
| `com.openkm.sdk4j.util` | Utilities, including ISO8601 date parsing |
| `com.openkm.sdk4j.exception` | Exception classes |

---

## Initialization and Authentication

### Creating a client instance

```java
import com.openkm.sdk4j.OKMWebservicesFactory;
import com.openkm.sdk4j.impl.OKMWebservices;

String host = "http://localhost:8080/openkm";
OKMWebservices ws = OKMWebservicesFactory.getInstance(host);
```

`OKMWebservicesFactory.getInstance(String host)` is a static factory method that creates and returns a new `OKMWebservices` object configured for the given host URL.

### Login with username and password

```java
ws.login(user, password);
```

### Login with username, password, expiration and IP restriction

```java
ws.login(user, password, expiration, restrictIp);
```

### Login with a Personal Access Token

```java
ws.login(personalAccessToken);
```

All `login()` variants obtain an authorization token from the server and automatically propagate it to every service implementation inside the `OKMWebservices` object. The token is stored internally; you do not need to manage it manually.

### Logout

```java
ws.logout();
```

Invalidates the session and resets the internal authorization token to `null`.

### Refresh token

```java
String newToken = ws.getRefreshToken();
```

Retrieves a refreshed token and updates all service implementations automatically.

### Setting authorization token manually

```java
ws.setAuthorizationToken(authorizationToken);
```

Useful when reusing a previously obtained token across sessions.

---

## OKMWebservices — Service Registry

`OKMWebservices` is the central object. It holds one public field per service area. All fields are initialized in the constructor.

```java
public class OKMWebservices implements Serializable {
    public AuthImpl          auth;
    public CronTabImpl       cronTab;
    public FolderImpl        folder;
    public PropertyGroupImpl propertyGroup;
    public RepositoryImpl    repository;
    public ActivityImpl      activity;
    public BookmarkImpl      bookmark;
    public ConversionImpl    conversion;
    public DashboardImpl     dashboard;
    public ImportImpl        quickImport;
    public NoteImpl          note;
    public NotificationImpl  notification;
    public PropertyImpl      property;
    public ShardImpl         shard;
    public RecordImpl        record;
    public NodeImpl          node;
    public PdfImpl           pdf;
    public PluginImpl        plugin;
    public ReportImpl        report;
    public UserConfigImpl    userConfig;
    public StampImpl         stamp;
    public TaskImpl          task;
    public RelationImpl      relation;
    public AutomationImpl    automation;
    public OcrImpl           ocr;
    public SearchImpl        search;
    public DocumentImpl      document;
    public MailImpl          mail;
    public WorkflowImpl      workflow;
    public WizardImpl        wizard;
}
```

Access any service directly via the field, for example:

```java
ws.document.getProperties(uuid);
ws.folder.getChildren(parentUuid);
ws.auth.getGrantedUsers(nodeUuid);
```

---

## Service Areas

The following table lists all service areas exposed by the SDK:

| Field | Interface | Description |
|-------|-----------|-------------|
| `ws.activity` | `BaseActivity` | Activity log management |
| `ws.auth` | `BaseAuth` | Security, users, roles, grants |
| `ws.automation` | `BaseAutomation` | Automation rules management |
| `ws.bookmark` | `BaseBookmark` | User bookmarks |
| `ws.conversion` | `BaseConversion` | Document format conversion |
| `ws.cronTab` | `BaseCronTab` | Scheduled tasks (cron) |
| `ws.dashboard` | `BaseDashboard` | Dashboard data retrieval |
| `ws.document` | `BaseDocument` | Document CRUD, locking, versioning |
| `ws.folder` | `BaseFolder` | Folder management |
| `ws.quickImport` | `BaseImport` | Bulk import capabilities |
| `ws.mail` | `BaseMail` | Email/mail node management |
| `ws.node` | `BaseNode` | Generic node operations |
| `ws.note` | `BaseNote` | Notes attached to nodes |
| `ws.notification` | `BaseNotification` | Subscription and notification management |
| `ws.ocr` | `BaseOcr` | OCR processing |
| `ws.pdf` | `BasePdf` | PDF operations |
| `ws.plugin` | `BasePlugin` | Plugin management |
| `ws.property` | `BaseProperty` | Categories and keywords |
| `ws.propertyGroup` | `BasePropertyGroup` | Metadata / property groups |
| `ws.record` | `BaseRecord` | Records management |
| `ws.relation` | `BaseRelation` | Node relationships |
| `ws.report` | `BaseReport` | Report execution |
| `ws.repository` | `BaseRepository` | Repository-level properties and info |
| `ws.search` | `BaseSearch` | Full-text and parametric search |
| `ws.shard` | `BaseShard` | Shard management |
| `ws.stamp` | `BaseStamp` | Stamp management |
| `ws.task` | `BaseTask` | Task creation and tracking |
| `ws.userConfig` | `BaseUserConfig` | User configuration |
| `ws.wizard` | `BaseWizard` | Wizard actions |
| `ws.workflow` | `BaseWorkflow` | Workflow process execution |

---

## Quick Start Example

```java
package com.openkm;

import com.openkm.sdk4j.OKMWebservicesFactory;
import com.openkm.sdk4j.bean.Folder;
import com.openkm.sdk4j.impl.OKMWebservices;

public class QuickStart {
    public static void main(String[] args) {
        String host     = "http://localhost:8080/openkm";
        String user     = "okmAdmin";
        String password = "admin";

        OKMWebservices ws = OKMWebservicesFactory.getInstance(host);

        try {
            ws.login(user, password);

            // List children folders of a node by UUID
            for (Folder fld : ws.folder.getChildren("4f873d10-654e-4d99-a94f-15466e30a0f6")) {
                System.out.println("Folder -> " + fld.getPath());
            }

            // Get repository version info
            System.out.println(ws.repository.getAppVersion());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

---

---

## ws.activity — Activity Log

Activity logs track system events across documents, folders, mail items, and records.

### findActivityLog

```java
ActivityList findActivityLog(int page, int length, Calendar beginDate,
                             Calendar endDate, String user, String action, String item, String hostname)
```

Retrieves activity log entries filtered by date range, user, action type, node, and hostname.

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | int | Page number for pagination (0-based) |
| `length` | int | Number of records per page |
| `beginDate` | Calendar | Start date filter |
| `endDate` | Calendar | End date filter |
| `user` | String | Username filter |
| `action` | String | Action type filter; empty string returns all actions |
| `item` | String | UUID of the node (document, folder, mail, or record) |
| `hostname` | String | Hostname filter; empty string returns all hosts |

**Returns:** `ActivityList` — iterable via `getActivities()`

```java
ws.login(user, password);

Calendar beginDate = Calendar.getInstance();
beginDate.add(Calendar.MONTH, -1);
Calendar endDate = Calendar.getInstance();
String item = "f84a2e1f-a858-4e53-9c09-36519d903782";

ActivityList results = ws.activity.findActivityLog(0, 20, beginDate, endDate, user, "", item, "");
for (Activity activity : results.getActivities()) {
    System.out.println(activity);
}
```

### getActivityActions

```java
List<String> getActivityActions()
```

Returns all valid activity action type identifiers supported by the system.

```java
for (String action : ws.activity.getActivityActions()) {
    System.out.println(action);
}
```

---

## ws.auth — Authentication & Authorization

### Permission constants

`com.openkm.sdk4j.bean.Permission` provides permission bit constants. Combine with `+` or `|`:

```java
int permission = Permission.READ + Permission.WRITE;
// Check:
if ((permission | Permission.WRITE) == Permission.WRITE) { /* has WRITE */ }
```

### Authentication

| Method | Description |
|--------|-------------|
| `ws.login(user, password)` | Login; token valid 24 h by default |
| `ws.login(user, password, expiration, restrictIP)` | Login with custom expiration (days) and optional IP lock |
| `ws.setAuthorizationToken(token)` | Reuse a previously obtained token |
| `ws.logout()` | Server-side logout; resets internal token |
| `ws.getRefreshToken()` | Refresh current token before expiration |
| `ws.auth.setUserTenant(tenantId)` | Switch tenant in multi-tenant environments |
| `ws.auth.getToken()` | Generate a fresh token (resets 24 h expiration) |

> **Note:** The first successful `login()` creates the user's folder structure (`/okm:trash/userId`). Without it, path-based calls will throw `PathNotExistsException`.

### User management

```java
// List all users (including disabled)
List<CommonUser> users = ws.auth.getUsers(true);

// Get a single user
CommonUser u = ws.auth.getUser("okmAdmin");

// Users by role
List<CommonUser> admins = ws.auth.getUsersByRole("ROLE_ADMIN");

// Create user
ws.auth.createUser("test", "password.2016", "test@mail.com", "User test", true);

// Update user
ws.auth.updateUser("test", "newpassword", "test@mail.com", "Test", false);

// Delete user
ws.auth.deleteUser("test");

// Get token for a specific user
String token = ws.auth.getUserToken("okmAdmin");
```

### Role management

```java
// List all roles
List<String> roles = ws.auth.getRoles(true);

// Roles assigned to a user
List<String> userRoles = ws.auth.getRolesByUser("okmAdmin");

// Create / update / delete role
ws.auth.createRole("ROLE_TEST", true);
ws.auth.updateRole("ROLE_TEST", true);
ws.auth.deleteRole("ROLE_TEST");

// Assign / remove role from user
ws.auth.assignRole("test", "ROLE_USER");
ws.auth.removeRole("test", "ROLE_USER");
```

### Node permissions (grants)

```java
// Read grants on a node
GrantedUsersAndRolesItem grants = ws.auth.getGrantedUsersAndRoles(uuid);
Map<String, Integer> userGrants = ws.auth.getGrantedUsers(uuid);
Map<String, Integer> roleGrants = ws.auth.getGrantedRoles(uuid);

// Grant permissions
ws.auth.grantUser(uuid, "sochoa", Permission.ALL_GRANTS, false);
ws.auth.grantRole(uuid, "ROLE_USER", Permission.ALL_GRANTS, true); // recursive

// Revoke permissions
ws.auth.revokeUser(uuid, "sochoa", Permission.ALL_GRANTS, false);
ws.auth.revokeRole(uuid, "ROLE_USER", Permission.ALL_GRANTS, false);

// Set (overwrite) permissions for a user or role on a node
ws.auth.setUserPermissions(uuid, "sochoa", Permission.READ + Permission.WRITE, false);
ws.auth.setRolePermissions(uuid, "ROLE_USER", Permission.READ + Permission.WRITE, false);
```

> The `recursive` flag only applies to folders and record nodes.

### Batch security changes

**`changeSecurity`** — adds grants (does not remove existing ones):

```java
List<GrantedUser> guList = new ArrayList<>();
GrantedUser gu = new GrantedUser();
gu.setUser("sochoa");
gu.setPermissions(Permission.ALL_GRANTS);
guList.add(gu);

List<GrantedRole> grList = new ArrayList<>();
GrantedRole gr = new GrantedRole();
gr.setRole("ROLE_TEST");
gr.setPermissions(Permission.READ | Permission.WRITE);
grList.add(gr);

ChangeSecurity cs = new ChangeSecurity();
cs.setGrantedUsersList(guList);
cs.setGrantedRolesList(grList);
cs.setRecursive(true);
ws.auth.changeSecurity(uuid, cs);
```

**`overwriteSecurity`** — replaces all grants completely:

```java
ChangeSecurity cs = new ChangeSecurity();
cs.setGrantedUsersList(guList);
cs.setRecursive(true);
ws.auth.overwriteSecurity(uuid, cs);
```

### Profiles

```java
// List active profiles
for (Profile p : ws.auth.getProfiles(true)) {
    System.out.println(p.getName());
}

// Get profile of a user
Profile p = ws.auth.getUserProfile("okmAdmin");

// Assign profile to user
ws.auth.setUserProfile("test", profile.getId());
```

### Tenants

```java
List<Tenant> tenants = ws.auth.getUserTenants();
ws.auth.setUserTenant(tenantId);
```

### Personal Access Tokens

```java
// Create long-lived token with expiration date
Calendar expiration = Calendar.getInstance();
expiration.set(Calendar.DAY_OF_MONTH, 10);
String pat = ws.auth.createPersonalAccessToken("openkm", expiration);
```

### Misc

```java
String sessionId        = ws.auth.getSessionId();
boolean isLowercase     = ws.auth.isLoginLowercase();
```

---

## ws.bookmark — Bookmarks

Bookmarks allow users to save references to nodes (documents, folders, mails, records) for quick access.

### getUserBookmarks

```java
List<Bookmark> getUserBookmarks()
```

Returns all bookmarks of the currently authenticated user.

```java
for (Bookmark bookmark : ws.bookmark.getUserBookmarks()) {
    System.out.println(bookmark);
}
```

### create

```java
void create(String uuid, String name)
```

Creates a bookmark for the node identified by `uuid`.

```java
ws.bookmark.create("a37f570f-5e7f-4a03-8d04-9b3689be82f1", "test bookmark");
```

### rename

```java
void rename(int bookmarkId, String name)
```

Renames an existing bookmark.

```java
ws.bookmark.rename(1, "set bookmark");
```

### delete

```java
void delete(int bookmarkId)
```

Removes a bookmark by its ID.

```java
ws.bookmark.delete(1);
```

---

---

## ws.conversion — Document & Image Conversion

All conversion methods stream content in and out via `InputStream`. Use `IOUtils.copy()` (Apache Commons IO) for stream handling and always close streams after use.

> **Server requirements:** `doc2pdf` requires OpenOffice/LibreOffice enabled. `imageConvert` requires ImageMagick. `html2pdf` requires an HTML-to-PDF tool. `img2txt` requires an OCR engine. `barcode2txt` requires barcode tools.

### doc2pdf

```java
InputStream doc2pdf(InputStream is, String fileName)
```

Converts a document to PDF. The `fileName` is used to detect the MIME type by extension (e.g. `"report.docx"`). Returns an `InputStream` with the PDF content.

```java
InputStream pdfStream = ws.conversion.doc2pdf(new FileInputStream("report.docx"), "report.docx");
IOUtils.copy(pdfStream, new FileOutputStream("report.pdf"));
pdfStream.close();
```

### imageConvert

```java
InputStream imageConvert(InputStream is, String fileName, String params, String dstMimeType)
```

Applies an ImageMagick transformation to an image.

| Parameter | Description |
|-----------|-------------|
| `is` | Source image stream |
| `fileName` | Original filename (for MIME detection) |
| `params` | ImageMagick parameters — **must end with `${fileIn} ${fileOut}`**, use single spaces |
| `dstMimeType` | Output MIME type (e.g. `"image/png"`) |

```java
InputStream result = ws.conversion.imageConvert(
    new FileInputStream("photo.jpg"), "photo.jpg",
    "-resize 800x600 ${fileIn} ${fileOut}", "image/jpeg");
```

> Tip: test your ImageMagick `params` locally before integrating.

### html2pdf

```java
InputStream html2pdf(String url)
```

Converts a public URL to a PDF document. Returns the PDF as an `InputStream`.

```java
InputStream pdf = ws.conversion.html2pdf("https://www.example.com");
IOUtils.copy(pdf, new FileOutputStream("page.pdf"));
pdf.close();
```

### doc2txt

```java
String doc2txt(InputStream is, String fileName)
```

Extracts plain text from a document. Requires an appropriate text extractor configured for the document's MIME type.

```java
String text = ws.conversion.doc2txt(new FileInputStream("report.pdf"), "report.pdf");
System.out.println(text);
```

### img2txt

```java
String img2txt(InputStream is, String fileName)
```

Performs OCR on an image and returns the recognised text.

```java
String text = ws.conversion.img2txt(new FileInputStream("scan.png"), "scan.png");
```

### barcode2txt

```java
String barcode2txt(InputStream is, String fileName)
```

Decodes a barcode from an image and returns the decoded value as a String.

```java
String barcode = ws.conversion.barcode2txt(new FileInputStream("barcode.png"), "barcode.png");
```

---

## ws.dashboard — Dashboard

All dashboard methods are paginated via `offset` and `limit` parameters and return a `DashboardResultSet` (except `getUserSearches` and `findUserSearches`).

**Pagination pattern:**
- `offset=0, limit=10` → first 10 results
- `offset=10, limit=10` → results 11–20

**Return types:**
- `DashboardResultSet` — contains total count + list of `DashboardResult` objects
- `List<QueryParams>` — list of saved search parameter objects
- `List<DashboardNodeResult>` — list of node results from a saved search

### Method reference

| Method | Description |
|--------|-------------|
| `getUserCheckedOutDocuments(offset, limit)` | Documents currently checked out by the user |
| `getUserLockedDocuments(offset, limit)` | Documents locked by the user |
| `getUserLastModifiedDocuments(offset, limit)` | Documents most recently modified by the user |
| `getUserLastDownloadedDocuments(offset, limit)` | Documents most recently downloaded |
| `getUserLastCreatedDocuments(offset, limit)` | Documents most recently created |
| `getUserLastDocumentsNotesCreated(offset, limit)` | Notes recently added to documents |
| `getUserSubscribedDocuments(offset, limit)` | Documents the user is subscribed to |
| `getUserLockedFolders(offset, limit)` | Folders locked by the user |
| `getUserSubscribedFolders(offset, limit)` | Folders the user is subscribed to |
| `getUserLastCreatedFolders(offset, limit)` | Folders recently created by the user |
| `getUserLastFoldersNotesCreated(offset, limit)` | Notes recently added to folders |
| `getUserLockedRecords(offset, limit)` | Records locked by the user |
| `getUserSubscribedRecords(offset, limit)` | Records the user is subscribed to |
| `getUserLastCreatedRecords(offset, limit)` | Records recently created |
| `getUserLastRecordsNotesCreated(offset, limit)` | Notes recently added to records |
| `getUserLockedMails(offset, limit)` | Mails locked by the user |
| `getUserLastImportedMails(offset, limit)` | Recently imported mail messages |
| `getUserLastMailsNotesCreated(offset, limit)` | Notes recently added to mails |
| `getUserLastImportedMailAttachments(offset, limit)` | Mails with attachments recently imported |
| `getUserSearches()` | Returns `List<QueryParams>` of saved user searches |
| `findUserSearches(long qpId)` | Executes a saved search by ID; returns `List<DashboardNodeResult>` |

```java
// Example: list the last 10 created documents
DashboardResultSet result = ws.dashboard.getUserLastCreatedDocuments(0, 10);
System.out.println("Total: " + result.getTotal());
for (DashboardResult item : result.getResults()) {
    System.out.println(item);
}

// Example: run a saved search
List<QueryParams> searches = ws.dashboard.getUserSearches();
for (QueryParams qp : searches) {
    List<DashboardNodeResult> nodes = ws.dashboard.findUserSearches(qp.getId());
    nodes.forEach(System.out::println);
}
```

---

## ws.document — Documents

The most comprehensive service in the SDK. Covers the full lifecycle of a document: creation, content management, versioning, metadata, security, PDF operations, OCR, and more.

### Creation

```java
Document create(String uuid, File file)
Document create(String uuid, String name, InputStream is)
Document create(String uuid, String name, InputStream is, long nodeClass)
```

- `uuid` — UUID of the parent folder or record node
- `nodeClass` — business type (series) ID; `0` = none
- Returns a `Document` object with all properties of the created document

```java
// From a File
Document doc = ws.document.create(folderUuid, new File("/tmp/report.pdf"));

// From an InputStream with a name
Document doc = ws.document.create(folderUuid, "report.pdf", new FileInputStream("/tmp/report.pdf"));
```

### Reading content & properties

```java
Document   getProperties(String uuid)
InputStream getContent(String uuid)
InputStream getContentByVersion(String uuid, String versionName)
String     getExtractedText(String uuid)
InputStream getThumbnail(String uuid, ThumbnailType type)
List<Document> getChildren(String uuid)        // children of a folder/record
List<Document> getCheckedOut()                 // all docs checked out by current user
int        getNumberOfPages(String uuid)
String     getPageAsImage(String uuid, int pageNumber, int maxWidth, int maxHeight)  // Base64
```

> Use `getProperties()` to get the content length before streaming; do not rely on `InputStream.available()`.

`ThumbnailType` values: `THUMBNAIL_PROPERTIES`, `THUMBNAIL_LIGHTBOX`, `THUMBNAIL_SEARCH`.

The `Document` bean returned by `getChildren()` (and other reading methods) also exposes `getUuid()` directly:

```groovy
// Get the first document in a record and obtain its UUID directly
List<Document> docs = ws.document.getChildren(recordUuid);
if (!docs.isEmpty()) {
    String docUuid = docs.get(0).getUuid();  // preferred over ws.repository.getNodeUuid(doc.getPath())
}
```

### Metadata

```java
void setTitle(String uuid, String title)
void setDescription(String uuid, String description)
void setLanguage(String uuid, String lang)        // ISO 639-1 code
void setExtractedText(String uuid, InputStream is)
void setNodeClass(String uuid, long nodeClass)
void setDispositionStage(String uuid, long stage)
void setIndexable(String uuid, boolean enabled)
List<String> getDetectedLanguages()
```

### Rename, move, copy, delete

```java
Document rename(String uuid, String newName)
void     move(String uuid, String dstId)
Document copy(String uuid, String dstId, String newName)
void     delete(String uuid)    // moves to /okm:trash/userId
void     purge(String uuid)     // permanent deletion — no recovery without backup
```

**`extendedCopy`** — copy with selective metadata transfer (use `ws.node.extendedCopy`):

```java
// copies categories, keywords, propertyGroups, notes, and security grants
ws.node.extendedCopy(nodeId, dstId, name, true, true, true, true, true);
```

### Version control

```java
void    checkout(String uuid)
void    cancelCheckout(String uuid)                // only by the user who checked out
void    forceCancelCheckout(String uuid)           // requires ROLE_ADMIN
boolean isCheckedOut(String uuid)
Version checkin(String uuid, InputStream is, String comment)
Version checkin(String uuid, InputStream is, String comment, int increment)
long    getVersionHistorySize(String uuid)         // bytes used by version history
```

- Only the user who performed `checkout` can `checkin` or `cancelCheckout`.
- `increment` ≥ 1 depends on the configured `VersionNumberAdapter`.

### Annotations & differences

```java
String      getAnnotations(String uuid, String versionName)
InputStream getDifferences(String uuid, String versionName1, String versionName2)  // PDF
```

### PDF operations

```java
String     mergePdf(String dstId, String docName, List<String> uuids)  // returns UUID of merged doc
InputStream getPdf(String uuid)
Document   saveAsPdf(String uuid, String newName, boolean propertyGroups, boolean security)
boolean    isConvertibleToPDF(String uuid)    // false if already PDF
void       webPageImport(String uuid, String url)  // saves web page as PDF into repository
```

### OCR / data capture

```java
boolean     isOCRDataCaptureSupported(String uuid)
OCRRecognise recognize(String uuid)
void        captureData(String uuid, long templateId)
```

### Stamps

```java
void stamp(String uuid, int type, long stampId)
```

### Templates

```java
WizardNode createWizard(String uuid, String name, long nodeClass, InputStream is)
void       updateFromTemplate(String uuid, String dstId, Map<String, String> properties)
```

`createWizard` returns a `WizardNode` describing pending wizard actions (keyword assignment, category selection, metadata entry). Property values in the `properties` map must use ISO 8601 date format for date fields.

### Live Edit

```java
void liveEditCheckin(String uuid, String comment, int increment)
void liveEditCancelCheckout(String uuid)
void liveEditForceCancelCheckout(String uuid)   // requires ROLE_ADMIN
void liveEditSetContent(String uuid, InputStream is)
```

### AutoCAD (XRef)

```java
List<XRef> getXrefs(String uuid)
void       refresh(String uuid)
```

### Utility / validation

```java
boolean isValid(String uuid)       // true if node is a document
boolean isAttachment(String uuid)  // true if node is a mail attachment
String  getPath(String uuid)       // converts UUID to repository path
```

---

---

## ws.filePlan — File Plan

The file plan organises records management through a hierarchy of **sections** and **classes** (node classes / business types). All methods are accessed via `ws.filePlan`.

**Key beans:**
- `NodeClassSectionDefinition` — an organisational section within the file plan
- `NodeClass` — a classification category (business type) for records

### Sections

```java
// Root sections
List<NodeClassSectionDefinition> getRootSections()
List<NodeClassSectionDefinition> getRootSectionsFilteredBySecurity(int permissions)
List<Long>                       getRootSectionsIdWithChildren()
List<NodeClassSectionDefinition> getAllSectionsOrdered()

// Children sections
List<NodeClassSectionDefinition> getChildrenSections(long parentId)
List<NodeClassSectionDefinition> getChildrenSectionsFilteredBySecurity(long parentId, int permissions)
List<Long>                       getChildrenSectionsIdByTenantWithChildren(long parentId)

// Search & navigation
List<NodeClassSectionDefinition> findSectionFiltered(String code, String name, int permissions)
List<NodeClassSectionDefinition> getBreadcrumb(long sectionId)   // path from root to section
```

### Classes (Node Classes)

```java
List<NodeClass> getChildrenClasses(long sectionId)
List<NodeClass> getChildrenClassesFilteredBySecurity(long sectionId, int permissions)
NodeClass       findNodeClassByPk(long id)
List<NodeClass> getAllNodeClasses()
List<NodeClass> findElectronicRecordClasses()
List<NodeClass> findElectronicRecordClassesFilteredBySecurity(int permissions)
List<NodeClass> findFilteredByCodeOrNameFilteredBySecurity(String code, String name, int permissions)
```

**Permissions** use constants from `com.openkm.sdk4j.bean.Permission` (`READ`, `WRITE`, `ALL_GRANTS`, etc.), combined with `+` or `|`.

```java
// Example: list root sections the current user can read
List<NodeClassSectionDefinition> roots =
    ws.filePlan.getRootSectionsFilteredBySecurity(Permission.READ);
for (NodeClassSectionDefinition s : roots) {
    System.out.println(s);
}

// Example: find all electronic record classes
List<NodeClass> classes = ws.filePlan.findElectronicRecordClasses();

// Example: search sections by code prefix
List<NodeClassSectionDefinition> results =
    ws.filePlan.findSectionFiltered("1.", "invoice", Permission.ALL_GRANTS);

// Example: get breadcrumb path to a section
List<NodeClassSectionDefinition> breadcrumb = ws.filePlan.getBreadcrumb(3L);
```

---

## ws.folder — Folders

### Creation

```java
Folder create(String uuid, String name)
Folder create(String uuid, String name, long nodeClass)
```

- `uuid` — UUID of the parent folder or record node
- `nodeClass` — business type (series) ID; `0` = none
- Returns a `Folder` object with all properties of the created folder

```java
Folder folder = ws.folder.create("1be884f4-5758-4985-94d1-f18bfe004db8", "test");
```

### Reading

```java
Folder         getProperties(String uuid)
List<Folder>   getChildren(String uuid)
ContentInfo    getContentInfo(String uuid)   // folder count, doc count, record count, mail count, total bytes
boolean        isValid(String uuid)          // true if node is a folder
String         getPath(String uuid)          // UUID → repository path
```

```java
for (Folder fld : ws.folder.getChildren(parentUuid)) {
    System.out.println(fld.getPath());
}

ContentInfo info = ws.folder.getContentInfo(uuid);
// info.getFolders(), info.getDocuments(), info.getRecords(), info.getMails(), info.getSize()
```

### Rename, move, copy, delete

```java
Folder rename(String uuid, String newName)
void   move(String uuid, String dstId)
Folder copy(String uuid, String dstId, String newName)          // null newName keeps original name
Folder extendedCopy(String uuid, String dstId, String newName,
                    boolean categories, boolean keywords,
                    boolean propertyGroups, boolean notes, boolean security)
void   delete(String uuid)    // moves to /okm:trash/userId
void   purge(String uuid)     // permanent — only recoverable from backup
```

> `copy` transfers only security grants. Use `extendedCopy` to also copy categories, keywords, property groups, notes, and/or security.

```java
// Simple copy, keep original name
ws.folder.copy(srcUuid, dstUuid, null);

// Extended copy with full metadata
ws.folder.extendedCopy(srcUuid, dstUuid, "new name folder", true, true, true, true, true);
```

### Metadata

```java
void setDescription(String uuid, String description)
void createMissingFolders(String fldPath)   // creates all missing folders in the given path
```

```java
ws.folder.createMissingFolders("/okm:root/projects/2024/q1");
```

### Templates

```java
Folder createFromTemplate(String uuid, String dstPath,
                          boolean categories, boolean keywords, boolean notes,
                          Map<String, String> properties)

Folder createFromTemplate(String uuid, String dstPath,
                          boolean categories, boolean keywords, boolean notes,
                          boolean propertyGroups, boolean security,
                          Map<String, String> properties)
```

Property values must use ISO 8601 for dates and JSON array notation for multi-value fields:

```java
Map<String, String> props = new HashMap<>();
props.put("okp:tpl.name", "sdk name");
props.put("okp:tpl.bird_date", ISO8601.formatBasic(Calendar.getInstance()));
props.put("okp:tpl.language", "[ \"java\" ]");
props.put("okp:technology.comment", "sdk comment");

Folder folder = ws.folder.createFromTemplate(
    templateUuid, "/okm:root/sdk/template",
    true, true, true, props);
```

---

## ws.quickImport — Quick Import

Optimised for bulk or high-performance scenarios. Executes fewer background steps than the standard `ws.document` and `ws.folder` creation methods.

### importDocument

```java
String importDocument(String uuid, File file)
```

Creates a document inside the folder or record identified by `uuid`. Returns the UUID of the created document.

```java
File file = new File("/tmp/logo.png");
String docUuid = ws.quickImport.importDocument(folderUuid, file);
System.out.println("Created: " + docUuid);
```

### importFolder

```java
String importFolder(String uuid, String name)
```

Creates a folder inside the parent folder or record identified by `uuid`. Returns the UUID of the created folder.

```java
String fldUuid = ws.quickImport.importFolder(parentUuid, "folder-name");
System.out.println("Created: " + fldUuid);
```

> Use `ws.quickImport` instead of `ws.document.create` / `ws.folder.create` when doing large batch imports where performance is critical.

---

---

## ws.mail — Mail

Mail nodes represent email messages stored in the repository. They can have attachments (as `Document` nodes), be imported from EML/MSG files, and be sent via the server's mail configuration.

### Core operations

```java
Mail       getProperties(String uuid)
List<Mail> getChildren(String uuid)       // mails inside a folder/record
boolean    isValid(String uuid)
String     getPath(String uuid)
void       delete(String uuid)            // moves to trash
void       purge(String uuid)             // permanent deletion
Mail       rename(String uuid, String newName)
void       move(String uuid, String dstId)
Mail       copy(String uuid, String dstId, String newName)
Mail       extendedCopy(String uuid, String dstId,
                        boolean categories, boolean keywords,
                        boolean propertyGroups, boolean notes,
                        boolean security, String newName)
```

### Metadata

```java
void   setTitle(String uuid, String title)
void   setDescription(String uuid, String description)
void   setNodeClass(String uuid, long nodeClass)
void   setDispositionStage(String uuid, long stage)
String getExtractedText(String uuid)
```

### Attachments

```java
Document       createAttachment(String uuid, String docName, InputStream is)
void           deleteAttachment(String uuid, String docId)
List<Document> getAttachments(String uuid)
```

```java
// Add attachment
InputStream is = new FileInputStream("/tmp/logo.png");
ws.mail.createAttachment(mailUuid, "logo.png", is);
is.close();

// List attachments
List<Document> atts = ws.mail.getAttachments(mailUuid);
```

### Import (EML / MSG)

```java
Mail importEml(String uuid, String title, InputStream is)
Mail importMsg(String uuid, String title, InputStream is)
```

```java
InputStream is = new FileInputStream("/tmp/email.eml");
Mail mail = ws.mail.importEml(folderUuid, "Email Title", is);
is.close();
```

### Export / PDF

```java
InputStream getContent(String uuid)               // raw EML stream
InputStream getThumbnail(String uuid, ThumbnailType type)
InputStream getPdf(String uuid)
Document    saveAsPdf(String uuid, String newName, boolean propertyGroups, boolean security)
```

### Sending mail

```java
// Single recipient
void sendMail(String to, String subject, String body)

// Multiple recipients
void sendMail(List<String> to, String subject, String body)

// With explicit sender
void sendMail(String from, List<String> to, String subject, String body)

// With repository document attachments
Mail sendMailWithAttachments(String from, List<String> to, List<String> cc,
                             List<String> bcc, List<String> replyTo,
                             String subject, String body,
                             List<String> docsId, String uuid)
```

```java
List<String> to    = Arrays.asList("user@example.com");
List<String> docsId = Arrays.asList("46762f90-82c6-4886-8d21-ad3017dd78a7");
ws.mail.sendMailWithAttachments("sender@example.com", to,
    new ArrayList<>(), new ArrayList<>(), new ArrayList<>(),
    "Subject", "Body", docsId, folderUuid);
```

### Forward email

```java
void forwardEmail(String uuid, List<String> users, List<String> roles,
                  List<String> mails, String message)
```

### Paginated listing

```java
SimpleNodeBaseResultSet getMailsPaginated(String context, int offset, int limit,
                                          MailFilterQuery filter,
                                          String orderColumn, boolean orderAsc)
```

`orderColumn` values: `uuid`, `subject`, `from`, `sentDate`, `hasAttachments`

```java
MailFilterQuery filter = new MailFilterQuery();
filter.setSubject("");
filter.setAttachments(AttachmentEnum.ALL);
SimpleNodeBaseResultSet results =
    ws.mail.getMailsPaginated("/okm:root", 0, 20, filter, "subject", true);
```

### Wizard creation

```java
WizardNode createWizard(String uuid, String title, InputStream is, String type)
// type: Mail.ORIGIN_EML or Mail.ORIGIN_MSG
```

### Mail account management

```java
List<MailAccount>    getAccounts()
void                 addAccount(MailAccount account)
void                 updateAccount(MailAccount account)
void                 testAccount(MailAccount account)
void                 deleteAccount(long mailAccountId)
MailServerMessages   getMailMessages(long accountId, long start)
void                 importMessages(long mailAccountId, List<Long> messageIds)
```

`MailAccount` protocol constants: `PROTOCOL_IMAPS`, `PROTOCOL_IMAP`, `PROTOCOL_POP3`, `PROTOCOL_POP3S`.

```java
MailAccount account = new MailAccount();
account.setMailProtocol(MailAccount.PROTOCOL_IMAPS);
account.setMailUser("inbox@gmail.com");
account.setMailPassword("secret");
account.setMailHost("imap.gmail.com");
account.setMailFolder("OpenKM");
account.setActive(true);
account.setRecursive(true);
ws.mail.addAccount(account);
```

### Mail filter management

```java
void               createFilter(long mailAccountId, MailFilter filter)
void               updateFilter(MailFilter filter)
void               deleteFilter(long mailFilterId)
List<MailFilterRule> getFilterRules(long filterId)
void               createRule(long filterId, MailFilterRule rule)
void               updateRule(MailFilterRule rule)
void               deleteRule(long ruleId)
```

`MailFilterRule` field constants: `FIELD_FROM`, `FIELD_TO`, `FIELD_SUBJECT`, `FIELD_CONTENT`, `FIELD_ATTACHMENT`
`MailFilterRule` operation constants: `OPERATION_EQUALS`, `OPERATION_NOT_EQUALS`, `OPERATION_CONTAINS`, `OPERATION_ENDS_WITH`, `OPERATION_STARTS_WITH`

```java
MailFilterRule rule = new MailFilterRule();
rule.setField(MailFilterRule.FIELD_FROM);
rule.setOperation(MailFilterRule.OPERATION_CONTAINS);
rule.setValue("test@domain.com");
rule.setActive(true);
ws.mail.createRule(filterId, rule);
```

---

## ws.node — Generic Node Operations

Node operations work across all node types (documents, folders, mails, records). Access via `ws.node`.

### Node retrieval

```java
Node       getNodeByUuid(String uuid)
List<Node> getNodesFiltered(List<String> uuids)
```

### Version management

```java
List<Version> getVersionHistory(String uuid)
void          restoreVersion(String uuid, String versionName)
void          renameVersion(String uuid, String versionName, String newName)
void          deleteVersion(String uuid, String versionName)
void          purgeVersionHistory(String uuid)
void          setComment(String uuid, String versionName, String comment)
```

### Locking

```java
LockInfo lock(String uuid)
LockInfo forceLock(String uuid)       // requires ROLE_ADMIN
void     unlock(String uuid)
void     forceUnlock(String uuid)     // requires ROLE_ADMIN
boolean  isLocked(String uuid)
LockInfo getLockInfo(String uuid)
boolean  hasNodesLockedByOtherUser(String uuid)
```

### Record promotion

```java
PromoteAsRecordEvaluation mayBePromotedAsRecord(String uuid, boolean fullEvaluation)
void   promoteAsRecord(String uuid)
void   degradeRecord(String uuid)
boolean isElectronicRecordPath(String uuid)
Record  getElectronicRecordInPath(String uuid)   // first Record ancestor in path
```

### Subscriptions

```java
void subscribe(String uuid)
void unsubscribe(String uuid)
```

### Paginated children

```java
SimpleNodeBaseList getChildrenNodesPaginated(
    String uuid, int offset, int limit,
    String filter, String orderByField, boolean orderAsc,
    List<Integer> filteredTypes)

SimpleNodeBaseList getChildrenNodesPaginated(
    String uuid, int offset, int limit,
    String filter, String orderByField, boolean orderAsc,
    List<Integer> filteredTypes, String pluginName)

SimpleNodeBaseList getChildrenNodesByCategoryPaginated(
    String uuid, int offset, int limit,
    String filter, String orderByField, boolean orderAsc,
    List<Integer> filteredTypes)

SimpleNodeBaseList getChildrenNodesByCategoryPaginated(
    String uuid, int offset, int limit,
    String filter, String orderByField, boolean orderAsc,
    List<Integer> filteredTypes, String pluginName)
```

`filteredTypes` uses constants from `SimpleNodeBase`: `TYPE_FOLDER`, `TYPE_DOCUMENT`, `TYPE_MAIL`, `TYPE_RECORD`
`orderByField` uses constants from `NodesPaginationInfo`: `ORDER_BY_NAME`, etc.

```java
List<Integer> types = Arrays.asList(SimpleNodeBase.TYPE_FOLDER, SimpleNodeBase.TYPE_DOCUMENT);
SimpleNodeBaseList result = ws.node.getChildrenNodesPaginated(
    parentUuid, 0, 10, "", NodesPaginationInfo.ORDER_BY_NAME, true, types);
// result.getNodes(), result.getTotal(), result.getFilteredCount()
```

### Navigation

```java
List<BreadCrumbItem> getBreadcrumb(String uuid)   // path from root to node
```

### ZIP import / export

```java
void        importZip(String uuid, InputStream is)
void        unZip(String uuid, String dstId)
InputStream exportZip(List<String> uuids, boolean withPath, boolean background)
ZipDownloadEvaluationResult evaluateDownloadZip(List<String> uuids)
```

```java
// Export nodes as ZIP
InputStream zip = ws.node.exportZip(Arrays.asList(uuid1, uuid2), true, false);
IOUtils.copy(zip, new FileOutputStream("/tmp/export.zip"));
```

### Download tokens

```java
String generateDownloadToken(String uuid, boolean preview, boolean oneTimeUse, Calendar validUntil)
```

- Download URL: `http://host/openkm/download?DTK=<token>`
- Preview URL: `http://host/openkm/download?PTK=<token>`

### Restore & misc

```java
Node restore(String uuid)   // recovers a deleted node from trash
```

---

## ws.note — Notes

Notes are text annotations attached to any node (document, folder, mail, record). A note has its own UUID (returned in `note.getPath()`).

```java
Note         add(String uuid, String text)           // attach note to a node; returns Note
Note         get(String noteId)                      // retrieve note by its UUID
void         delete(String noteId)                   // remove note by its UUID
void         set(String noteId, String text)         // update note text
List<Note>   list(String uuid)                       // all notes on a node
List<NoteHistory> getNoteHistories(String uuid)      // full edit history of notes on a node
```

```java
// Add a note
Note note = ws.note.add(docUuid, "Reviewed and approved.");
String noteId = note.getPath();   // UUID used for subsequent operations

// List all notes on a node
for (Note n : ws.note.list(docUuid)) {
    System.out.println(n.getText());
}

// Update note text
ws.note.set(noteId, "Reviewed, approved, and archived.");

// Delete note
ws.note.delete(noteId);

// Retrieve full history
for (NoteHistory nh : ws.note.getNoteHistories(docUuid)) {
    System.out.println(nh);
}
```

---

---

## ws.notification — Notifications

Sends email notifications referencing repository nodes to OpenKM users, roles, and/or external email addresses.

```java
void notify(List<String> uuids, List<String> users, List<String> roles,
            List<String> mails, String message, boolean attachment)
```

| Parameter | Description |
|-----------|-------------|
| `uuids` | UUIDs of nodes (documents, folders, mails, records) to reference |
| `users` | OpenKM usernames to notify |
| `roles` | OpenKM role names to notify |
| `mails` | External email addresses to notify |
| `message` | Email body text |
| `attachment` | `true` to attach the referenced node(s) to the email |

```java
List<String> uuids = Arrays.asList("b153c4b7-3d1c-4589-bd42-0ed0f34fd338");
List<String> users = Arrays.asList("test", "sochoa");
List<String> roles = Arrays.asList("ROLE_TEST");
List<String> mails = new ArrayList<>();

ws.notification.notify(uuids, users, roles, mails, "Body of the message", false);
```

---

## ws.pdf — PDF Operations

All operations work on documents stored in the repository that are PDF or convertible to PDF. Results are saved as new documents in the specified destination folder/record.

### getImage

```java
InputStream getImage(String uuid, int page, String size)
```

Converts a PDF page to an image. `page` starts at 1. `size` is the width in pixels (`null` defaults to 150).

```java
InputStream is = ws.pdf.getImage(docUuid, 1, null);
IOUtils.copy(is, new FileOutputStream("/tmp/page-1.png"));
is.close();
```

### split

```java
List<Document> split(String uuid, String dstId, String baseName, List<Integer> pages)
```

Splits a PDF into multiple documents, one per specified page. Naming pattern: `baseName-001.pdf`, `baseName-003.pdf`, etc.

```java
List<Document> docs = ws.pdf.split(srcUuid, dstFolderUuid, "split", Arrays.asList(2, 3));
```

### extract

```java
boolean extract(String uuid, String dstId, String name, List<Integer> pages)
```

Extracts specified pages into a new PDF document. Returns `true` on success.

```java
boolean ok = ws.pdf.extract(srcUuid, dstFolderUuid, "extract.pdf", Arrays.asList(2, 3));
```

### remove

```java
Document remove(String uuid, String dstId, String name, List<Integer> pages)
```

Creates a new PDF with the specified pages removed. Returns the new `Document`.

```java
Document doc = ws.pdf.remove(srcUuid, dstFolderUuid, "without-pages.pdf", Arrays.asList(2, 3));
```

### rotate

```java
Document rotate(String uuid, String dstId, String name, Integer angle, List<Integer> pages)
```

Rotates specified pages and saves as a new PDF. `angle` values: `90`, `180`, `270`.

```java
Document doc = ws.pdf.rotate(srcUuid, dstFolderUuid, "rotated.pdf", 180, Arrays.asList(2, 3));
```

### insertPages

```java
Document insertPages(String uuid, InsertPagesRequest request)
```

Merges pages from one or more source documents into a target PDF at specified positions. Returns the resulting `Document`.

```java
InsertPagesRequest request = new InsertPagesRequest();
request.name = "merged.pdf";
request.dstId = dstFolderUuid;

PageInsertOperation op = new PageInsertOperation();
op.srcId = sourceDocUuid;
op.pages = Arrays.asList(1, 2, 3);
op.insertFromPage = 1;       // insert before page 1 of target
request.insertOperations.add(op);

Document result = ws.pdf.insertPages(targetDocUuid, request);
```

### optimize

```java
boolean optimize(String uuid)
```

Compresses and optimises a PDF in place. Returns `true` on success.

```java
boolean ok = ws.pdf.optimize(docUuid);
```

---

## ws.plugin — REST Plugins

Executes custom server-side REST plugins registered in OpenKM. Plugins must implement the `RestPlugin` interface and be annotated with `@PluginImplementation`. Identified by their fully qualified class name.

### executePluginPost

```java
Object executePluginPost(String className, Map<String, String> parameters,
                         Class<?> clazz, InputStream is)
```

Invokes a plugin via POST. `clazz` is used to unmarshall the response. `is` can be `null` or a document stream to upload.

```java
Map<String, String> params = new HashMap<>();
params.put("param1", "value1");
String result = (String) ws.plugin.executePluginPost(
    "com.openkm.plugin.rest.TestRestPlugin", params, String.class, null);
```

### executePluginPostReturnFile

```java
InputStream executePluginPostReturnFile(String className,
                                        Map<String, String> parameters,
                                        InputStream is)
```

Invokes a plugin via POST that returns a file stream.

```java
Map<String, String> params = new HashMap<>();
params.put("docId", "/okm:root/invoices/invoice.pdf");
InputStream is = ws.plugin.executePluginPostReturnFile(
    "com.openkm.plugin.rest.TestGetDocumentRestPlugin", params, null);
IOUtils.copy(is, new FileOutputStream("/tmp/invoice.pdf"));
is.close();
```

### executePluginGet

```java
Object executePluginGet(String className, Map<String, String> parameters, Class<?> clazz)
```

Invokes a plugin via GET. Response is unmarshalled to `clazz`.

```java
Map<String, String> params = new HashMap<>();
params.put("docId", "/okm:root/invoices/invoice.pdf");
String result = (String) ws.plugin.executePluginGet(
    "com.openkm.plugin.rest.TestRestPlugin", params, String.class);
```

### executePluginGetReturnFile

```java
InputStream executePluginGetReturnFile(String className, Map<String, String> parameters)
```

Invokes a plugin via GET that returns a file stream.

```java
Map<String, String> params = new HashMap<>();
params.put("docId", "/okm:root/invoices/invoice.pdf");
InputStream is = ws.plugin.executePluginGetReturnFile(
    "com.openkm.plugin.rest.TestGetDocumentRestPlugin", params);
IOUtils.copy(is, new FileOutputStream("/tmp/result.pdf"));
is.close();
```

---

---

## ws.property — Properties (Categories, Keywords, Encryption, Signatures)

Manages node-level metadata: categories, keywords, encryption flags, and digital signature status. Applies to documents, folders, mails, and records.

### Categories

```java
void addCategory(String uuid, String catId)
void removeCategory(String uuid, String catId)
```

`catId` is the UUID of the category folder node.

```java
ws.property.addCategory(docUuid, categoryFolderUuid);
ws.property.removeCategory(docUuid, categoryFolderUuid);
```

### Keywords

```java
void addKeyword(String uuid, String keyword)
void removeKeyword(String uuid, String keyword)
```

Keywords are case-sensitive. Use lowercase, single words or underscore-separated values (`"invoice"`, `"two_words"`).

```java
ws.property.addKeyword(docUuid, "invoice");
ws.property.addKeyword(docUuid, "pending_review");
ws.property.removeKeyword(docUuid, "pending_review");
```

### Encryption flag

```java
void setEncryption(String uuid, String cipherName)
void unsetEncryption(String uuid)
```

These methods mark the document as encrypted/unencrypted **without** performing actual cryptographic operations. They update the metadata flag only.

```java
ws.property.setEncryption(docUuid, "AES");
ws.property.unsetEncryption(docUuid);
```

### Digital signature flag

```java
void    setSigned(String uuid, boolean signed)
boolean isSigned(String uuid)
```

Marks the document as signed or unsigned **without** performing cryptographic verification. Updates the metadata flag only.

```java
ws.property.setSigned(docUuid, true);
boolean signed = ws.property.isSigned(docUuid);
```

---

---

## ws.propertyGroup — Metadata Groups (Property Groups)

Metadata groups are collections of structured fields (inputs, selects, dates, checkboxes, textareas, suggestboxes) attached to nodes. Group and field names follow the JCR namespace convention: `okg:groupName` for groups and `okp:groupName.fieldName` for fields.

**Date handling** — use `com.openkm.sdk4j.util.ISO8601`:
```java
// Write
properties.put("okp:technology.date", ISO8601.formatBasic(Calendar.getInstance()));
// Read back
Calendar cal = ISO8601.parseBasic(properties.get("okp:technology.date"));
```

**Multi-value select fields** — use JSON array strings:
```java
properties.put("okp:technology.type", "[ \"t1\", \"t2\" ]");
// Or with Gson:
properties.put("okp:technology.language", new Gson().toJson(Arrays.asList("java", "python")));
```

### Group assignment

```java
void addGroup(String uuid, String grpName, Map<String, String> propertiesMap)
void removeGroup(String uuid, String grpName)
boolean hasGroup(String uuid, String grpName)
```

> `addGroup` must be called before `setProperties`. Partial property maps are accepted — omitted fields retain their default/existing values.

```java
Map<String, String> props = new HashMap<>();
props.put("okp:technology.comment", "initial comment");
props.put("okp:technology.date", ISO8601.formatBasic(Calendar.getInstance()));
props.put("okp:technology.type", "[ \"t1\", \"t2\" ]");
ws.propertyGroup.addGroup(uuid, "okg:technology", props);

boolean has = ws.propertyGroup.hasGroup(uuid, "okg:technology");
ws.propertyGroup.removeGroup(uuid, "okg:technology");
```

### Reading group data

```java
List<PropertyGroup>  getGroups(String uuid)                         // groups assigned to a node
List<PropertyGroup>  getAllGroups()                                  // all groups in the system
List<PropertyGroup>  getAllGroups(String uuid)                       // all groups, filtered by automation events for node
PropertyGroup        getGroup(String grpName)                        // group definition by name
Map<String, String>  getProperties(String uuid, String grpName)     // field values as key→value map
```

```java
Map<String, String> values = ws.propertyGroup.getProperties(uuid, "okg:technology");
for (Map.Entry<String, String> e : values.entrySet()) {
    System.out.println(e.getKey() + " > " + e.getValue());
}
```

### Updating field values

```java
void setProperties(String uuid, String grpName, Map<String, String> properties)
```

Partial updates allowed — only specified keys are modified.

### Form element definitions

Form elements describe the field schema (type, label, options, validators, etc.).

```java
List<FormElement> getPropertyGroupFormDefinition(String grpName)              // empty form definition
List<FormElement> getPropertyGroupFormDefinition(String uuid, String grpName) // definition in node context
List<FormElement> getPropertyGroupForm(String uuid, String grpName)           // definition + current values
```

### Version-specific access

```java
List<PropertyGroup>  getGroupsByVersion(String uuid, String versionName)
Map<String, String>  getPropertiesByVersion(String uuid, String grpName, String versionName)
List<FormElement>    getPropertiesByVersionForm(String uuid, String grpName, String versionName)
```

```java
Map<String, String> v13 = ws.propertyGroup.getPropertiesByVersion(uuid, "okg:technology", "1.3");
```

### Suggestbox helpers

```java
List<String>         getSuggestions(String uuid, String grpName, String propName)
String               getSuggestBoxKeyValue(String grpName, String propertyName, String key)
Map<String, String>  getSuggestBoxKeyValuesFiltered(String grpName, String propertyName, String filter)
```

```java
// Autocomplete suggestions
List<String> opts = ws.propertyGroup.getSuggestions(uuid, "okg:technology", "okp:technology.language");

// Lookup a single key
String val = ws.propertyGroup.getSuggestBoxKeyValue("okg:consulting", "okp:consulting.suggestbox", "countryKey");

// Filter key-value pairs
Map<String, String> filtered = ws.propertyGroup.getSuggestBoxKeyValuesFiltered(
    "okg:consulting", "okp:consulting.suggestbox", "pai");
```

### Field validation

```java
String validateField(String value, String className, List<String> uuids)
```

Invokes a custom `FieldValidator` plugin (identified by fully qualified class name). Returns an empty string if valid, or an error message.

```java
String msg = ws.propertyGroup.validateField(
    "test",
    "com.openkm.plugin.form.validator.DuplicateDocumentNumberValidator",
    Arrays.asList(uuid1, uuid2));
if (!msg.isEmpty()) System.out.println("Validation error: " + msg);
```

### Definition management (ROLE_ADMIN only)

```java
String getRegisteredDefinition()                        // returns XML definition
void   registerDefinition(InputStream is, String name) // applies XML definition
```

```java
// Read current definition
System.out.println(ws.propertyGroup.getRegisteredDefinition());

// Upload new definition
InputStream is = new FileInputStream("/tmp/PropertyGroups.xml");
ws.propertyGroup.registerDefinition(is, "test");
is.close();
```

**Minimal XML definition structure:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE property-groups PUBLIC "-//OpenKM//DTD Property Groups 2.0//EN"
    "http://www.openkm.com/dtd/property-groups-2.0.dtd">
<property-groups>
  <property-group label="Technology" name="okg:technology">
    <select label="Type" name="okp:technology.type" type="multiple">
      <option label="Alfa" value="t1"/>
      <option label="Beta" value="t2"/>
    </select>
    <input label="Date" name="okp:technology.date" type="date"/>
    <input label="Comment" name="okp:technology.comment"/>
    <textarea label="Description" name="okp:technology.description"/>
    <checkbox label="Important" name="okp:technology.important"/>
  </property-group>
</property-groups>
```

---

## ws.record — Records

Records are a special node type for formal records management. They behave similarly to folders (they can contain documents, mails, and child records) but carry additional classification, disposition, and lifecycle metadata.

### Creation

```java
Record     create(String uuid, String name, String title, long nodeClass)
WizardNode createWizard(String uuid, String name, String title, long nodeClass)
```

- `uuid` — parent folder or record UUID
- `nodeClass` — business type (series) ID from the file plan

```java
Record rec = ws.record.create(parentUuid, "Q1-2024", "Q1 2024 Invoices", nodeClassId);
```

### Reading

```java
Record         getProperties(String uuid)
List<Record>   getChildren(String uuid)
boolean        isValid(String uuid)
String         getPath(String uuid)
```

### Rename, move, copy, delete

```java
Record  rename(String uuid, String newName)
void    move(String uuid, String dstId)
Record  copy(String uuid, String dstId, String newName)
Record  extendedCopy(String uuid, String dstId, String newName,
                     boolean categories, boolean keywords,
                     boolean propertyGroups, boolean notes, boolean security)
void    delete(String uuid)   // moves to trash
void    purge(String uuid)    // permanent — no recovery without backup
```

### Metadata

```java
void setTitle(String uuid, String title)
void setDescription(String uuid, String description)
void setNodeClass(String uuid, long nodeClass)
void setDispositionStage(String uuid, long stage)
```

### Templates

```java
Record createFromTemplate(String uuid, String dstPath,
                          boolean categories, boolean keywords, boolean notes,
                          Map<String, String> properties)

Record createFromTemplate(String uuid, String dstPath,
                          boolean categories, boolean keywords, boolean notes,
                          boolean propertyGroups, boolean security,
                          Map<String, String> properties)
```

Property map values must use ISO 8601 for dates and JSON array notation for multi-value fields.

---

## ws.relation — Node Relations

Relations link pairs of nodes. Three relation types are supported: `RelationType.BIDIRECTIONAL`, `RelationType.PARENT_CHILD`, `RelationType.MANY_TO_MANY`. MANY_TO_MANY relations can be further organised into named groups.

### Relation types

```java
List<RelationType> getRelationTypes(String type)
// type: RelationType.BIDIRECTIONAL | RelationType.PARENT_CHILD | RelationType.MANY_TO_MANY
```

### Simple relations

```java
void            add(String nodeAId, String nodeBId, long relTypeId)
void            delete(long relationId)
List<Relation>  getRelations(String uuid)
```

```java
ws.relation.add(docUuidA, docUuidB, relTypeId);

for (Relation r : ws.relation.getRelations(docUuidA)) {
    System.out.println(r);
}
```

### Relation groups (MANY_TO_MANY)

```java
void                    addGroup(String uuid, String groupName, long type)
void                    addNodeToGroup(String uuid, long groupId)
void                    deleteItemFromGroup(String uuid, long groupId)
void                    deleteGroup(long groupId)
RelationGroup           getGroup(long groupId)
void                    setGroupName(long groupId, String groupName)
List<RelationGroup>     getRelationGroups(String uuid)
RelationGroupResultSet  getAllRelationGroups(long relationTypeId, String filter,
                                            int offset, int limit)
```

```java
// Create a group and add nodes
ws.relation.addGroup(anchorUuid, "Invoice Set 2024", relTypeId);
RelationGroup group = ws.relation.getRelationGroups(anchorUuid).get(0);
ws.relation.addNodeToGroup(otherUuid, group.getId());

// Paginated listing of all groups of a type
RelationGroupResultSet result =
    ws.relation.getAllRelationGroups(relTypeId, "", 0, 20);
```

---

---

## ws.report — Reports

Reports are pre-configured queries that can be executed and exported in multiple formats.

**Format constants** (defined in `Report`):
`FORMAT_CSV`, `FORMAT_DOCX`, `FORMAT_HTML`, `FORMAT_ODT`, `FORMAT_PDF`, `FORMAT_RTF`, `FORMAT_TEXT`

**Date parameters** — use `yyyy-MM-dd` format (e.g. `"2024-01-01"`).
**Multi-value parameters** — separate with comma (e.g. `"value1,value2"`).

### JasperReports-based reports

```java
List<Report> getReports(boolean active)
Report       getReport(long rpId)
String       generateDownloadReportToken(long rpId)

InputStream execute(long rpId, Map<String, String> params, String format, String uuid)
Document    save(long rpId, Map<String, String> params, String format,
                 String dstId, String docName, String uuid)
```

- `uuid` — optional context node UUID; pass `""` when not needed
- `execute` returns the report as a stream; `save` stores it as a `Document` in the repository

```java
Map<String, String> params = new HashMap<>();
params.put("from_date", "2024-01-01");
params.put("to_date",   "2024-06-30");

// Stream to local file
InputStream is = ws.report.execute(1L, params, Report.FORMAT_PDF, "");
IOUtils.copy(is, new FileOutputStream("/tmp/report.pdf"));
is.close();

// Save directly to repository
Document doc = ws.report.save(1L, params, Report.FORMAT_PDF,
    dstFolderUuid, "report.pdf", "");
```

### SQL-based reports

```java
List<SqlReport> getSqlReports(boolean active)
InputStream     executeSql(long rpId)
Document        saveSql(long rpId, Map<String, String> params, String dstId, String docName)
```

```java
List<SqlReport> reports = ws.report.getSqlReports(true);

InputStream is = ws.report.executeSql(1L);
IOUtils.copy(is, new FileOutputStream("/tmp/activity.csv"));
is.close();

Document doc = ws.report.saveSql(1L, new HashMap<>(), dstFolderUuid, "activity.csv");
```

---

## ws.repository — Repository

Provides access to well-known repository paths, node lookup utilities, scripting, raw SQL/HQL queries, translations, and system information.

### Well-known folders

```java
Folder getRootFolder()           // /okm:root
Folder getTrashFolder()          // /okm:trash/{userId}
Folder getTrashFolderBase()      // /okm:trash
Folder getTemplatesFolder()      // /okm:templates
Folder getPersonalFolder()       // /okm:personal/{userId}
Folder getPersonalFolderBase()   // /okm:personal
Folder getMailFolder()           // /okm:mail/{userId}
Folder getMailFolderBase()       // /okm:mail
Folder getCategoriesFolder()     // /okm:categories
```

### Trash

```java
void purgeTrash()   // permanently removes all nodes from the user's trash
```

> Purged nodes can only be restored from a repository backup.

### Node lookup & path conversion

```java
boolean hasNode(String nodeId)        // nodeId can be UUID or path
String  getNodePath(String uuid)
String  getNodeUuid(String nodePath)
```

### System info

```java
AppVersion  getAppVersion()
String      getRepositoryUuid()
String      getClusterUuid()
String      getUpdateMessage()
String      getServerTime()           // ISO8601 format
LicenseInfo getLicenseInfo()
```

### Attribute copy

```java
void copyAttributes(String uuid, String dstId,
                    boolean categories, boolean keywords,
                    boolean propertyGroups, boolean notes)
```

### Scripting — ROLE_ADMIN only

Executes server-side BeanShell scripts.

```java
ScriptExecutionResult executeScript(InputStream is)
ScriptExecutionResult executeScript(String script)
```

`ScriptExecutionResult` provides `getResult()`, `getStdout()`, `getStderr()`.

```java
InputStream is = new FileInputStream("/tmp/script.bsh");
ScriptExecutionResult r = ws.repository.executeScript(is);
System.out.println(r.getResult());
if (!r.getStderr().isEmpty()) System.out.println("Error: " + r.getStderr());
is.close();
```

### SQL queries — ROLE_ADMIN only (single statement)

```java
SqlQueryResults executeSqlQuery(InputStream is)
SqlQueryResults executeSqlQuery(String sql)
```

```java
SqlQueryResults result = ws.repository.executeSqlQuery(
    "SELECT NBS_UUID, NBS_NAME FROM OKM_NODE_BASE LIMIT 10;");
for (SqlQueryResultColumns row : result.getResults()) {
    System.out.println(row.getColumns().get(0) + " / " + row.getColumns().get(1));
}
```

### HQL queries — ROLE_ADMIN only (single statement)

```java
HqlQueryResults executeHqlQuery(InputStream is)
HqlQueryResults executeHqlQuery(String hql)
```

```java
HqlQueryResults result = ws.repository.executeHqlQuery(
    "SELECT uuid, author FROM NodeBase WHERE name = 'okm:root';");
for (HqlQueryResultColumns row : result.getResults()) {
    System.out.println(row.getColumns().get(0));
}
```

### Translations & configuration

```java
Map<String, String> getTranslations(String lang, String module)
// module: "frontend" | "extension" | "mobile" | custom module name

Configuration getConfiguration(String key)
// note: may be restricted to ROLE_ADMIN depending on 'webservices.visible.properties' setting
```

### Change log & locales

```java
List<ChangeLogged>  getChangeLog(String nodePath, Calendar modificationsFrom)
Map<String, String> getAvailableLocales(String locale)
```

---

## ws.search — Search

The search service supports structured queries via `QueryParams`, Lucene query strings, and paginated result sets.

### QueryParams fields

| Field | Description |
|-------|-------------|
| `domain` | Node types to include: `QueryParams.DOCUMENT`, `FOLDER`, `MAIL`, `RECORD` — combine with `\|` |
| `name` | Node name filter; supports wildcards (`*`, `?`) |
| `content` | Full-text content filter; supports wildcards |
| `author` | Creator userId filter |
| `title` | Title filter; supports wildcards |
| `keywords` | Set of keyword strings |
| `categories` | Set of category folder UUIDs |
| `mimeType` | MIME type filter (documents only) |
| `language` | Language code filter (documents only) |
| `folder` | Parent folder UUID (defaults to `/okm:root`) |
| `folderRecursive` | `true` to search recursively |
| `lastModifiedFrom` / `lastModifiedTo` | Calendar date range |
| `mailSubject`, `mailFrom`, `mailTo` | Mail-specific filters; support wildcards |
| `notes` | Notes text filter; supports wildcards |
| `properties` | `Map<String,String>` for metadata field filtering |
| `queryName` | Required when saving a search |

**Domain combination:** `params.setDomain(QueryParams.DOCUMENT | QueryParams.FOLDER)`
**Date range in metadata:** `"ISO8601.formatBasic(from),ISO8601.formatBasic(to)"`
**Multiple metadata values:** `"value1;value2"`
**All search conditions are combined with AND logic.**

### Sort field constants (`SearchSortField`)

`NAME`, `AUTHOR`, `LAST_MODIFIED`

### find — full objects

```java
List<QueryResult> find(QueryParams queryParams, String propertiesPlugin)
List<QueryResult> find(QueryParams queryParams, String sortField, boolean sortReverse,
                       String propertiesPlugin)
```

`propertiesPlugin` — canonical class name implementing `NodeProperties`; pass `null` for default behaviour.

```java
QueryParams params = new QueryParams();
params.setDomain(QueryParams.DOCUMENT | QueryParams.FOLDER);
params.setName("invoice*");

for (QueryResult qr : ws.search.find(params, null)) {
    System.out.println(qr);
}
```

### findSimpleNodeBasePaginated — lightweight paginated

Returns `SimpleNodeBase` objects (~60-70% faster than full nodes). Preferred for large result sets.

```java
SimpleNodeBaseResultSet findSimpleNodeBasePaginated(QueryParams queryParams, int offset, int limit)
SimpleNodeBaseResultSet findSimpleNodeBasePaginated(QueryParams queryParams,
                            String sortField, boolean sortReverse, int offset, int limit)
SimpleNodeBaseResultSet findSimpleNodeBasePaginated(QueryParams queryParams,
                            String sortField, boolean sortReverse, int offset, int limit,
                            String pluginName)
```

```java
SimpleNodeBaseResultSet rs = ws.search.findSimpleNodeBasePaginated(params, 0, 20);
System.out.println("Total: " + rs.getTotal());
for (SimpleNodeBase sn : rs.getResults()) {
    System.out.println(sn);
}
```

### findWithMetadata — results with metadata group values

```java
List<QueryResult> findWithMetadata(QueryParams params, String propertiesPlugin, List<String> groups)
List<QueryResult> findWithMetadata(QueryParams params, String sortField, boolean sortReverse,
                                   String propertiesPlugin, List<String> groups)

ResultSet findWithMetadataPaginated(QueryParams queryParams, int offset, int limit,
                                    String propertiesPlugin, List<String> groups)
ResultSet findWithMetadataPaginated(QueryParams queryParams, String sortField, boolean sortReverse,
                                    int offset, int limit, String propertiesPlugin, List<String> groups)
```

```java
List<String> groups = Arrays.asList("okg:consulting");
ResultSet rs = ws.search.findWithMetadataPaginated(params, 0, 10, null, groups);
System.out.println("Total: " + rs.getTotal());
```

### findByQuery — Lucene query syntax

```java
List<QueryResult> findByQuery(String query, String propertiesPlugin)
List<QueryResult> findByQuery(String query, String sortField, boolean sortReverse,
                              String propertiesPlugin)

SimpleNodeBaseResultSet findSimpleNodeBaseByQueryPaginated(String query, int offset, int limit)
SimpleNodeBaseResultSet findSimpleNodeBaseByQueryPaginated(String query,
                            String sortField, boolean sortReverse, int offset, int limit)
```

Lucene field syntax: `field:value`. Combine with `AND`, `OR`. Wildcards supported.

```java
List<QueryResult> results = ws.search.findByQuery("keyword:test AND name:invoice*.pdf", null);

SimpleNodeBaseResultSet rs = ws.search.findSimpleNodeBaseByQueryPaginated(
    "text:contract AND name:*.pdf", SearchSortField.LAST_MODIFIED, true, 0, 10);
```

### Category search

```java
List<Document> getCategorizedDocuments(String categoryId)  // categoryId = category folder UUID
```

### Saved searches

```java
long             saveSearch(QueryParams params)    // params must have queryName set
void             updateSearch(QueryParams params)  // only creator can update
QueryParams      getSearch(int qpId)              // only creator can access
List<QueryParams> getAllSearches()                 // all searches by current user
void             deleteSearch(int qpId)           // only creator can delete
List<QueryParams> findAllDefaultByNodeClass(long ncId)
```

### Other

```java
NodeSearchConfig getSearchConfig(String pluginName)

InputStream csvExport(String lang, QueryParams queryParams,
                       List<String> propertyGroups, boolean compact)
// lang: ISO 639-1 code (e.g. "es-ES"); compact=true omits empty columns
```

```java
InputStream is = ws.search.csvExport("en-GB", params, new ArrayList<>(), false);
IOUtils.copy(is, new FileOutputStream("/tmp/results.csv"));
is.close();
```

---

---

## ws.shard — Shards

Shards partition repository binary storage across multiple locations. Three methods are available.

```java
List<Shard> getShards()
void        setCurrentShard(long shardId)         // change active shard for the session
void        changeShard(String uuid, long shardId) // reassign a node's binary to a different shard
```

```java
for (Shard shard : ws.shard.getShards()) {
    System.out.println(shard);
}

ws.shard.setCurrentShard(2L);
ws.shard.changeShard("e2391b01-ce10-42c7-94a9-b0d6b394048e", 2L);
```

---

## ws.stamp — Stamps

Stamps apply text, images, or barcodes onto PDF documents. They can use predefined stamp configurations or be applied with custom parameters and expressions.

### Positioning expressions

The stamping engine resolves macro expressions to pixel coordinates:
`IMAGE_WIDTH`, `IMAGE_HEIGHT`, `PAGE_WIDTH`, `PAGE_HEIGHT`, `PAGE_CENTER`, `PAGE_MIDDLE`

Example — horizontal centering: `"PAGE_CENTER - IMAGE_WIDTH / 2"`

### Discovery

```java
List<StampItem> getAllStamps()
StampText       getStampTextByPk(long id, String uuid)
StampImage      getStampImageByPk(long id, String uuid)
StampBarcode    getStampBarcodeByPk(long id, String uuid)
```

### Apply predefined stamps

```java
void stampText(String uuid, long id)
void stampImage(String uuid, long id)
void stampBarcode(String uuid, long id)
```

```java
ws.stamp.stampText(docUuid, stampTextId);
ws.stamp.stampImage(docUuid, stampImageId);
ws.stamp.stampBarcode(docUuid, stampBarcodeId);
```

### Apply stamps with custom parameters

```java
void stampTextCustom(String uuid, long stampTextId, String text,
                     String exprX, String exprY, String range)

void stampImageManually(String uuid, long id, String exprX, String exprY,
                        String range, String personalStampUuid)

void stampImageCustom(String uuid, long stampImageId, String exprX, String exprY,
                      String range, double ratio, InputStream is)
```

- `exprX` / `exprY` — positioning expressions (see macros above)
- `range` — page range string (e.g. `"1-3"`, `"all"`)
- `ratio` — scale factor for custom image
- `is` — image stream for `stampImageCustom`

### Coordinate helpers

```java
StampCoordinates calculateStampCoordinates(
    long imgToStampWidth, long imgToStampHeight,
    long floatingDivWidth, long floatingDivHeight,
    String exprX, String exprY,
    String stampType, String stampAlign)

StampExpressions calculateStampExpressions(
    long imgToStampWidth, long imgToStampHeight,
    long posX, long posY)

StampImageSize calculateFlyImageDimensions(
    String uuid,
    long imgToStampWidth, long imgToStampHeight,
    long floatingImageWidth, long floatingImageHeight,
    int pageNumber)
```

### Personal stamps

```java
List<Document>    getPersonalStamps()                                    // user's seal images under /okm:templates/{userId}/stamps
StampPersonalImage getPersonalStampImage(String uuid, String imageUuid, long id)
```

---

## ws.task — Task Management

Full task management including tasks, projects, types, statuses, notes, and history.

### Task fields reference

| Field | Type | Notes |
|-------|------|-------|
| `subject` | String | Required |
| `start` / `end` | String (ISO8601) | `start` required |
| `description` | String | Optional |
| `statusId` | long | Required |
| `projectId` | long | Required |
| `typeId` | long | Required |
| `user` | String | Assigned OpenKM userId |
| `progress` | int | 0–100 (only on update) |
| `repeatExpression` | String | Cron syntax: `"min hour day month dayOfWeek"` |
| `repeatUntil` | String | ISO8601 end date for recurrence |
| `repeatTimes` | int | Max repetitions |
| `reminderStartUnit` / `reminderEndUnit` | String | `"m"` = minutes, `"h"` = hours, `"d"` = days |
| `reminderStartValue` / `reminderEndValue` | int | Quantity before start/end |

**Repeat expression examples:**
- `"0 0 1 * *"` — midnight on the 1st of every month
- `"0 9 * * 1"` — every Monday at 09:00

### Create / update / get / delete

```java
Task create(String subject, String start, String end, String description,
            long statusId, long projectId, long typeId, String user,
            List<String> notificationUsers, List<String> externalUsers,
            List<String> relatedDocuments, List<String> relatedFolders,
            List<String> relatedRecords, List<String> relatedMails,
            String repeatExpression, String repeatUntil, String formatDate,
            int repeatTimes, String reminderStartUnit, int reminderStartValue,
            String reminderEndUnit, int reminderEndValue)

Task update(long taskId, String subject, String start, String end, String description,
            long statusId, long projectId, long typeId, String user,
            List<String> notificationUsers, List<String> externalUsers,
            List<String> relatedDocuments, List<String> relatedFolders,
            List<String> relatedRecords, List<String> relatedMails, String owner,
            String repeatExpression, String repeatUntil, String formatDate,
            int repeatTimes, int progress,
            String reminderStartUnit, int reminderStartValue,
            String reminderEndUnit, int reminderEndValue)

Task getTask(long taskId)
void delete(long taskId)
```

```java
String start = ISO8601.formatBasic(Calendar.getInstance());
Calendar endCal = Calendar.getInstance();
endCal.add(Calendar.DATE, 15);
String end = ISO8601.formatBasic(endCal);

Task task = ws.task.create(
    "Review contract", start, end, "Review and sign",
    statusId, projectId, typeId, "okmAdmin",
    Arrays.asList("sochoa"), new ArrayList<>(),
    Arrays.asList(docUuid), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(),
    "", "", "dd-MM-yyyy", 0, "d", 1, "d", 1);
```

### Listing tasks (all methods have two overloads: with and without date range)

```java
// Without date range
TaskList getAssigned(long projectId, long typeId, long statusId,
                     String orderColumn, boolean orderAsc, int offset, int limit,
                     String subject)
// With date range
TaskList getAssigned(long projectId, long typeId, long statusId,
                     String orderColumn, boolean orderAsc, int offset, int limit,
                     Calendar from, Calendar to, String subject)

// Same overloads exist for:
TaskList getActive(...)
TaskList getFinished(...)
TaskList getNotified(...)
```

`orderColumn` values: `"subject"`, `"start"`, `"end"`, `"progress"`, `"owner"`

```java
TaskList list = ws.task.getAssigned(
    projectId, typeId, statusId, "subject", true, 0, 10,
    from, to, "");
for (Task t : list.getTasks()) {
    System.out.println(t);
}
```

### Count methods

```java
long getAssignedCount(long statusId, long projectId, long typeId)
long getActiveCount(long statusId, long projectId, long typeId)
long getFinishedCount(long statusId, long projectId, long typeId)
long getNotifiedCount(long statusId, long projectId, long typeId)
```

### Related tasks & history

```java
TaskList         getRelatedTasks(String uuid)     // tasks related to a node UUID
List<TaskHistory> getHistory(long taskId)
```

### Projects

```java
List<TaskProject> getProjects(boolean filterActive)
TaskProject       getProject(long projectId)
TaskProject       createProject(String name, boolean active, String description)
TaskProject       updateProject(long projectId, boolean active, String name, String description)
void              deleteProject(long projectId)
```

### Types

```java
List<TaskType> getTypes(boolean filterActive)
TaskType       getType(long typeId)
TaskType       createType(String name, boolean active, String description)
TaskType       updateType(long typeId, boolean active, String name, String description)
void           deleteType(long typeId)
```

### Statuses

```java
List<TaskStatus> getStatus()
TaskStatus       getStatus(long statusId)
TaskStatus       createStatus(String name, boolean finish)  // finish=true marks as completion status
TaskStatus       updateStatus(long statusId, String name, boolean finish)
void             deleteStatus(long statusId)
```

### Notes

```java
List<TaskNote> getNotes(long taskId)
TaskNote       createNote(long taskId, String text)
TaskNote       updateNote(long noteId, String text)
void           deleteNote(long noteId)
```

---

---

## ws.userConfig — User Configuration

Two methods for reading and writing the current user's home node setting.

```java
UserConfig getConfig()
void       setHome(String uuid)
```

```java
UserConfig cfg = ws.userConfig.getConfig();
System.out.println("User: " + cfg.getUser());
Node home = ws.node.getNodeByUuid(cfg.getHomeNode());
System.out.println("Home: " + home.getPath());

// Change home node
ws.userConfig.setHome("53751cb7-c462-4320-ba46-4cc33e4667ae");
```

---

## ws.wizard — Wizard

Wizards are pending action queues attached to nodes after creation via `createWizard`. They track what the user still needs to do: assign categories, keywords, metadata groups, OCR capture, digital signatures, or start a workflow. All `add*` methods accept an `order` parameter to control step sequence.

> **Important:** `setAutostart` must always be set **last** when configuring a wizard.

### Querying wizards

```java
List<WizardNode> findByUser()                         // pending wizards for current user
List<WizardNode> findAllByUser()                      // all wizards (including completed) for current user
WizardNode       findByUserAndUuid(String uuid)        // wizard for a specific node
boolean          hasWizardByUserAndNode(String uuid)
```

### Deleting / postponing

```java
void deleteAll()                                       // remove all pending wizards for current user
void deleteByNode(String uuid)                         // remove wizard for a specific node
void postponeNode(String uuid, Calendar date)          // postpone wizard until given date
void cancelPostponedNode(String uuid)                  // cancel a postponement
```

### Adding / removing wizard steps (all `add*` require `int order`)

```java
void addDigitalSignature(String uuid, int order)
void removeDigitalSignature(String uuid)

void addShowWizardCategories(String uuid, int order)
void removeShowWizardCategories(String uuid)

void addShowWizardKeywords(String uuid, int order)
void removeShowWizardKeywords(String uuid)

void addShowWizardOCRDataCapture(String uuid, int order)
void removeShowWizardOCRDataCapture(String uuid)

void addGroup(String uuid, String group, int order)    // metadata group name e.g. "okg:tpl"
void removeGroup(String uuid, String group)

void addWorkflow(String uuid, String workflow, int order)  // workflow name e.g. "purchase"
void removeWorkflow(String uuid, String workflow)

void setAutostart(String uuid)                         // must be configured last
```

```java
ws.wizard.addShowWizardCategories(uuid, 1);
ws.wizard.addShowWizardKeywords(uuid, 2);
ws.wizard.addGroup(uuid, "okg:tpl", 3);
ws.wizard.addWorkflow(uuid, "purchase", 4);
ws.wizard.setAutostart(uuid);   // always last
```

---

## ws.workflow — Workflow

Manages jBPM-based workflow process definitions and instances.

### Process definitions

```java
void                    registerProcessDefinition(InputStream is)    // upload a .par file
void                    deleteProcessDefinition(long pdId)
ProcessDefinition       getProcessDefinition(long pdId)
List<ProcessDefinition> findAllProcessDefinitions()
List<ProcessDefinition> findLatestProcessDefinitions()               // latest version per name
ProcessDefinition       findLastProcessDefinition(String name)
String                  getProcessDefinitionImage(long pdId, String uuid) // Base64 diagram image
Map<String, List<FormElement>> getProcessDefinitionForms(long pdId)
```

### Running processes

```java
// Start workflow associated with a node
ProcessInstance runProcessDefinition(long pdId, String uuid, Map<String, String> propertiesMap)

// Start workflow without node association
ProcessInstance runProcessDefinition(long pdId, Map<String, String> propertiesMap)

ProcessInstance       getProcessInstance(long piId)
List<ProcessInstance> findProcessInstances(long pdId)
List<ProcessInstance> findProcessInstancesByNode(String uuid)
```

### Task instances

```java
TaskInstanceResultSet findUserTaskInstances(int offset, int limit)     // tasks assigned to current user
TaskInstanceResultSet findPooledTaskInstances(int offset, int limit)   // unassigned tasks available to current user
List<TaskInstance>    findTaskInstances(long piId)                     // all tasks for a process instance
TaskInstance          getTaskInstance(long tiId)

void startTaskInstance(long tiId)
void setTaskInstanceActorId(long tiId, String actorId)                 // assign to user
void endTaskInstance(long tiId, String transName)                      // e.g. "end", "approve", "reject"
void setTaskInstanceValues(long pdId, long tiId, String tiName,
                           String transName, Map<String, String> properties)
```

```java
// List tasks for current user
TaskInstanceResultSet tasks = ws.workflow.findUserTaskInstances(0, 20);

// Complete a task
ws.workflow.startTaskInstance(tiId);
Map<String, String> values = new HashMap<>();
values.put("field1", "value1");
ws.workflow.setTaskInstanceValues(pdId, tiId, "Review", "approve", values);
ws.workflow.endTaskInstance(tiId, "approve");
```

### Search / overview (paginated)

```java
ProcessInstanceResultSet searchAssignedTasks(Calendar start, Calendar end,
    String status, Long procDefId, int offset, int limit)

ProcessInstanceResultSet searchStartedByMe(Calendar start, Calendar end,
    String status, Long procDefId, int offset, int limit)

ProcessInstanceResultSet searchOverview(Calendar start, Calendar end,
    String status, Long procDefId, String initiator, String taskUser,
    int offset, int limit)
```

### SuggestBox helpers (workflow forms)

```java
String              getSuggestBoxKeyValue(long pdId, String uuid, String taskName,
                                          String propertyName, String key)
Map<String, String> getSuggestBoxKeyValuesFiltered(long pdId, String uuid, String taskName,
                                                    String propertyName, String filter)
```

---

<!-- SECTION: TO BE EXPANDED -->
<!-- Next sections to add (one URL batch at a time):
     - cronTab
     - automation
     - beans reference
     - exceptions reference
-->
