# OpenKM 8.2 — Core Utility Classes
# Full reference for LLM consumption
# Version: 8.2.5

**Bean access:** `ContextWrapper.getContext().getBean(ClassName.class)`

---

## ArchiveUtils

Utility class for creating and manipulating ZIP and JAR archives.

**Bean:** `ContextWrapper.getContext().getBean(ArchiveUtils.class)`

### createZip (single file)

```
createZip(File path, OutputStream os) → void
```

Creates a ZIP archive containing a single file.

```java
ArchiveUtils archiveUtils = ContextWrapper.getContext().getBean(ArchiveUtils.class);
File file = new File("/home/openkm/test.pdf");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
archiveUtils.createZip(file, baos);
```

### createZip (directory)

```
createZip(File path, String root, OutputStream os) → void
```

Recursively creates a ZIP archive from a directory. `root` is the root entry name inside the ZIP.

```java
ArchiveUtils archiveUtils = ContextWrapper.getContext().getBean(ArchiveUtils.class);
FileUtils fileUtils = ContextWrapper.getContext().getBean(FileUtils.class);
File tmp = fileUtils.createTempDir();
// ... populate tmp with files ...
FileOutputStream os = new FileOutputStream(new File("/home/openkm/output.zip"));
archiveUtils.createZip(tmp, "myexport", os);
```

### deleteFilesFromZipByExtension

```
deleteFilesFromZipByExtension(String zipPath, String fileExtension) → void
```

Deletes all files with a given extension from an existing ZIP archive in place. The extension match is case-insensitive.

```java
ArchiveUtils archiveUtils = ContextWrapper.getContext().getBean(ArchiveUtils.class);
archiveUtils.deleteFilesFromZipByExtension("/home/openkm/output.zip", ".xml");
```

### createJar

```
createJar(File path, String root, OutputStream os) → void
```

Recursively creates a JAR archive from a directory. `root` is the root entry name inside the JAR.

```java
ArchiveUtils archiveUtils = ContextWrapper.getContext().getBean(ArchiveUtils.class);
PathUtils pathUtils = ContextWrapper.getContext().getBean(PathUtils.class);
FileUtils fileUtils = ContextWrapper.getContext().getBean(FileUtils.class);
RepositoryExporter repositoryExporter = ContextWrapper.getContext().getBean(RepositoryExporter.class);

StringWriter out = new StringWriter();
File dirFile = new File("/home/openkm");
final File tmpFile = File.createTempFile("test", ".jar", dirFile);
FileOutputStream os = new FileOutputStream(tmpFile);
File tmp = fileUtils.createTempDir();
repositoryExporter.exportFolder(null, "/okm:root/jar", tmp, false, out, new TextInfoDecorator("/okm:root/jar"));
archiveUtils.createJar(tmp, pathUtils.getName("/home/openkm"), os);
```

---

## AutomationUtils

`AutomationUtils` is a Spring bean that provides type-safe access to the event context map (`env`) shared between automation events and automation actions/validators. When an event fires (e.g. Document create, Mail move, Property group set), the system populates `env` with variables relevant to that event. Your action reads those values through `AutomationUtils` getters.

**Bean:** `ContextWrapper.getContext().getBean(AutomationUtils.class)` — or inject via `@Autowired` in your action class.

**Key rule:** Most getters throw `AutomationException` if the variable is not present. Use `has*()` guards or catch the exception. Some getters return null instead (noted below).

### Typical action skeleton

```java
@PluginImplementation
public class MyCustomAction implements Action {
    @Autowired
    private AutomationUtils automationUtils;

    @Override
    public void execute(Map<String, Object> env, Object... params) throws Exception {
        String param0 = automationUtils.getString(0, params);   // UI-configured param
        NodeBase node = automationUtils.getNodeBase(env);        // event context
        String name = automationUtils.getName(env);
    }
}
```

### Action parameter accessors
Read the typed values from the `params` array (UI-configured values for the action).

```
getString(int index, Object... params)  → String
getInteger(int index, Object... params) → Integer
getLong(int index, Object... params)    → Long
getBoolean(int index, Object... params) → Boolean
getList(int index, Object... params)    → List<String>
```

### Node getters

| Method | Return | Notes |
|---|---|---|
| `getNodeBase(env)` | `NodeBase` | Main node involved in the event |
| `getNodeToEvaluate(env)` | `NodeBase` | Smart: returns destinationNode on PRE create/move, else nodeBase |
| `getNodeBaseList(env)` | `List<NodeBase>` | Bulk operations (e.g. ZIP export) |
| `getDestinationNode(env)` | `NodeParent` | Target container for create/move events |
| `hasDestinationNode(env)` | `boolean` | Guard for getDestinationNode |
| `getSourcePath(env)` | `String` | Original path before a move |
| `hasSourcePath(env)` | `boolean` | Guard for getSourcePath |
| `getName(env)` | `String` | Node name; writable in PRE create/rename |
| `hasName(env)` | `boolean` | Guard for getName |
| `getMimeType(env)` | `String` | Document MIME type |
| `getCategories(env)` | `Set<String>` | Category UUIDs |
| `getKeywords(env)` | `Set<String>` | Node keywords |
| `getNotes(env)` | `List<NodeNote>` | Notes attached to the node |
| `getTitle(env)` | `String` | Title metadata field |
| `getVersion(env)` | `String` | Document version label (e.g. "1.0") |
| `getCreationDate(env)` | `Calendar` | Node creation date |
| `getNodeClass(env)` | `Long` | Node class ID; -1 if absent |
| `getNewNodeClass(env)` | `Long` | New class ID in set-node-class events; -1 if absent |
| `getOldNodeClass(env)` | `Long` | Previous class ID in set-node-class events; -1 if absent |

### Document getters

| Method | Return | Notes |
|---|---|---|
| `getFile(env)` | `File` | Temp local file with document content |
| `hasFile(env)` | `boolean` | Guard for getFile |
| `getInputStream(env)` | `InputStream` | Document content stream (PRE create/update) |
| `getDocumentSize(env)` | `long` | Content size in bytes |
| `getTextExtracted(env)` | `String` | Full extracted text (text extraction event) |
| `getLanguageDetected(env)` | `String` | ISO 639-1 detected language code |
| `getTextExtractor(env)` | `TextExtractor` | Extractor plugin instance; **returns null if absent** |

### Mail getters (mail events only)

| Method | Return |
|---|---|
| `getMailFrom(env)` | `String` |
| `getMailReply(env)` | `String[]` |
| `getMailTo(env)` | `String[]` |
| `getMailCc(env)` | `String[]` |
| `getMailBcc(env)` | `String[]` |
| `getMailSubject(env)` | `String` |
| `getMailContent(env)` | `String` |
| `getMailSentDate(env)` | `Calendar` |
| `getMailReceivedDate(env)` | `Calendar` |

### Property group getters (metadata events)

| Method | Return | Notes |
|---|---|---|
| `getPropertyGroupName(env)` | `String` | Group name in add/set/remove group events |
| `hasPropertyGroupName(env)` | `boolean` | Guard |
| `getPropertyGroupProperties(env)` | `Map<String,String>` | Field name → value map being set |
| `hasPropertyGroupProperties(env)` | `boolean` | Guard |
| `getPropertyGroups(env)` | `Map<String,String>` | All groups assigned to the node |
| `getAllPropertyGroups(env)` | `List<PropertyGroup>` | All system group definitions |

### Task getters (task events)

| Method | Return |
|---|---|
| `getTaskId(env)` | `long` |
| `getTaskManagerTask(env)` | `TaskManagerTask` |

### User / session getters

| Method | Return | Notes |
|---|---|---|
| `getUser(env)` | `DbUser` | User object in user create/login/logout events |
| `getTenant(env)` | `long` | Tenant ID in multi-tenant deployments |
| `getHttpServletRequest(env)` | `HttpServletRequest` | **Returns null** if not in HTTP context |
| `getHttpServletResponse(env)` | `HttpServletResponse` | **Returns null** if not in HTTP context |
| `getAuthentication(env)` | `Authentication` | Spring Security auth; **returns null** if absent |
| `getNoteText(env)` | `String` | Note text in note events; **returns null** if absent |

### Event type helpers

| Method | Return | Notes |
|---|---|---|
| `getEvent(env)` | `AutomationRule.EnumEvents` | Current event enum value |
| `getEventAt(env)` | `String` | `AT_PRE` or `AT_POST` |
| `isPreEvent(env)` | `boolean` | True if PRE phase (can still modify outcome) |
| `isMoveAction(env)` | `boolean` | True for any move event |
| `isCreationAction(env)` | `boolean` | True for any create event |
| `isDocumentCreateFromTemplate()` | `boolean` | Stack-trace check; no env needed |
| `isFolderCreateFromTemplate()` | `boolean` | Stack-trace check; no env needed |
| `isRecordCreateFromTemplate()` | `boolean` | Stack-trace check; no env needed |

```java
// Example: skip processing when document comes from template
if (automationUtils.isDocumentCreateFromTemplate()) {
    return;
}

// Example: check PRE phase and read node info
if (automationUtils.isPreEvent(env)) {
    NodeBase node = automationUtils.getNodeBase(env);
    String name = automationUtils.getName(env);
    // name can be modified here by calling automationUtils.setName(env, newName)
}

// Example: read property group data on metadata event
if (automationUtils.hasPropertyGroupProperties(env)) {
    Map<String, String> props = automationUtils.getPropertyGroupProperties(env);
    String invoiceNum = props.get("okp:invoice.number");
}
```

---

## ConfigSrv

`ConfigSrv` is a Spring service that provides typed read and write access to the OpenKM configuration parameter store. Parameters are persisted in the database, identified by a string key, and typed (string, text, boolean, integer, long, html, password, hidden, list, file).

**Bean:** `ContextWrapper.getContext().getBean(ConfigSrv.class)`

**Key behavior of typed getters:** If a key does not exist, it is automatically created with the supplied default value. This is the standard pattern for registering default config values in plugins.

### CRUD methods

```
save(Config cfg)            → void      — Create or update a Config entity
exists(String key)          → boolean   — Returns true if the key exists
delete(String key)          → void      — Deletes the parameter by key
findByPk(String key)        → Config    — Returns the full Config entity or null
findAll()                   → List<Config>  — Returns all parameters
findAllFiltered(String txt) → List<Config>  — Returns parameters whose key contains txt
```

```java
ConfigSrv configSrv = ContextWrapper.getContext().getBean(ConfigSrv.class);

// Create/update
Config cfg = new Config();
cfg.setKey("plugin.myPlugin.enabled");
cfg.setType(Config.BOOLEAN);
cfg.setValue("true");
configSrv.save(cfg);

// Check existence
boolean exists = configSrv.exists("plugin.myPlugin.enabled");

// Read full entity
Config c = configSrv.findByPk("plugin.myPlugin.enabled");

// List filtered
for (Config entry : configSrv.findAllFiltered("plugin.myPlugin")) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}
```

### Typed getters

All getters auto-create the key with the default value if it does not exist.

```
getHidden(String key, String defaultValue)   → String   — Not shown in admin UI
getString(String key, String defaultValue)   → String
getText(String key, String defaultValue)     → String   — Multi-line string
getBoolean(String key, boolean defaultValue) → boolean
getInteger(String key, int defaultValue)     → int
getLong(String key, long defaultValue)       → long
getHtml(String key, String defaultValue)     → String   — HTML markup
getPassword(String key, String defaultValue) → String   — DES-encrypted at rest, decrypted on read
getFile(String key, String path)             → ConfigStoredFile  — path is a classpath resource
getList(String key, String defaultValue)     → List<String>  — newline-delimited stored value
```

```java
ConfigSrv configSrv = ContextWrapper.getContext().getBean(ConfigSrv.class);

String  url      = configSrv.getString("plugin.myPlugin.serverUrl", "http://localhost:8080");
boolean enabled  = configSrv.getBoolean("plugin.myPlugin.enabled", false);
int     retries  = configSrv.getInteger("plugin.myPlugin.maxRetries", 3);
long    maxSize  = configSrv.getLong("plugin.myPlugin.maxFileSize", 10485760L);
String  secret   = configSrv.getPassword("plugin.myPlugin.apiSecret", "changeme");
List<String> roles = configSrv.getList("plugin.myPlugin.allowedRoles", "ROLE_ADMIN");
```

---

## DbSessionManager

`DbSessionManager` is a singleton that manages the in-memory map of active user sessions. Each session maps a string token to a Spring Security `Authentication` object.

**Access:** `DbSessionManager.getInstance()` — not a Spring bean, do not inject it.

### System token (most common use)

```
getSystemToken()              → String   — Admin token for the current user's tenant
getSystemToken(long tenantId) → String   — Admin token for an explicit tenant ID
```

Pass the system token to any API method to execute it as the system super-user, bypassing normal user permissions. Typical use in cron tasks and automation actions.

```java
String systemToken = DbSessionManager.getInstance().getSystemToken();
okmDocument.delete(systemToken, "d50133e3-dbfa-4d01-a109-28785cd48f40");
```

### Session inspection

```
getAuthentication(String token) → Authentication   — Spring Security auth for the token; updates last-access. Returns null if token unknown.
getUser(String token)           → String            — Username for the token. Returns null if unknown.
getRoles(String token)          → Set<String>       — Role names for the token. Returns empty set if unknown.
getToken(Authentication auth)   → String            — Reverse lookup: token for a given Authentication. Returns null if not found.
getInfo(String token)           → DbSessionInfo     — Session object with auth, creation time, and last-access time.
getTokens()                     → List<String>      — All active session tokens.
```

```java
DbSessionManager mgr = DbSessionManager.getInstance();

// Who owns a token?
String user  = mgr.getUser(token);
Set<String> roles = mgr.getRoles(token);

// Reverse lookup
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String token = mgr.getToken(auth);

// List all active sessions
for (String t : mgr.getTokens()) {
    System.out.println(t + " -> " + mgr.getUser(t));
}
```

### Session lifecycle

```
add(String token, Authentication auth) → void   — Register a new session
remove(String token)                   → void   — Remove a session
```

```java
String token = UUID.randomUUID().toString();
DbSessionManager.getInstance().add(token, auth);
// ... later ...
DbSessionManager.getInstance().remove(token);
```

---

## ExecutionUtils

Utility class for executing BeanShell scripts and operating-system commands. All methods are static — no bean access needed.

### runScript (from file)

```
runScript(File script) → Object[]
```

Executes a BeanShell script from a `.bsh` file. The returned `Object[]` has three elements: `[0]` return value (may be null), `[1]` stdout as String, `[2]` stderr as String.

```java
File script = new File("/home/openkm/test.bsh");
Object[] result = ExecutionUtils.runScript(script);
System.out.println("Return: " + result[0]);
System.out.println("StdOut: " + result[1]);
System.out.println("StdErr: " + result[2]);
```

### runScript (from string with system token)

```
runScript(String script, String systemToken) → Object[]
```

Executes a BeanShell script string with the system token pre-injected as the variable `systemToken` inside the script environment.

```java
String systemToken = DbSessionManager.getInstance().getSystemToken();
String script = "import com.openkm.api.OKMDocument;\n" +
                "import com.openkm.util.ContextWrapper;\n" +
                "OKMDocument doc = ContextWrapper.getContext().getBean(OKMDocument.class);\n" +
                "System.out.println(doc.getProperties(systemToken, \"/okm:root/test.pdf\"));";
Object[] result = ExecutionUtils.runScript(script, systemToken);
System.out.println("StdOut: " + result[1]);
System.out.println("StdErr: " + result[2]);
```

### runScript (from string with custom environment)

```
runScript(String script, Map<String, Object> env) → Object[]
```

Executes a BeanShell script string with all variables from the `env` map pre-injected into the interpreter. Most flexible overload for passing custom context to scripts.

```java
Map<String, Object> env = new HashMap<>();
env.put("systemToken", DbSessionManager.getInstance().getSystemToken());
env.put("folderPath", "/okm:root/reports");
String script = "System.out.println(\"Processing folder: \" + folderPath);";
Object[] result = ExecutionUtils.runScript(script, env);
System.out.println("StdOut: " + result[1]);
```

### runCmd (string, default timeout)

```
runCmd(String cmd) → CmdExecutionResult
```

Executes an OS command string using the system default timeout. The command is split on spaces; quoted arguments containing spaces are handled correctly.

`CmdExecutionResult` fields: `exitValue` (0 = success), `stdout`, `stderr`.

```java
CmdExecutionResult result = ExecutionUtils.runCmd("ls -la /home/openkm");
System.out.println("Exit: " + result.getExitValue());
System.out.println("StdOut: " + result.getStdout());
System.out.println("StdErr: " + result.getStderr());
```

### runCmd (string, custom timeout)

```
runCmd(String cmd, long timeout) → CmdExecutionResult
```

Executes an OS command string with a custom timeout in milliseconds. The process is killed if it does not finish within the timeout.

```java
long timeout = TimeUnit.SECONDS.toMillis(30);
CmdExecutionResult result = ExecutionUtils.runCmd("convert input.pdf output.png", timeout);
if (result.getExitValue() != 0) {
    System.err.println("Command failed: " + result.getStderr());
}
```

### runCmd (string array)

```
runCmd(String[] cmd) → CmdExecutionResult
```

Executes an OS command provided as a pre-split string array using the system default timeout. Use this overload when arguments already contain spaces or special characters that should not be re-split.

```java
String[] cmd = { "cp", "/home/openkm/source.pdf", "/home/openkm/dest dir/copy.pdf" };
CmdExecutionResult result = ExecutionUtils.runCmd(cmd);
System.out.println("Exit: " + result.getExitValue());
```

### printErrorInfo

```
printErrorInfo(Logger srcLog, CmdExecutionResult er) → void
```

Logs the exit value, stdout, and stderr of a `CmdExecutionResult` at ERROR level using the provided SLF4J logger. Use after a failed command execution to emit diagnostic output.

```java
CmdExecutionResult result = ExecutionUtils.runCmd("somecommand --arg");
if (result.getExitValue() != 0) {
    ExecutionUtils.printErrorInfo(log, result);
}
```

---

## FileLogger

Utility class for writing timestamped log entries to date-stamped files. Log files are named `<baseName>_yyyyMMdd.log` and written to `Config.LOG_DIR`.

Two usage modes: **instance** (construct, call methods, then `close()`) and **static** (call static overloads directly, file is opened/closed per call).

Message format: supports `%s` (Java `String.format`) and `{0}` (`MessageFormat`) placeholders.

### Instance mode

```
new FileLogger(String baseName)       — constructor; opens/appends the log file
info(String message, Object... params) → void
warn(String message, Object... params) → void
error(String message, Object... params) → void
close()                                → void   — must be called when done
```

```java
FileLogger logger = new FileLogger("myProcess");
try {
    logger.info("Starting process for user %s", "admin");
    logger.warn("Retrying step %s", "step1");
    logger.error("Failed with code %s", "500");
} finally {
    logger.close();
}
```

### Static mode

```
info(String baseName, String message, Object... params)               → void
info(Logger logger, String baseName, String message, Object... params) → void

warn(String baseName, String message, Object... params)               → void
warn(Logger logger, String baseName, String message, Object... params) → void

error(String baseName, String message, Object... params)              → void
error(Logger logger, String baseName, String message, Object... params) → void
error(Logger logger, String baseName, String message, Throwable e)    → void   — appends full stack trace
error(Logger logger, String baseName, Throwable e)                    → void   — uses e.getMessage() + stack trace
```

When a `Logger` is passed the same message is also emitted to it at the corresponding SLF4J level.

```java
// Without logger
FileLogger.info("logName", "Processing file %s", fileName);
FileLogger.warn("logName", "Slow response: %s ms", elapsed);
FileLogger.error("logName", "Unexpected value: %s", value);

// With logger and exception
try {
    // ...
} catch (Exception e) {
    FileLogger.error(log, "logName", "Unexpected error", e);
}
```

---

## FileUtils

Utility class with static helpers for common file operations. All methods are static.

### Name and path utilities

```
getFileName(String file)           → String          — File name without extension
getFileExtension(String file)      → String          — Extension without dot; "" if none
getParent(String file)             → String          — Parent directory path
toValidFilename(String filename)   → String          — Strips Windows-reserved chars (\ / : " * ? < > |)
getFileNameNotDuplicatedInDestinationPath(String pathFile, String name, String ext) → String
                                                     — Returns name.ext, or name(1).ext if already exists
generateUniqueName(String basename) → String         — Prepends yyyyMMddHHmmss to basename
```

```java
String baseName = FileUtils.getFileName("report.pdf");   // "report"
String ext      = FileUtils.getFileExtension("report.pdf"); // "pdf"
String safe     = FileUtils.toValidFilename("Q1/2024: report.pdf"); // "Q12024 report.pdf"
String unique   = FileUtils.generateUniqueName("report.pdf");       // "20240315143022report.pdf"
```

### Temporary file and directory creation

```
createTempDir()                          → File    — Unique temp dir in system temp
createTempFile()                         → File    — Temp file with .tmp extension
createTempFile(String ext)               → File    — Temp file with given extension
createSharedTempFile(String ext)         → File    — Temp file in Config.REPOSITORY_TEMP_HOME (shared FS)
createTempFileFromMime(String mimeType)  → File    — Temp file; extension resolved from MIME registry; fallback .bin
```

```java
File dir  = FileUtils.createTempDir();
File tmp  = FileUtils.createTempFile("pdf");
File tmp2 = FileUtils.createTempFileFromMime("application/pdf");
```

### Directory creation

```
createDateDir(String parent)     → File   — Creates yyyy/MM/dd under parent; returns the leaf dir
createDateUserDir(String parent) → File   — Creates yyyy/MM/dd/<currentUser> under parent
```

### Read and write

```
readFileToByteArray(File file)              → byte[]        — Entire file as bytes
readFileToString(File file)                 → String        — Entire file as UTF-8 string
readLines(File file)                        → List<String>  — File lines as UTF-8
writeStringToFile(File file, String data)   → void          — Write UTF-8 string to file (overwrite)
```

### Copy

```
copy(InputStream input, File output)    → void
copy(Reader input, File output)         → void   — UTF-8
copy(File input, OutputStream output)   → void
copy(File input, File output)           → void
```

### Delete

```
deleteQuietly(File file)  → boolean  — Delete file/dir silently; true if deleted
cleanDirectory(File dir)  → void     — Delete all contents, keep dir
deleteEmpty(File file)    → void     — Delete dir only if empty
secureDelete(File file)   → boolean  — Overwrite with random bytes then delete
```

### Existence checks

```
existFile(String file)       → boolean
existFile(File file)         → boolean
existDirectory(String dir)   → boolean
existDirectory(File dir)     → boolean
```

### File listing

```
listFiles(File dir, String[] extensions, boolean recursive) → Collection<File>
    — extensions: array like {"pdf","docx"}, or null for all
countFiles(File dir) → int   — recursive count of files + directories
```

### MIME type detection (Apache Tika)

```
getMimeType(InputStream is) → String     — Detects from magic bytes; null for unknown
getMimeType(File file)      → String     — Detects from magic bytes; null for unknown
getMediaType(String mimeType) → MediaType — Converts "type/subtype" to Spring MediaType
```

```java
String mime = FileUtils.getMimeType(new File("/home/openkm/doc.pdf")); // "application/pdf"
```

---

## FormatUtil

Utility class with static helpers for formatting, sanitizing, and validating strings. All methods are static.

### Size

```
formatSize(long bytes)   → String   — Human-readable size: "2.3 MB", "512 B", etc.
parseSize(String text)   → long     — Parse "10MB", "2GB" → bytes
```

### Dates (format: dd-MM-yyyy HH:mm:ss)

```
formatDate(Calendar cal)      → String   — null-safe; uses calendar's own timezone
formatDate(Date date)         → String
formatDate(long unixEpoch)    → String
```

### Time intervals

```
formatSeconds(long ms)            → String   — "HH:MM:SS"
formatSecondsSince(long start)    → String   — elapsed since start millis
formatMilliSeconds(long ms)       → String   — "HH:MM:SS.mmm"
formatMilliSecondsSince(long start) → String — elapsed since start millis
```

```java
long start = System.currentTimeMillis();
// ... operation ...
System.out.println(FormatUtil.formatMilliSecondsSince(start)); // "00:00:01.234"
```

### Object/array formatting

```
formatArray(String[] values)  → String   — Single element returned directly; multi uses ArrayUtils.toString
formatObject(Object value)    → String   — Object[] uses ArrayUtils.toString; otherwise toString()
```

### HTML escaping and input sanitization

```
escapeHtml(String str)          → String   — Escapes < > as entities; \n → <br/>
sanitizeInput(String string)    → String   — XSS removal via AntiSamy or legacy regex (config-driven)
cleanXSS(String value)          → String   — Escapes angle brackets, removes eval/javascript/script
sanitizeEmailString(String v)   → String   — Removes non-letter/digit/punctuation/space chars
```

### Encoding and validation

```
isValidUUID(String str)                  → boolean   — true if valid UUID format
validUTF8(byte[] input)                  → boolean   — true if valid UTF-8
fixUTF8(byte[] input)                    → byte[]    — Replace 0x00 with 0x20
fixUTF8(String input)                    → String    — Replace \u0000 with space, trim
trimUnicodeSurrogates(String text)       → String    — Remove surrogate chars, trim
stripNonValidXMLCharacters(String in)    → String    — Keep only XML 1.0 valid chars
```

---

## GeneralUtils

Spring bean for sending in-application UI notifications to users.

**Bean:** `ContextWrapper.getContext().getBean(GeneralUtils.class)`

```
uiNotify(String msg)                    → void   — Notify the currently authenticated user
uiNotify(String msg, Set<String> users) → void   — Notify a specific set of users
uiNotifyAll(String msg)                 → void   — Notify all users in the current tenant
```

```java
GeneralUtils generalUtils = ContextWrapper.getContext().getBean(GeneralUtils.class);

// Notify current user
generalUtils.uiNotify("Your export is ready.");

// Notify specific users
Set<String> recipients = new HashSet<>();
recipients.add("jsmith");
generalUtils.uiNotify("Scheduled maintenance in 10 minutes.", recipients);

// Notify everyone
generalUtils.uiNotifyAll("System will restart in 5 minutes.");
```

---

## ISO8601

Static utility class for converting between Java time objects and ISO 8601 strings.

Two formats:
- **Extended:** `YYYY-MM-DDThh:mm:ss.SSSTZD` — e.g. `2024-03-15T14:30:00.000+01:00`
- **Basic:** `yyyyMMddHHmmss` — e.g. `20240315143000` (used by OpenKM property group date fields)

```
isExtended(String value)         → boolean   — true if string matches extended pattern
parseExtended(String value)      → Calendar  — parse extended; null input → null
formatExtended(Calendar value)   → String    — format as extended; null input → null

parseBasic(String value)         → Calendar  — parse yyyyMMddHHmmss; null input → null
formatBasic(Calendar value)      → String    — format as yyyyMMddHHmmss; null input → null
formatBasic(Date value)          → String    — format as yyyyMMddHHmmss
printBasic(String value)         → String    — reformat yyyyMMddHHmmss → yyyy-MM-dd (display only)
```

```java
// Read a date from a property group field and reformat for display
String raw = "20240315143000";
Calendar cal = ISO8601.parseBasic(raw);
System.out.println(ISO8601.printBasic(raw)); // "2024-03-15"

// Write current time to a property group field
String fieldValue = ISO8601.formatBasic(Calendar.getInstance()); // "20240315143000"
```

---

## MailUtils

Spring service for sending emails and forwarding stored OpenKM mails.

**Bean:** `ContextWrapper.getContext().getBean(MailUtils.class)`

### Sending messages (no attachments)

```
sendMessage(String toAddress, String subject, String content)                                             → void
sendMessage(Collection<String> toAddress, String subject, String content)                                 → void
sendMessage(String fromAddress, String toAddress, String subject, String content)                         → void
sendMessage(String fromAddress, List<String> toAddress, String subject, String content)                   → void
sendMessage(String from, String to, List<String> replyTo, List<String> cc, List<String> bcc,
            String subject, String content)                                                               → void
sendAdminMessage(String subject, String content)    → void   — Sends to the configured admin user
```

### Sending documents as attachments

`docsPath` / `docPath` are OpenKM repository paths or UUIDs, not local disk paths.

```
sendDocument(String from, List<String> to, String subject, String text, String docPath)
                                                    → MimeMessage
sendDocuments(String from, List<String> to, String subject, String text, List<String> docsPath)
                                                    → MimeMessage
sendDocuments(String from, List<String> replyTo, List<String> to, List<String> cc, List<String> bcc,
              String subject, String text, List<String> docsPath)
                                                    → MimeMessage
```

```java
MailUtils mailUtils = ContextWrapper.getContext().getBean(MailUtils.class);
mailUtils.sendDocuments("noreply@openkm.com",
    Arrays.asList("user@example.com"),
    "Monthly report", "Please find the reports attached.",
    Arrays.asList("/okm:root/documents/report.pdf"));
```

### Forwarding and utilities

```
forwardMail(String token, String from, Collection<String> to, String message, String mailId) → MimeMessage
    — Forwards a stored mail; prepends message; subjects gets "Fwd: " prefix

parseMailList(String mails)   → List<String>   — Parse comma-separated emails; invalid entries dropped
```

### Static helpers

```
getMailFileName(Mail mail)   → String   — Returns <subject>.eml or <subject>.msg
getMailMimeType(Mail mail)   → String   — Returns EML or Outlook MIME type
genMessageId()               → String   — Generates a unique X-Message-Id value
```

---

## PathUtils

Static utility class for working with OpenKM repository paths (e.g. `/okm:root/folder/file.pdf`).

### Path navigation

```
getParent(String path)    → String    — Parent path; "/" if no parent
getName(String path)      → String    — Last segment (node name)
getContext(String path)   → String    — Root context: "/okm:root/folder/f" → "/okm:root"
getDepth(String path)     → int       — Number of segments
getElements(String path)  → String[]  — All segments as array
```

### Sanitization

```
escape(String name)              → String   — Remove Windows-reserved and OpenKM-forbidden chars
cleanup(String name)             → String   — Remove / * ; collapse spaces; trim
toValidPathName(String path)     → String   — Apply cleanup+escape to each segment below root context
shortenName(String name, int max) → String  — Truncate file name preserving extension
shortenPath(String path, int max) → String  — Shorten middle of parent path with "..."
```

### Entity encoding

```
encodeEntities(String path)   → String   — Encode & < > " ' as HTML entities (idempotent)
decodeEntities(String path)   → String   — Reverse of encodeEntities
```

### Validation and inspection

```
isPath(String nodeId)                        → boolean  — true if starts with "/"
checkPath(String path)                       → boolean  — true if starts with "/okm:"
isChild(String parentPath, String childPath) → boolean  — true if child is descendant of parent
fixContext(String context)                   → String   — "/okm:root" → "okm_root"
```

```java
PathUtils.getParent("/okm:root/folder/file.pdf")  // "/okm:root/folder"
PathUtils.getName("/okm:root/folder/file.pdf")    // "file.pdf"
PathUtils.isPath("d50133e3-dbfa-4d01-a109-28785cd48f40") // false
PathUtils.checkPath("/okm:root/file.pdf")          // true
PathUtils.isChild("/okm:root/folder", "/okm:root/folder/sub/file.pdf") // true
```

---

## PDFUtils

Spring bean for PDF operations (merge, encrypt/decrypt, split, extract, rotate, form filling, page rendering). Static overloads work on local files/streams; instance methods work on OpenKM repository documents.

**Bean:** `ContextWrapper.getContext().getBean(PDFUtils.class)`

### Merging

```
merge(List<InputStream> inputs, OutputStream output)                                    → void
merge(String token, List<String> docIds, String dstPath)                                → String   — returns dstPath
```

### Form operations

```
fillForm(InputStream input, Map<String, Object> values, OutputStream output)            → void
    — Fills AcroForm fields; supports text, checkbox, date; flattens filled fields
listFormFields(String filePath)                                                          → List<String>
```

### Encryption

```
encrypt(InputStream in, String userPwd, String ownerPwd, int perms, OutputStream out)   → void
encrypt(File in, String userPwd, String ownerPwd, int perms)                            → File (temp)
encrypt(String token, String docId, String userPwd, String ownerPwd, int perms,
        String dstPath)                                                                  → void

decrypt(InputStream in, String ownerPwd, OutputStream out)                              → void
decrypt(File in, String ownerPwd)                                                        → File (temp)

isEncrypted(InputStream in)   → boolean
isEncrypted(File in)          → boolean

disableEncryption(InputStream in, OutputStream out)   → void
disableEncryption(File in)                            → File (temp)
```

### Page count and rendering

```
getNumberOfPages(File pdf)                     → int
getNumberOfPages(InputStream stream)           → int
getNumberOfPages(String token, String docId)   → int   — auto-converts non-PDF to PDF

getDocumentInPdf(String token, String docId)   → InputStream   — caller must close
getPageAsImage(String token, String docId, int page, String size)  → File (temp PNG)
```

### Page operations

```
split(File file, String baseName, List<Integer> pages)                                  → File (ZIP)
split(String token, String docId, String dstId, List<Integer> pages, String baseName)   → List<Document>

extract(File src, File dst, List<Integer> pages)                                        → void
extract(String token, String docId, String dstId, List<Integer> pages, String docName)  → Document

rotate(File src, File dst, List<Integer> pages, Integer angle)                          → void
rotate(String token, String docId, String dstId, List<Integer> pages,
       String docName, Integer angle)                                                    → Document
```

### Optimization and conversion

```
optimize(File pdfIn, File pdfOut)                  → void
optimize(String token, String docId)               → void   — in-place checkout/checkin
convertToPdfA(File input)                          → File (temp)
```

### Utility

```
generateSample(int paragraphs, OutputStream os)    → void   — Lorem Ipsum sample PDF
createPdfWithText(String text, File out)           → void   — Single-page PDF with text
```

---

## PrincipalUtils

Static utility class for querying the current user's identity, roles, and tenant from the Spring Security context.

**Note:** Methods reading from the security context require a valid authenticated session. In background threads use the token-based overloads or `DbSessionManager`.

```
getUser()                        → String         — Username from security context
isUser(String userId)            → boolean        — true if current user equals userId
getUserByToken(String token)     → String         — Username for a session token; throws AccessDeniedException if not found

getTenantId()                    → long           — Tenant ID of current user; fallback to DEFAULT_TENANT_ID
getTenantIdByToken(String token) → long           — Tenant ID for a session token

getRoles()                       → Set<String>    — Role names of current user; empty set if not authenticated
hasRole(String role)             → boolean        — true if current user has the role

isRegularUser()                  → boolean        — true if current user lacks admin role
isAdminUser()                    → boolean        — true if current user has admin role
isSystemUser()                   → boolean        — true if current user is the internal system user
isSuperUser()                    → boolean        — true if current user is the default admin user (okmAdmin)
hasFullAccess()                  → boolean        — true if system user, super user, or admin role

getAuthentication()                    → Authentication — From SecurityContextHolder or HTTP request; null if not found
getAuthenticationByToken(String token) → Authentication — Throws AccessDeniedException if token unknown
getRemoteAddress()                     → String         — IP of current user; null if unavailable
```

```java
String user = PrincipalUtils.getUser();
long tenant = PrincipalUtils.getTenantId();
boolean isAdmin = PrincipalUtils.isAdminUser();
boolean hasAccess = PrincipalUtils.hasFullAccess();
```

---

## StackTraceUtils

Utility class with static helper methods for inspecting the current call stack, formatting exception stack traces, capturing thread dumps, and locating heap dump files. All methods are static. Trace results automatically exclude common framework packages (Tomcat, Spring, Spring Security) so that only application-level frames are returned.

### Call stack inspection

#### whoCalledMe()
- Returns: `StackTraceElement`
- Returns the StackTraceElement of the method that called the current method (two frames up in the stack). Returns null if the stack is too shallow.

#### whoseCalledMe()
- Returns: `List<StackTraceElement>`
- Returns the full list of StackTraceElement entries from the calling frame upward. Returns an empty list if the stack is too shallow.

#### isCallingMe(String className)
- Returns: `boolean`
- Returns true if any frame in the current call stack has a class name that starts with className.

#### isCallingMe(String className, String methodName)
- Returns: `boolean`
- Returns true if any frame in the current call stack matches both the given class name prefix and the exact method name.

#### logTrace(Logger log)
- Returns: `void`
- Logs the current call stack at WARN level using the supplied SLF4J Logger, one frame per line. Framework frames (Tomcat, Spring, etc.) are omitted.

### Stack trace formatting

#### getTrace()
- Returns: `String`
- Returns the current call stack as a formatted string, excluding framework frames.

#### getTrace(Throwable t)
- Returns: `String`
- Returns the stack trace of the given throwable as a formatted string, excluding framework frames.

#### getTrace(Throwable t, int row)
- Returns: `String`
- Returns the stack trace of the given throwable starting from frame index row, excluding framework frames.

#### getTraceList(Throwable t)
- Returns: `List<String>`
- Returns the stack trace of the given throwable as a list of strings, one per frame, excluding framework frames.

#### getTraceList(Throwable t, int row)
- Returns: `List<String>`
- Returns the stack trace of the given throwable as a list of strings starting from frame index row, excluding framework frames.

#### toString(Throwable t)
- Returns: `String`
- Converts the full exception stack trace (including message and all frames) to a plain text string, equivalent to printing it to a PrintWriter.

### Thread and heap diagnostics

#### getThreadDump()
- Returns: `String`
- Returns a full thread dump of all live threads in the JVM, including lock and monitor information. Useful for diagnosing deadlocks or hung threads.

#### searchHprofFiles()
- Returns: `List<File>`
- Searches the Tomcat home directory (catalina.home system property) for heap dump files ending in .hprof. Returns an empty list if catalina.home is not set or is not accessible.

Example:
```java
package com.openkm;

import com.openkm.util.StackTraceUtils;
import java.io.File;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        boolean calledFromAction = StackTraceUtils.isCallingMe("com.openkm.automation.action");
        System.out.println("Called from action: " + calledFromAction);

        try {
            List<String> keys = null;
            for (String key : keys) {}
        } catch (Exception e) {
            System.out.println(StackTraceUtils.toString(e));
            System.out.println(StackTraceUtils.getTrace(e));
        }

        System.out.println(StackTraceUtils.getThreadDump());

        List<File> hprofs = StackTraceUtils.searchHprofFiles();
        hprofs.forEach(f -> System.out.println("Heap dump: " + f.getAbsolutePath()));
    }
}
```

## TemplateUtils

Utility class with static helper methods for processing FreeMarker templates and performing simple variable substitution. Templates are loaded from the OpenKM home directory (Config.HOME_DIR). All methods are static.

### FreeMarker configuration

#### getConfig()
- Returns: `Configuration`
- Returns the singleton FreeMarker Configuration object, initializing it on first call. The configuration loads templates from Config.HOME_DIR.

### Template existence check

#### templateExists(String name)
- Returns: `boolean`
- Returns true if a FreeMarker template with the given name exists in Config.HOME_DIR.

### Template variable replacement

#### replace(String name, String template, Map<String, Object> model)
- Returns: `String`
- Processes a FreeMarker template string using the given model and returns the rendered result. name is used only as an identifier for error messages.

#### replace(String name, InputStream input, Map<String, Object> model, OutputStream out)
- Returns: `void`
- Reads a FreeMarker template from input, processes it with the given model, and writes the rendered result to out.

#### replace(String template, Map<String, String> model)
- Returns: `String`
- Replaces all occurrences of ${key} in the template string with the corresponding value from model. This is a simple literal string substitution (no FreeMarker). Values are trimmed before substitution.

### Variable helpers

#### toVar(String id)
- Returns: `String`
- Wraps the given identifier in ${...} to produce a template variable reference. For example, toVar("user") returns "${user}".

### Built-in templates

#### useMailTemplate(String body)
- Returns: `String`
- Wraps the given HTML body in the built-in OpenKM mail template (/tpl/mail.ftlh) and returns the fully rendered HTML. Throws IOException on template processing errors.

#### useNotesTemplate(String docName, List<NodeNote> notes)
- Returns: `String`
- Renders the built-in OpenKM notes template (/tpl/notes.ftlh) with the given document name and list of notes. Returns the rendered HTML. NodeNote has fields: author, date, text.

Example:
```java
package com.openkm;

import com.openkm.core.Config;
import com.openkm.util.TemplateUtils;
import com.openkm.util.TemplateUtils.NodeNote;

import java.util.*;

public class Test {

    public static void main(String[] args) {
        try {
            // FreeMarker replace
            Map<String, Object> model = new HashMap<>();
            model.put("fileIn", "/tmp/input.xls");
            String cmd = TemplateUtils.replace("SYSTEM_XLS2CSV", Config.SYSTEM_CATDOC_XLS2CSV, model);
            System.out.println("Command: " + cmd);

            // Simple string substitution
            Map<String, String> vars = new HashMap<>();
            vars.put("user", "jsmith");
            String msg = TemplateUtils.replace("Hello ${user}!", vars);
            System.out.println(msg); // "Hello jsmith!"

            // Built-in templates
            String html = TemplateUtils.useMailTemplate("<p>Document approved.</p>");
            System.out.println(html);

            NodeNote n = new NodeNote();
            n.setAuthor("jsmith");
            n.setDate("2024-03-15");
            n.setText("Reviewed and approved.");
            String notesHtml = TemplateUtils.useNotesTemplate("report.pdf", List.of(n));
            System.out.println(notesHtml);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

## WorkflowUtils

Utility class with static helper methods for querying workflow process instances and their log entries. All methods are static and may throw WorkflowException if the workflow engine is unavailable.

### Process instance queries

#### findProcessInstancesByNode(String uuid)
- Returns: `List<ProcessInstance>`
- Returns all process instances associated with the repository node identified by the given UUID. The UUID can belong to a document, folder, mail, or record. Returns an empty list if the UUID is null or no instances are found.

#### findLogsByProcessInstance(long processInstanceId)
- Returns: `List<ProcessInstanceLogEntry>`
- Returns the log entries associated with the given process instance ID, sorted by date. Each entry contains: processDefinitionId, processDefinitionName, processInstanceId, token, date, type, info.

#### getRunningWorkflowNodes()
- Returns: `Set<String>`
- Returns the set of repository node UUIDs that have at least one running (not yet ended) workflow process instance. Returns an empty set if no workflows are currently running or if the workflow engine is unavailable.

### Process instance conversion

#### copy(ProcessInstanceRef piRef)
- Returns: `ProcessInstance`
- Converts a jBPM-specific ProcessInstanceRef adapter object to an OpenKM ProcessInstance bean. Used internally by the workflow adapter layer.

### Inner classes

**ProcessInstanceLogEntry** — data class with fields: processInstanceId (long), processDefinitionId (long), processDefinitionName (String), token (String), date (Date), type (String), info (String).

**DiagramInfo** — holds layout info for a workflow diagram: width (int), height (int), nodeMap (Map<String, DiagramNodeInfo>). Method getNodes() returns an unmodifiable list of DiagramNodeInfo.

**DiagramNodeInfo** — layout info for a single diagram node: name (String), x (int), y (int), width (int), height (int).

Example:
```java
package com.openkm;

import com.openkm.bean.workflow.ProcessInstance;
import com.openkm.util.WorkflowUtils;
import com.openkm.util.WorkflowUtils.ProcessInstanceLogEntry;
import java.util.Set;

public class Test {

    public static void main(String[] args) {
        try {
            String uuid = "d50133e3-dbfa-4d01-a109-28785cd48f40";
            for (ProcessInstance pi : WorkflowUtils.findProcessInstancesByNode(uuid)) {
                System.out.println("Process instance: " + pi.getId());
                for (ProcessInstanceLogEntry entry : WorkflowUtils.findLogsByProcessInstance(pi.getId())) {
                    System.out.println("  [" + entry.getDate() + "] " + entry.getType() + ": " + entry.getInfo());
                }
            }
            Set<String> running = WorkflowUtils.getRunningWorkflowNodes();
            System.out.println("Nodes with active workflows: " + running.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

## ShardUtils

ShardUtils is a Spring service that provides methods for managing the OpenKM shard configuration. Shards allow the document repository to be distributed across multiple physical disks or directories. Access via ContextWrapper.getContext().getBean(ShardUtils.class) or @Autowired.

Use getShardIds() to list available shard IDs before calling methods that require a shardId parameter.

### Shard configuration

#### setCurrentShard(Long shardId)
- Returns: `void`
- Sets the active shard to the given shard ID. Updates the configuration in the database and reloads it across the cluster if clustering is enabled. Throws RepositoryException if the shard ID is not registered.

#### createShardDirs(Long shardId)
- Returns: `void`
- Creates the required storage directories for the given shard ID across all tenants. Should be called after registering a new shard in the database.

### Moving nodes between shards

#### changeNodeShard(String uuid, Long newShardId)
- Returns: `void`
- Moves the physical content of the given repository node to the target shard. Behaviour varies by node type: documents move all version files and extraction; mails move content, extraction, and child attachments recursively; folders/records apply recursively. Throws RepositoryException if the shard is not registered, IOException if a file move fails.

### Querying shards

#### getShardIds()
- Returns: `List<Long>`
- Returns the list of all registered shard IDs.

#### getShardSize(Long shardId)
- Returns: `Long`
- Returns the total size in bytes of all content stored in the given shard across all tenants.

#### getShardPaths(Long shardId)
- Returns: `List<String>`
- Returns the list of absolute filesystem paths for the given shard, one per tenant.

#### getShards()
- Returns: `List<Shard>`
- Returns the list of all registered Shard entities from the database.

#### getShardFolders()
- Returns: `List<File>`
- Returns the list of shard directories found under Config.REPOSITORY_HOME, identified by the configured shard prefix, sorted alphabetically.

Example:
```java
package com.openkm;

import com.openkm.db.bean.Shard;
import com.openkm.util.ContextWrapper;
import com.openkm.util.ShardUtils;
import java.io.File;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        try {
            ShardUtils shardUtils = ContextWrapper.getContext().getBean(ShardUtils.class);
            for (long id : shardUtils.getShardIds()) {
                System.out.println("Shard " + id + " size: " + shardUtils.getShardSize(id) + " bytes");
                System.out.println("Shard " + id + " paths: " + shardUtils.getShardPaths(id));
            }
            shardUtils.getShards().forEach(System.out::println);
            shardUtils.changeNodeShard("e2391b01-ce10-42c7-94a9-b0d6b394048e", 2L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

## LegacySrv

LegacySrv is a Spring service that provides direct database access methods for executing raw SQL and HQL (Hibernate Query Language) queries. Package: com.openkm.db.service. Access via ContextWrapper.getContext().getBean(LegacySrv.class) or @Autowired.

WARNING: Direct SQL/HQL execution bypasses OpenKM security and permission checks. Only a single statement per call is supported. Admin privileges required.

### SQL execution

#### executeSQL(Reader rd)
- Returns: `void`
- Reads and executes an SQL script from the given Reader, one statement per line. Lines starting with -- and blank lines are skipped. Errors per line are logged but do not abort the script.

#### executeSQL(String query)
- Returns: `List<List<String>>`
- Executes a single SQL statement. Returns results as a list of rows (each row is a list of column values as strings). BLOB columns are represented as "BLOB". For non-SELECT statements, returns the affected row count as a single-row result.

#### executeSQL(String query, Object... params)
- Returns: `List<List<String>>`
- Parameterized variant using ? placeholders. Supported types: String, Integer, Long, java.sql.Date, java.sql.Time, java.sql.Timestamp, Boolean.

#### executeSqlAsMap(String query)
- Returns: `List<Map<String, Object>>`
- Executes a SQL SELECT statement. Returns results as a list of maps (column name → typed Java object). Type mapping: BIGINT→Long, INTEGER→Integer, TIMESTAMP→Timestamp, BLOB→"BLOB", others→String.

#### executeSqlAsMap(String query, Object... params)
- Returns: `List<Map<String, Object>>`
- Parameterized variant of executeSqlAsMap.

### HQL execution

#### executeHQL(String hql)
- Returns: `List<Object>`
- Executes a single HQL statement. For SELECT/FROM queries, returns matching entities or Object[] projections. For UPDATE/DELETE/INSERT, returns a single Integer with the affected row count.

Example:
```java
package com.openkm;

import java.util.List;
import java.util.Map;
import com.openkm.db.service.LegacySrv;
import com.openkm.util.ContextWrapper;

public class Test {

    public static void main(String[] args) {
        try {
            LegacySrv legacySrv = ContextWrapper.getContext().getBean(LegacySrv.class);

            // SQL query
            List<List<String>> rows = legacySrv.executeSQL(
                "SELECT NBS_UUID, NBS_AUTHOR FROM OKM_NODE_BASE WHERE NBS_AUTHOR = ? LIMIT 5;", "jsmith");
            rows.forEach(r -> System.out.println("uuid=" + r.get(0) + " author=" + r.get(1)));

            // SQL as map
            List<Map<String, Object>> maps = legacySrv.executeSqlAsMap(
                "SELECT NBS_UUID, NBS_SIZE FROM OKM_NODE_BASE LIMIT 5;");
            maps.forEach(r -> System.out.println("uuid=" + r.get("NBS_UUID") + " size=" + r.get("NBS_SIZE")));

            // HQL query
            List<Object> results = legacySrv.executeHQL(
                "SELECT nb.uuid, nb.author FROM NodeBase nb WHERE nb.name = 'okm:root'");
            for (Object obj : results) {
                if (obj instanceof Object[]) {
                    Object[] cols = (Object[]) obj;
                    System.out.println("uuid=" + cols[0] + " author=" + cols[1]);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```
