diff --git a/AI Skills/README.md b/AI Skills/README.md new file mode 100644 index 0000000..1819aad --- /dev/null +++ b/AI Skills/README.md @@ -0,0 +1,33 @@ +# AI Skills for OIE developers + +Skills for AI coding agents that read `SKILL.md` files — they help you **build and review** Open +Integration Engine plugins and channels. Each skill is a folder; a `SKILL.md` holds the instructions and +(where present) a `references/` folder holds detail loaded on demand. + +| Skill | Use it to… | +|---|---| +| [`oie-plugin-development/`](./oie-plugin-development/) | **Build** a plugin: Maven project setup, `plugin.xml`, server/client/shared classes, REST servlets, DB migrations, packaging & signing. | +| [`oie-plugin-code-review/`](./oie-plugin-code-review/) | **Review Java/plugin code** — smells, thread/resource safety, JDBC & migrations, security/PHI, plugin lifecycle, tests, static-analysis findings. | +| [`oie-channel-code-review/`](./oie-channel-code-review/) | **Review channel/template JavaScript** (the Rhino runtime) — block-scoping traps, E4X/HL7 field access, scope-map lifetime, per-message cost, PHI in logs. | + +Plugin *building* and channel *JavaScript* are different runtimes with different rules, so the two review +skills are deliberately separate — use the one that matches what you're reviewing. + +Each `SKILL.md` opens with YAML frontmatter — `name` and `description` are the portable core; the review +skills also carry optional tool-control keys (`allowed-tools`, `disable-model-invocation`). Those extra keys +are optional metadata that agents which don't recognize them simply ignore. + +## Install +Copy the skill folder(s) you want into the skills directory your agent reads (global or per-project — see +your tool's docs). For example: + +```bash +cp -r "AI Skills/oie-plugin-development" / +``` + +Then invoke it by name or let your agent pick it up from the `SKILL.md` description. + +## Credit +These skills are adapted from work by **[@pacmano1](https://github.com/pacmano1)** — the OIE Plugin +Development Guide and the "Irritable Developer Check" review checklist. Thank you. Content has been +reorganized into the skill format (and the two review skills split by runtime); the domain knowledge is his. diff --git a/AI Skills/oie-channel-code-review/SKILL.md b/AI Skills/oie-channel-code-review/SKILL.md new file mode 100644 index 0000000..c4fdc7e --- /dev/null +++ b/AI Skills/oie-channel-code-review/SKILL.md @@ -0,0 +1,130 @@ +--- +name: oie-channel-code-review +description: Review OIE/Mirth channel & code-template JavaScript (the Rhino runtime) the way an experienced, AI-skeptical developer would — Rhino/ES5 constraints and the one real block-scoping trap, E4X/HL7 field access, scope-map lifetime, per-message cost, unclosed DB connections, PHI in logs, and channel test coverage. Run before deploying a channel or when asked to review transformer/filter/code-template JS. For Java/plugin/server code, use the oie-plugin-code-review skill instead. +disable-model-invocation: true +allowed-tools: Grep Read Bash +--- + +# Irritable Developer Check — OIE channel / JavaScript (Rhino) + +Review channel and code-template JavaScript the way an experienced developer would before deploying it. Be +specific and cite `file:line`. Favor catching real problems over being agreeable. + +> **Credit:** adapted from the **"Irritable Developer Check" by [@pacmano1](https://github.com/pacmano1)** — +> the checklist and its hard-won judgment are his. This is the **channel/JavaScript (Rhino)** half; +> Java/plugin/server review lives in the companion **`oie-plugin-code-review`** skill (different runtime, +> different rules). Thank you. +> +> The block-scoping rule below intentionally differs from the original "`var` only" guidance: it targets the +> *actual* Rhino loop bug while keeping modern `const`/`let` everywhere they're safe (rationale is in the +> rule itself). + +**First, ask which scope to review, and wait for the answer:** recent changes (git diff) vs. the full set of +channel/template scripts. If it's not a git repo, ask for the paths (exported channel XML, code-template +files, or a `src/` of deployed JS). + +## Principle: follow JS best practice; deviate only where Rhino forces it +Channels, code templates, transformers, and filters run as JavaScript on **Rhino** (ES5-era, partial ES6, +compiled to JVM bytecode) with **Java interop**. That forces a *specific, documented* set of deviations from +modern JS — listed below. **Flag violations of those forced rules.** Do **not** flag modern JS that runs +fine on Rhino, and do **not** push `var`-for-its-own-sake or other downgrades: over-correcting away from +best practice is its own smell. Every forced deviation should be understood (and ideally commented) as *why*, +not cargo-culted. + +## Reviewer self-check `[BIAS]` +Before reporting, check yourself — these matter more than any single rule. +- **False positives to look thorough**: don't invent findings to seem diligent. A clean category reported clean is a real result. +- **Confident fabrication**: don't claim a global exists or that code does X from memory. Read the line or grep for it; cite the `file:line` you actually looked at. +- **Sycophancy**: when the author defends the code, re-check the code, not the argument. Hold the finding until the code changes your mind. +- **Over-engineering as "improvement"**: don't flag "this could be a class/framework" when the direct code is correct. Flag simplification, never elaboration. +- **Theory-chasing / anchoring**: confirm the actual root cause before proposing a fix. (Classic trap: days lost on TLS/cipher theories when the real bug was a `const` reused inside a compiled Rhino loop.) +- **Imposing mainstream idioms over what runs on Rhino**: match the runtime's reality. But the inverse is also a bug — don't downgrade correct, Rhino-safe modern JS to legacy forms out of superstition. +- **Severity inflation**: separate correctness from taste; say which is which. +- **Prematurely dismissing real findings**: don't wave a genuine problem away as "pre-existing" or "later." Report it; let the author decide. + +## Rhino runtime rules `[RHINO]` +These differ from Java and catch bugs the Java sections never will. Confirm exact signatures/globals against +the OIE version in use. + +- **Block scoping inside loops — the one real trap.** Default to `const`/`let` (modern best practice: no + hoisting surprises, no accidental reassignment). The *forced* exception: Rhino does **not** create a fresh + per-iteration binding for a `const`/`let` declared **inside a loop body** — it hoists one binding to + function scope, shared across iterations. What that shared binding does depends on the keyword: + - **`const` is the real bug**: it can't be reassigned, so every iteration keeps the first value (canonical + case: a `while` over HTTP response headers pushing six identical `Date`s) — and some builds throw + "redeclaration of const" on the second pass. **Flag `const` declared-and-reused inside a loop body.** + - **`let` is reassigned each iteration**, so reading it *within the same iteration* is correct — **prefer + `let` there.** The one case where the shared binding still bites: a **closure created inside the loop** + that captures the variable (all closures then see the final value). For that, iterate with `.forEach()` — + its callback parameter is a genuine per-iteration scope. + + Do **not** flag `const`/`let` at function scope, and do **not** rewrite working code to `var` — that + discards block-scoping safety to dodge one bug. (Some report the same mis-alias in `if`/`switch` blocks on + certain builds — verify against your Rhino version; a binding declared inside any block and reused is the + first suspect when values behave oddly.) +- **Watch for the ES6 features Rhino handles poorly**: template literals, `async`/`await`, `Promise`, + optional chaining `?.`, nullish `??`, spread in parameter lists, and `for...of` are unreliable or missing + on the bundled Rhino — flag them and use `+`/`.join()`, callbacks/retries, plain `try/catch`, `||`, and + indexed/`.forEach()` loops instead. (Support is language-version dependent — OIE defaults Rhino to ES6, so + `let`/`const`/arrow functions *do* work; confirm borderline features against the server in use.) +- **Use the channel `logger`, not `System.out`/`System.err`** — output goes through the provided `logger` at + the right level, never `println`. And never log HL7 content, PHI, or credentials (see `[SECURITY]`). +- **Unclosed database connections**: a connection from `DatabaseConnectionFactory` must be closed in a + `finally`. One leak per message drains the pool and eventually wedges the channel. +- **Scope-map misuse**: `channelMap`/`connectorMap`/`sourceMap` are per-message and don't persist to the next + message; `globalMap`/`globalChannelMap` live in memory until restart, so large/unbounded objects parked + there are a slow leak. Pick the narrowest scope that still carries where you need it. (`globalChannelMap` + can't store `null` — guard reads with `|| {}`.) +- **E4X / HL7 access assumes fields exist**: a missing segment/field comes back as an empty `XMLList`, not + `null`, so `if (field == null)` never fires and a naive `.toString()` yields `""` that reads like + real-but-empty data. Guard with an existence/length check before trusting a value. +- **Java/JavaScript type traps**: values from `msg`, the maps, or Java calls are often Java objects, not JS + primitives. `==` between a Rhino string and a Java `String`, or between two E4X nodes, doesn't compare what + you expect; coerce explicitly (`String(x)`, `.toString()`) and compare deliberately. +- **Per-message expense**: compiling a regex, constructing a `SimpleDateFormat`, or opening a connection on + *every* message runs at full channel throughput. Hoist what you safely can (mind thread-safety for anything + shared — a `SimpleDateFormat` is not thread-safe). + +## Channel data access `[DATA]` +- **Query in a loop / N+1**: transformers iterate HL7 segments and messages, so a per-item DB lookup turns + one message into hundreds of queries. Batch or set-based instead. +- **No statement timeout**: a query with no `setQueryTimeout` can hang the channel's worker thread. +- **Long/wide work inside a live connection**: keep DB work short; close in `finally` (above). + +## Security `[SECURITY]` +Channels carry HL7/PHI outward by design. +- **Logging PHI/secrets**: never log message content, patient identifiers, credentials, or tokens — not even + at debug. Healthcare data does not belong in logs. +- **SQL by string concatenation**: use parameterized queries via `DatabaseConnectionFactory`, never + message-interpolated SQL. +- **Unreviewed PHI egress**: any HTTP/API/LLM/analytics call that ships message content to an external + service needs a BAA-covered destination or a non-PHI/redacted model. OIE routes data outward by design, so + unreviewed egress is a real HIPAA exposure. +- **PHI in outbound URLs**: identifiers in a path/query string land in proxy/server logs — use the body. + +## Tests `[TEST]` +- **Untestable-by-design logic**: Rhino code can't run under a Node test runner directly. Pure logic should + live in functions a test can call with **mocked OIE globals** (`msg`, maps, `logger`); flag transformers + that bury testable logic inline with no seam. +- **Happy-path only**: no test for the missing-segment, empty-`XMLList`, or error branch — exactly where the + E4X and type traps above bite. +- **A bug fix with no regression test that fails without the fix.** + +## Six-month regrets `[REGRET]` +Decisions whose cost arrives later — name the future moment each bites. +- **Forced-deviation with no "why" comment**: a `var`/`let`-in-loop or `.join()`-instead-of-template-literal + that a future reader will "clean up" back into a bug because nothing says it was deliberate. +- **Reasoning that lives outside the repo/channel**: why a value is coerced, why a scope map is used — if it's + only in someone's memory, the next editor breaks it. Put it in a comment or a committed note. +- **"Temporary" channel hacks with no removal trigger**: a hardcoded endpoint, a disabled filter, a `// TODO` + with no ticket — temporary plus no trigger equals permanent. + +## Reporting findings +Open with a one-line verdict: **ship / ship with caveats / block**, and the one or two risks that drive it. +Then the findings, sorted worst-first. Tag each with its category — `[RHINO]`, `[DATA]`, `[SECURITY]`, +`[TEST]`, `[REGRET]` — a **severity** (`[Blocker]`/`[Should]`/`[Nit]`), and a **confidence** +(`[Confirmed]` = you read/ran it; `[Suspected]` = needs a closer look; labeling a guess `[Suspected]` is +required). Auto-`[Blocker]`: PHI/secrets in logs or outbound URLs, unreviewed PHI egress, a `const`-in-loop +reuse bug, or SQL built by concatenation. + +If a category is clean, say so briefly — "clean on scope-maps and E4X guards" is a useful result; don't pad. diff --git a/AI Skills/oie-plugin-code-review/SKILL.md b/AI Skills/oie-plugin-code-review/SKILL.md new file mode 100644 index 0000000..b67649c --- /dev/null +++ b/AI Skills/oie-plugin-code-review/SKILL.md @@ -0,0 +1,245 @@ +--- +name: oie-plugin-code-review +description: Review OIE/Mirth (or general Java) plugin/server code the way an experienced, AI-skeptical developer would: code smells, stray/unused declarations, error handling, resource and thread safety, database access and transactions, migration safety, security, serialization, API contracts, tests, static-analysis findings, and OIE plugin-specific gotchas. Run before a release or when asked to review Java/plugin code. For channel/template JavaScript (Rhino), use the oie-channel-code-review skill instead. +disable-model-invocation: true +allowed-tools: Grep Read Bash +--- + +# Irritable Developer Check — Java / OIE plugin code + +Review code the way an experienced, AI-skeptical Java developer would before signing off on a release. Be specific and cite `file:line`. Favor catching real problems over being agreeable. + +> **Credit:** adapted from the **"Irritable Developer Check" by [@pacmano1](https://github.com/pacmano1)** — +> the checklist and its hard-won judgment are his. This is the **Java / plugin** half; channel/template +> JavaScript (Rhino) review lives in the companion **`oie-channel-code-review`** skill (different runtime, +> different rules). Thank you. + +**First, ask which scope to review, and wait for the answer before reviewing anything:** + +- **Recent work**: uncommitted changes plus recent commits. Check `git status` and `git diff`, diff against the base branch (e.g. `git diff main...`), and for committed work use `git diff HEAD~1` or the branch's commit range. Then widen to the files and declarations those changes touch. +- **Full source tree**: the entire codebase, not just what changed. + +Ask this as a quick either/or up front. Only skip the question if the user already told you which scope they want. If the project is not a git repo, ask for the paths to review. + +## Reviewer self-check `[BIAS]` + +Before reporting, check yourself. These are the ways an AI reviewer gives a wrong answer, and they matter more than any single code rule. + +- **False positives to look thorough**: don't invent findings so the review feels diligent. A clean category reported clean is a real result. Never manufacture concern to fill space. +- **Confident fabrication**: don't claim a library has a method, or that code does X, from memory. Read the line or grep for it first. Cite the `file:line` you actually looked at, not what you assume is there. +- **Sycophancy**: when the author explains or defends the code, re-check against the code, not the explanation. Agreement is not verification. Hold the finding until the code changes your mind, not the argument. +- **Over-engineering as "improvement"**: do not flag "this could be a strategy pattern / dependency injection / an interface" when the direct code is correct. Complexity is a cost. Flag simplification, never elaboration. +- **Novelty bias**: don't push the newest idiom because it is new. A record, stream, or switch expression is a tool, not a goal. Recommend it only when it is clearly better here. +- **Theory-chasing / anchoring**: don't latch onto the first plausible cause and chase it. Confirm the actual root cause before proposing a fix. (The classic trap: days lost on TLS/cipher theories when the real bug was `const`/`let` in a compiled Rhino loop.) +- **Severity inflation**: don't dress a cosmetic nit as a bug. Separate correctness from taste, and say which is which. +- **Imposing mainstream idioms over the codebase's own conventions**: match the surrounding code, don't normalize it to the "standard" way you were trained on. The canonical trap here is rewriting working OIE code to use `const`/`let` when the module deliberately uses `var` for Rhino compatibility. +- **Rule-gaming instead of fixing**: treat a change that adds `@SuppressWarnings`, `//NOSONAR`, or loosens a Checkstyle/SpotBugs/PMD/IntelliJ rule to make a violation pass as a finding in itself, not a resolution. Suppressing the alarm is not fixing the fire. +- **Prematurely dismissing real findings**: watch yourself using "pre-existing," "out of scope," or "we'll fix it later" to wave away a genuine problem. Report it and let the author decide; don't rationalize it out of the review. + +## Code smells `[SMELL]` + +- **Duplicated logic**: near-identical methods that should share a traversal, visitor, or callback. +- **Too many parameters**: methods with 5+ params that should take a request/config object or a builder. +- **God classes**: one class doing everything, 500+ lines. +- **Magic numbers/strings**: hardcoded values without constants or explanation. +- **Commented-out code**: dead code left "just in case." +- **Inconsistent naming**: mixing conventions (`getData` vs `fetchInfo` vs `retrieveResult` for the same pattern). +- **Code that "looks generated"**: repetitive structure, boilerplate-heavy, em-dash-laden prose, or patterns a human wouldn't write. +- **Deep nesting**: 4+ levels of `if`/`for`/`try` that should be flattened with guard clauses and early returns. +- **Long methods**: a single method running ~50+ lines through many sequential steps that should be extracted, even when the enclosing class is small. +- **Comments that restate the code**: `count++; // increment counter`. A comment should capture intent or the reason, not narrate the obvious; redundant narration is a classic generated-code tell. + +## Strays: leftover or unused declarations `[STRAY]` + +- **Unused declared dependencies**: every dep in `pom.xml` / `build.gradle` / `package.json` / `Cargo.toml` should actually be imported. For each declared dep, `grep -rln "import .*"` across `src/` and `test/`; zero hits means delete the dep AND its version property. A `WARNING` about a transitive POM often traces back to an unused direct dep. (Real example: `org.reflections:reflections:0.9.10` was a test dep no test imported; deleting it killed a javassist POM warning for free.) +- **Unused imports**: check every touched file. +- **Unused locals, parameters, private fields, and private methods**: declared but never read or called. Confirm with "Find Usages" or `grep -rn "name"`. +- **Orphan files**: source files no longer referenced by any classpath / module / build config; common after a refactor or rename. +- **Stale TODO / FIXME comments**: references to resolved issues, dropped features, or dead tickets. +- **Config keys / properties that nothing reads**: settings in `application.properties` / `*.yml` / `pom.xml` `` never accessed by code. The version property left dangling after deleting its consumer is the canonical case. +- **Empty packages / directories**: left behind after the last file moved or was deleted. +- **Committed build artifacts or binaries**: `target/`, compiled `.class` files, generated jars, IDE output, or large binaries that belong in `.gitignore`, not the tree. +- **Vendored source pasted into the tree**: upstream helper classes copied into `src/` that no dependency manifest tracks, so nobody patches them and they rot. Distinct from a declared-but-unused dependency. + +## Error handling `[ERROR]` + +- **Swallowed exceptions**: empty `catch` blocks, or `catch` that only logs and continues when it shouldn't. +- **Catching too broadly**: `catch (Exception e)` / `catch (Throwable t)` where specific exceptions should be handled differently. +- **Throwing raw `RuntimeException`**: use a meaningful custom or standard exception. +- **Dropping the cause when wrapping**: `throw new SomeException(msg, e)`, never `new SomeException(e.getMessage())` (loses the stack trace). +- **Swallowing `InterruptedException`**: at minimum restore the flag with `Thread.currentThread().interrupt()`; don't silently ignore it. +- **Exceptions for control flow**: throwing/catching to handle expected, non-exceptional cases. +- **`return` or `continue` inside `finally`**: silently discards exceptions and surprises everyone. +- **External calls with no timeout or bad retry logic**: network/HTTP/DB calls with no connect and read timeout hang a worker thread indefinitely; retry loops that retry non-retriable errors (4xx, auth failures) or hammer with no backoff make an outage worse. Integration and server code exists to talk to external systems, so this is a real hang-the-thread bug (the BIOTRONIK HTTPS integration is the canonical OIE case). + +## Resource & thread safety `[SAFETY]` + +- **Unclosed resources**: streams, connections, readers not in try-with-resources. +- **Thread safety**: mutable shared state without synchronization, non-thread-safe collections used concurrently, or a shared `SimpleDateFormat` / `DateFormat` (not thread-safe; use a local instance, `DateTimeFormatter`, or a `ThreadLocal`). Server-side code runs multi-threaded. +- **Reach for `java.util.concurrent`**: prefer `ConcurrentHashMap`, `Atomic*`, and `ExecutorService` over hand-rolled synchronization or raw `Thread`s. +- **Leaked executors/threads**: an `ExecutorService` or thread pool not shut down on `stop()` or a shutdown hook; ties into the lifecycle contract. +- **Synchronizing on the wrong lock**: a public field, a mutable object, a `String` literal, or a boxed `Integer`. +- **Wasteful instantiation**: creating expensive objects per-request when they could be shared (mind the thread-safety caveat above). +- **Ignored `Future` from `submit()`**: discarding the `Future` returned by `ExecutorService.submit()` swallows any exception the task threw; keep it and check it, or use `execute()` deliberately with its own error handling. Same for an unhandled `CompletableFuture` exceptional path. +- **Connection-pool misconfiguration**: if the code manages its own JDBC pool, watch for a max-lifetime/recycle set above the database's `wait_timeout` (stale connections after idle or failover), no validation/pre-ping, or an undersized pool paired with long transactions (exhaustion under load). These are classic, hard-to-reproduce production failures. + +## Database access & transactions `[DATA]` + +Much of this code runs against relational databases over JDBC, often from many threads at once. None of this is browser-tier concern; it is where server-side Java actually breaks under load. + +- **N+1 / query in a loop**: one round-trip per item where a single set-based or batched query belongs. OIE transformers iterate HL7 segments and messages, so a per-item lookup turns one message into hundreds of queries. +- **Per-row `execute()` instead of a batch**: many single `PreparedStatement.execute()` calls where `addBatch()` / `executeBatch()` is far faster and lock-lighter for bulk inserts. +- **Long or wide transactions**: a transaction held open across parsing, transformation, or I/O, pinning a connection and holding locks. Keep transactions short. +- **External work inside an open transaction**: an HTTP call, message publish, or async operation made while a DB transaction is live pins the connection for the whole call and risks partial state. Do the external call before opening the transaction where possible. +- **Publish-before-commit (dual-write)**: emitting a message or event and then hitting a DB error that rolls back, leaving an emitted message with no committed record behind it. +- **No statement/query timeout**: a `Statement`/`PreparedStatement` with no `setQueryTimeout`, and no server-side statement timeout. One hung query blocks a worker thread and can stall the pipeline (an OIE channel, a request handler, a batch job). +- **Deadlock-prone lock ordering**: code paths that lock rows in different orders with no bounded retry on deadlock or lock-wait. Parallel messages writing the same tables will deadlock under load otherwise. +- **`SELECT *` / over-fetching in hot paths**: selecting every column wastes the buffer pool, over-fetches PHI, and breaks silently when the schema changes. Select the columns you use. +- **Deep `OFFSET` pagination**: `LIMIT ... OFFSET n` on a large table scans and discards `n` rows and degrades linearly; use keyset/cursor pagination for batch jobs. +- **Dialect-specific SQL assumed portable**: upsert syntax, `SHOW REPLICA` vs `SHOW SLAVE`, MySQL vs MariaDB row-alias forms. OIE connects to Oracle, SQL Server, MySQL/MariaDB, and Postgres, so hardcoded dialect syntax is a correctness trap. + +## Database migration safety `[MIGRATE]` + +For any code that ships schema changes or migrations. Most of these look harmless in the diff and only bite in production or on the next fresh environment build. + +- **`NOT NULL` column added without a default**: locks the table and rewrites every row. Add nullable, backfill, then add the constraint. +- **Index built without `CONCURRENTLY` (Postgres) or online DDL (MySQL)** on a large table: blocks writes for the whole build. +- **DDL and DML mixed in one migration**: hard to roll back and produces long transactions; separate schema and data changes. +- **Editing an already-deployed migration**: deployed migrations are immutable. Altering one causes environment drift; fix forward with a new migration. +- **Large backfill in a single transaction**: works on 100 rows, locks on 10M. Batch it with progress. +- **Irreversible migration with no rollback path**: no `DOWN`, not marked irreversible, and no documented restore. Convert "we can undo it" into a checked artifact. +- **Dropping or renaming a column before its consumers are gone**: the app errors on the missing column. Use expand-contract: add the new, migrate readers/writers, then remove the old. +- **Relying on ORM auto-DDL or drop-and-create in production**: schema mutated on startup silently diverges across environments or destroys data. Ship versioned, additive, backward-compatible migrations instead. + +## Security `[SECURITY]` + +Higher stakes here: this is often server code that handles HL7/PHI and talks to databases. + +- **SQL built by string concatenation**: use `PreparedStatement` with bound parameters, never string-interpolated SQL. +- **Unsafe deserialization**: XStream or Java `ObjectInputStream` on untrusted input is an RCE vector. Allow-list types (that is what `ObjectXMLSerializer.allowTypes()` is for); never deserialize attacker-controlled bytes blind. +- **Logging secrets, PII, or PHI**: never log credentials, tokens, or HL7 message content. Healthcare data does not belong in logs. +- **Hardcoded secrets**: credentials, keys, or tokens in source or config that should come from a secret store. +- **Weak randomness**: `java.util.Random` where security matters; use `SecureRandom` for tokens, IDs, or anything that must be unguessable. +- **XXE / unsafe XML parsing**: disable external entities and DTDs on parsers handling untrusted XML. +- **Path traversal**: validate and normalize any file path built from external input. +- **SSRF via message- or config-controlled outbound URLs**: an outbound HTTP host or URL built from HL7 message content or channel config lets an attacker reach internal services or cloud metadata endpoints. Allow-list target hosts. +- **Command injection**: `Runtime.exec` / `ProcessBuilder` / external-script invocation with arguments built from message or user input. Use an argument array, never a concatenated shell string. +- **Leaking internals in responses or errors**: stack traces, SQL text, internal paths, or PHI returned to a caller, shown in the panel, or shipped to an error-tracking/APM service. Return a generic message with an opaque ID; log the detail server-side only. This is distinct from the "don't log PHI" rule; it is about what leaves the server. +- **PHI in URLs or query strings**: patient identifiers (MRN, name, national ID) placed in the path/query of an outbound HTTP call land in proxy and server logs. Use opaque IDs or the request body. +- **PHI not encrypted in transit or at rest**: TLS missing on a JDBC link or endpoint that carries PHI, or stored messages/attachments left unencrypted. PHI in HL7 rows travels cleartext between OIE and the database otherwise. +- **Over-privileged DB account**: the runtime connection using an admin or `ALL PRIVILEGES` grant turns any SQL-injection or logic bug into a full-schema blast radius. Separate the runtime user from migration/admin work. +- **Trusting forwarded headers for security decisions**: reading `X-Forwarded-For` for auth, rate limiting, or audit without a trusted-proxy setup is trivially spoofed on an HTTP Listener connector. +- **Unverified webhook/callback payloads**: verify the signature before parsing or trusting any field of an inbound callback. +- **Known-vulnerable dependencies**: run OWASP Dependency-Check / Snyk and fail on published CVEs in declared deps. Distinct from the unused-dep hygiene in `[STRAY]`; a shipped dependency can be used and still be a CVE. + +## OIE / Mirth plugin-specific `[OIE]` + +- **Classpath conflicts**: bundling dependencies the OIE server already provides (commons-lang, log4j, etc.); shade or exclude them. +- **Plugin lifecycle misuse**: not respecting `start()`/`stop()` contracts, leaking resources on stop, or doing heavy work in the constructor. +- **Logging**: use the server's logging framework, not `System.out.println` / `System.err.println`; prefer parameterized logging (`log.debug("x={}", x)`) over string concatenation; use correct levels; don't log-and-rethrow (double logging). Never log secrets or PHI (see `[SECURITY]`). +- **Missing `ObjectXMLSerializer` allow-list registration**: every plugin that defines its own `ConnectorProperties` (or any XStream-serialized class) MUST register them via `ObjectXMLSerializer.getInstance().allowTypes(...)`, from both the client entry point (panel constructor) and the server entry point (servlet + dispatcher static block). Without it, channel saves silently break, most visibly as spurious "another user has edited" errors on brand-new channels. Grep for `allowTypes` in `shared/`; if absent, add a `SerializationController`. +- **Unguarded servlet endpoints**: every custom servlet operation a plugin registers must enforce the server's auth/role check, deny-by-default. An operation that exposes data or an action with no authorization gate is a direct PHI or auth-bypass exposure. +- **Plaintext stored connector credentials**: endpoint passwords and API secrets in `ConnectorProperties`/config must go through OIE's `Encryptor` (reversible, since they are replayed to endpoints), never stored or serialized in plaintext. Distinct from the hardcoded-secrets rule; this is about runtime-stored credentials. +- **Non-idempotent reprocessing**: OIE channels redeliver and reprocess messages by design. Writes, jobs, and inbound handlers must be safe to run twice. A plain `INSERT` on reprocess that should be an upsert or a dedup-guarded write is a real defect class here. +- **No fail-fast startup validation**: validate required config and prove dependency connectivity in `start()` and fail loudly, rather than surfacing an NPE or connection error mid-message. Ties into the lifecycle contract above. +- **No audit trail for PHI access**: reads, writes, prints, and exports of patient data should be auditable (who, what, when) via an insert-only log. A plugin that routes or transforms records with no audit hook is a compliance gap. +- **Unreviewed PHI egress**: any path that ships PHI to an external service (cloud API, LLM provider, analytics, error tracker) needs a confirmed BAA-covered destination, or redaction / a non-PHI event model. OIE routes data outward by design, so unreviewed egress is a real HIPAA exposure. + +## API design & contracts `[API]` + +- **Boolean parameters**: `doThing(true, false, true)` is unreadable at the call site; use enums or a config object. +- **Stringly-typed code**: `String` where an enum, type, or value object belongs. +- **Missing `@Override`**: overriding without the annotation. +- **Returning `null` for a collection or array**: return an empty one (`List.of()`, `Collections.emptyList()`) so callers don't have to null-check. +- **No fail-fast validation**: use `Objects.requireNonNull(x, "x")` on constructor/method params instead of letting a later NPE surface far from the cause. +- **`Optional` misuse**: use it as a return type, not for fields or parameters. +- **Leaking internal mutable state**: hand back a defensive copy or unmodifiable view instead of the internal array/collection; copy mutable inputs you store. +- **Declaring concrete types**: declare fields and returns to interfaces (`List`, `Map`), not implementations (`ArrayList`, `HashMap`). + +## Serialization & validation `[SERDE]` + +- **Hand-rolled serialization**: writing JSON/CSV/XML by hand when a library is on the classpath. +- **Incomplete escaping**: HTML, JSON, or SQL escaping that misses characters. +- **Missing input validation**: e.g. invalid regex surfacing as a raw exception instead of a user-friendly message. +- **Breaking a persisted/serialized format**: renaming, removing, or retyping a field in an XStream-serialized `ConnectorProperties` (or any persisted format) breaks already-saved channels with no migration or version guard. High-impact and OIE-specific; the failure shows up as channels that silently won't load. + +## Reinvention `[REINVENT]` + +- **Old-school workarounds**: `int[]` as a mutable holder instead of `AtomicInteger`, raw strings instead of enums. +- **Reinventing the wheel**: utilities that already exist in dependencies or `java.util`. +- **Not using available Java 17 idioms**: a verbose data class where a `record` fits, a long `if/else` chain where a switch expression reads cleaner, manual string building where a text block is clearer. Consider them; don't force them (a plain loop often beats a contrived stream). +- **Boolean-flag soup for mutually exclusive states**: carrying `isLoading` / `hasError` / `result` fields that allow contradictory combinations. Model the states with a sealed interface or enum so impossible states are unrepresentable. + +## Tests `[TEST]` + +- **New or changed logic with no test.** +- **Tests that never assert**, or assert nothing meaningful. +- **Disabled, ignored, or empty tests** left in the suite. +- **Testing implementation details** instead of observable behavior. +- **A bug fix with no regression test that would have caught it**: the accompanying test must fail without the fix, not merely pass after it. Ask whether the test proves the bug, or was written to match the new code. +- **Order-dependent tests**: tests that share mutable static state or depend on execution order, the classic "passes alone, fails in the suite." Common with shared JUnit fixtures. +- **Happy-path-only coverage**: no test for the null, empty, oversized-input, or exception branches. OIE handlers especially need the failure branch tested. +- **Vague test names**: `works()` / `test1()`. A name should state the scenario and the expected outcome. + +## Static-analysis / IntelliJ inspections `[STATIC]` + +Run the real tools first when they exist: the build, the linter, existing static analysis (`mvn`/`gradle` compile and any configured inspections). Report what the tools actually said. Only fall back to eyeballing for what no tool is configured to catch. Don't claim "tests pass" or "it compiles" unless you ran it. + +The things IntelliJ (default profile plus a few stricter inspections) would underline. Many are real bugs, not just style. (Unused declarations live under `[STRAY]`; thread-unsafe shared state lives under `[SAFETY]`.) + +**Likely bugs:** + +- **`==` on Strings or boxed types** instead of `.equals()`: use `Objects.equals(a, b)` for null-safety. +- **Possible `NullPointerException`**: dereferencing a value that can be null; `Optional.get()` without an `isPresent()` / `isEmpty()` check. +- **`float`/`double` for money**: use `BigDecimal`; binary floating point can't represent currency exactly. +- **Integer overflow**: large `int` arithmetic that should be `long`, or use `Math.*Exact`. Compare floating-point with a tolerance, not `==`. +- **Ignored return value**: discarding the result of a side-effect-free method (`str.trim();`, a `stream()` with no terminal op). +- **Constant conditions**: conditions always true or false; unreachable code. +- **`switch` with no `default`, or unintended fall-through.** +- **Missing `serialVersionUID`** on a `Serializable` class. +- **Locale-sensitive calls without a Locale**: `toUpperCase()` / `toLowerCase()` / `String.format()` that should pin `Locale.ROOT`. + +**Cleanup / redundancy:** + +- **Redundant cast, redundant `else` after `return`, redundant `throws`, unnecessary boxing/unboxing.** +- **Diamond operator**: `new HashMap()` can be `new HashMap<>()`. +- **Raw types**: `List` where `List` is meant. +- **`isEmpty()` over `size() == 0`**; `String.isBlank()` where appropriate. +- **String concatenation in a loop**: use `StringBuilder`. +- **Field could be local; method could be `static` or `private`; field or class could be `final`.** +- **`equals()` without `hashCode()`** (or the reverse). +- **Deprecated API usage.** + +## Six-month regrets `[REGRET]` + +What will we be kicking ourselves for in six months? Not bugs, decisions whose cost arrives later. For each hit, name the future moment it bites ("the first Dependabot PR", "the next Gradle major", "when the cert rotates"). + +- **Processes that assume a human starts them**: any workflow that breaks the moment a bot, cron job, or new hire triggers it instead of the author. (Canonical case: a checksum-verification gate that fails every automated dependency-bump PR until a human runs a ritual by hand.) +- **Protection by procedure instead of mechanism**: safety that exists only because docs say "remember to run X before Y." Discipline decays; nothing enforces it. Ask what converts the habit into a check. +- **Documented bridges nobody walks**: an escape hatch or migration path written down but never exercised ("we'll enable it per-library during upgrades"). If no one has walked it once, assume it's a wall with a sign on it. +- **Friction that compounds**: a cost that's fine once but lands weekly: a ten-minute manual step per dependency bump, hand-enumerating transitive deps per new library. Multiply by the real frequency before calling it acceptable. +- **Loosened screws with a timer**: something relaxed because it's harmless *under current assumptions* (a verification scope narrowed, a check disabled "since we don't use that path"). When the assumption changes, nobody will remember the screw. Demand a re-tighten trigger in writing. +- **Pinned to a dying line**: dependencies on an EOL release train (fixes only exist in the next major), dead upstreams with no version to bump to, or libraries whose upgrade path crosses a license cliff (iText 2.x → AGPL is the classic). +- **"Temporary" with no removal trigger**: stubs, tombstones, suppression entries, `.skip`/`@Ignore`, compatibility shims, with no condition or date for removal. Temporary plus no trigger equals permanent. +- **Follow-ups that exist only in conversation**: "we'll do that later" with no filed issue number. No number, no later. +- **Reasoning that lives outside the repo**: decisions explained only in chat, PR comments, or a contributor's memory. The "why is this line like this" archaeology of the future needs it in a committed doc or an ADR. +- **Point-in-time evidence presented as ongoing**: baselines, benchmarks, or audits anchored to a moment, without a clear statement of what's frozen versus what re-verifies continuously. Six months on, someone will cite the stale number as current. +- **Architectural change with no ADR**: a PR that adopts a new framework, library, database, pattern, or schema-design choice with the rationale living only in chat or PR comments. Sharpen the "reasoning outside the repo" point into a concrete trigger: this decision needs an ADR. +- **Stale ADR not marked superseded**: a decision doc that no longer matches the code, with no link to its replacement. A silently-wrong "why" doc is worse than none. + +## Reporting findings + +Open with a one-line verdict: ship / ship with caveats / block, and the one or two risks that drive it. Then the findings. + +Tag each finding with its category and give a location. Tags: `[SMELL]`, `[STRAY]`, `[ERROR]`, `[SAFETY]`, `[DATA]`, `[MIGRATE]`, `[SECURITY]`, `[OIE]`, `[API]`, `[SERDE]`, `[REINVENT]`, `[TEST]`, `[STATIC]`, `[REGRET]`. + +Tag each finding with its category, a **severity** (`[Blocker]` / `[Should]` / `[Nit]`), and a **confidence** (`[Confirmed]` = you read the code or ran it; `[Suspected]` = needs a closer look). Labeling a guess as `[Suspected]` is required, not optional. Sort findings by severity, worst first. + +Some findings are automatically `[Blocker]` regardless of judgment: missing auth on an endpoint exposing sensitive data or PHI, secrets committed to source or written to logs, a non-idempotent path that retry or reprocessing will double-execute, or a high-impact change with no rollback. + +1. **[SECURITY][Blocker][Confirmed] SQL built by string concatenation in `Dao.lookup`** (`Dao.java:88`): switch to a `PreparedStatement` with bound params. +2. **[STRAY][Should][Confirmed] Unused dependency `commons-codec` in `pom.xml`**: no import hits; remove the dep and its `` property. +3. **[OIE][Blocker][Confirmed] Missing `ObjectXMLSerializer.allowTypes()` for a custom `ConnectorProperties`**: channel saves will break; register in client + server entry points. +4. **[REGRET][Should][Confirmed] Dependency bumps need a manual metadata ritual** (`CONTRIBUTING.md`): every future Dependabot PR fails CI until a human runs it; bites at the first bot-generated bump. File the CI auto-regen issue now. + +If a category is clean, say so briefly. "Clean on strays and error handling" is itself a useful result; don't pad with vague concerns. diff --git a/AI Skills/oie-plugin-development/SKILL.md b/AI Skills/oie-plugin-development/SKILL.md new file mode 100644 index 0000000..1eefb9b --- /dev/null +++ b/AI Skills/oie-plugin-development/SKILL.md @@ -0,0 +1,68 @@ +--- +name: oie-plugin-development +description: >- + Develop, scaffold, and build a plugin/extension for Open Integration Engine (OIE) or Mirth Connect. + Use when creating a new OIE/Mirth plugin, setting up its Maven project (root/module POMs), writing the + plugin.xml descriptor, implementing server-side or client-side (Administrator GUI) plugin classes, building + a source/destination connector (subclassing a stock connector, settings panel, TLS-plugin compatibility), + adding a REST servlet endpoint, doing database migrations (Migrator + MyBatis), handling permissions/event + logging/XStream serialization, or packaging/signing the plugin .zip. NOT for engine-core changes or for + channel/template JavaScript. +--- + +# OIE / Mirth plugin development + +Build a plugin (extension) for the Open Integration Engine (OIE) — the open-source fork of Mirth Connect. +The detail lives in [`references/`](./references/), loaded on demand; this file is the map. Read the +reference for the task at hand rather than pulling in the whole guide. + +> **Credit:** this skill is adapted from the **OIE Plugin Development Guide by +> [@pacmano1](https://github.com/pacmano1)** — the hard-won knowledge here is his. Thank you. + +## First, the thing that surprises people +Plugins build with **Maven** (root POM + module POMs), even though the **engine itself builds with Gradle** +(it migrated off Ant in June 2026 — older guides still say Ant). Don't mix them up: use the Maven workflow below for plugin work. You never build +the engine to build a plugin — its artifacts come from a Maven repo (see +[`references/maven-poms.md`](./references/maven-poms.md)), so the engine's own build system is not your concern. + +## Shape of a plugin +A plugin is a multi-module Maven project that produces an installable `.zip`: +- **server** module — classes the engine instantiates (services, REST servlets, migrators). +- **client** module — classes the **Administrator** GUI instantiates (panels, settings). +- **shared** module — types used by both. +- a **`plugin.xml`** descriptor wiring it together, and a **packaging/assembly** step that zips it. + +## Typical workflow (→ reference for each step) +1. **Set up** the project — prerequisites, layout, scaffolding → [`references/project-structure.md`](./references/project-structure.md) +2. **Maven POMs** — root + module POMs, and the build commands → [`references/maven-poms.md`](./references/maven-poms.md) +3. **`plugin.xml`** descriptor → [`references/plugin-descriptor.md`](./references/plugin-descriptor.md) +4. **Server-side** plugin — services, **REST/servlet** endpoints, **permissions**, **event logging** → [`references/server-plugin.md`](./references/server-plugin.md) +5. **Client-side** (Administrator GUI) + **shared** module → [`references/client-and-shared.md`](./references/client-and-shared.md) +6. **Database** — Migrator + MyBatis → [`references/database.md`](./references/database.md) +7. **Package, sign, serialize** — assembly, code signing, XStream → [`references/packaging-signing-serialization.md`](./references/packaging-signing-serialization.md) +8. **Gotchas & a minimal working example** (read early — saves pain) → [`references/gotchas-and-example.md`](./references/gotchas-and-example.md) + +**Building a source/destination connector?** That's a distinct shape (a connector descriptor + settings +panel, often subclassing a stock connector) with its own hard-won lessons — styling, live testing, and +staying compatible with TLS plugins → [`references/connector-plugins.md`](./references/connector-plugins.md). + +## Router (jump straight to a task) +| I'm trying to… | Reference | +|---|---| +| Scaffold a new plugin / understand the layout / prerequisites | `references/project-structure.md` | +| Write or fix the root/module POMs; find the build command | `references/maven-poms.md` | +| Configure `plugin.xml` (classes, resources, version) | `references/plugin-descriptor.md` | +| Add a server service, a REST endpoint, permissions, or event logging | `references/server-plugin.md` | +| Add an Administrator-GUI panel or a shared type | `references/client-and-shared.md` | +| Build a **source/destination connector** (subclass a stock one, settings panel, TLS-plugin compat, live testing) | `references/connector-plugins.md` | +| Add/alter a table (Migrator) or a query (MyBatis) | `references/database.md` | +| Build the `.zip`, sign it (incl. the Administrator Launcher signing wall), or (de)serialize with XStream | `references/packaging-signing-serialization.md` | +| Debug a weird build/runtime problem; see a full tiny example | `references/gotchas-and-example.md` | + +## Notes +- Most references are pacmano1's guide text, split by topic (each footers back to the source). + `connector-plugins.md` and the Administrator-Launcher signing section are **field notes** from building a + connector — additions to, not from, the original guide. +- This skill covers **building the plugin**. For JavaScript that runs *inside a channel* (transformers, + filters, code templates), that's a different runtime (Rhino) with different rules — see the companion + **`oie-channel-code-review`** skill. diff --git a/AI Skills/oie-plugin-development/references/client-and-shared.md b/AI Skills/oie-plugin-development/references/client-and-shared.md new file mode 100644 index 0000000..0f47699 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/client-and-shared.md @@ -0,0 +1,129 @@ +## Client-Side Plugin + +### SettingsPanelPlugin (adds a Settings tab) + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.plugins.SettingsPanelPlugin; + +public class YourSettingsPanelPlugin extends SettingsPanelPlugin { + + private YourSettingsPanel settingsPanel; + + public YourSettingsPanelPlugin(String name) { + super(name); + } + + @Override + public AbstractSettingsPanel getSettingsPanel() { + if (settingsPanel == null) { + settingsPanel = new YourSettingsPanel("Your Plugin", this); + } + return settingsPanel; + } + + @Override + public void start() { } + + @Override + public void stop() { } + + @Override + public void reset() { } + + @Override + public String getPluginPointName() { + return "Your Plugin Settings"; + } +} +``` + +### ClientPlugin (adds task pane actions) + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.plugins.ClientPlugin; + +public class YourClientPlugin extends ClientPlugin { + + public YourClientPlugin(String name) { + super(name); + } + + @Override + public void start() { + // Add a task to the channel list panel + parent.addTask( + "yourAction", // action name + "Your Action Label", // display text + "Description shown on hover.", // tooltip + "", // keyboard shortcut + new ImageIcon( // icon + Frame.class.getResource("images/world.png")), + parent.channelPanel.channelTasks, // task pane + parent.channelPanel // component + ); + + // Optionally add to channel editor tasks too: + // parent.channelPanel.channelEditTasks + } + + @Override + public void stop() { } + + @Override + public void reset() { } + + @Override + public String getPluginPointName() { + return "Your Client Plugin"; + } + + // This method is called when the user clicks the task + public void yourAction() { + // Open a dialog, execute an action, etc. + } +} +``` + +--- + +## Shared Module + +The shared module contains: + +1. **DTOs / Models** - POJOs that travel between server and client via REST +2. **Servlet Interface** - The JAX-RS interface that defines your REST API contract +3. **Constants** - Shared strings, enums, etc. + +### Model classes + +```java +package com.yourorg.yourplugin; + +import java.io.Serializable; + +public class YourModel implements Serializable { + private static final long serialVersionUID = 1L; + + private String id; + private String name; + // ... fields, getters, setters +} +``` + +If you use XStream for serialization (standard for OIE), add the alias annotation: + +```java +import com.thoughtworks.xstream.annotations.XStreamAlias; + +@XStreamAlias("yourModel") +public class YourModel implements Serializable { ... } +``` + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/connector-plugins.md b/AI Skills/oie-plugin-development/references/connector-plugins.md new file mode 100644 index 0000000..ca24434 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/connector-plugins.md @@ -0,0 +1,130 @@ +## Building a connector (source / destination) plugin + +A **connector** is a different kind of plugin from the service/servlet plugins the rest of this skill +describes. These are field notes from building a production destination connector (a multi-endpoint TCP +sender) against OIE 4.5.2 — the pacmano1 guide doesn't cover connectors, so start here for that case. + +### Shape (differs from a service plugin) + +A connector is three classes wired by a **connector descriptor XML** — `source.xml` / `destination.xml` +(a `ConnectorMetaData`), *not* `plugin.xml`: + +- **shared** — `…ConnectorProperties` (extend the stock properties, or `ConnectorProperties`): the + serialized settings object. +- **server** — the `SourceConnector` / `DestinationConnector` (e.g. a `*Dispatcher` / `*Receiver`). +- **client** — a `ConnectorSettingsPanel` (the source/destination settings UI). + +Two things that silently break a connector if you get them wrong: +- Put your classes under `com.mirth.connect.connectors.`. XStream **auto-allows** + `com.mirth.connect.connectors.**`, so you skip the `allowTypes()` registration entirely. +- The descriptor `` must **exactly** equal `ConnectorProperties.getName()`. If it doesn't, the channel + won't bind the connector — the destination is dropped and the channel deploys source-only, with no error. + +### Extend a stock connector — don't reimplement it + +To add behavior to TCP/HTTP/etc., **subclass the stock connector and override only what you must**; let +`super` do the real I/O. For the TCP sender we overrode only `send()` (endpoint selection + failover) and +`replaceConnectorProperties()`; all socket lifecycle, MLLP framing, TLS, and keep-alive came from +`super.send()`. + +**Keep `getProtocol()` unchanged.** The engine resolves per-connector hooks by the **protocol string**. +SSL/TLS is the big one: + +```java +// stock TcpDispatcher.getConfigurationClass() — inherited, do NOT override +configurationController.getProperty(connectorProperties.getProtocol(), "tcpConfigurationClass"); +``` + +Because our properties keep `getProtocol()` = `"TCP"` and we delegate socket creation to `super`, a +third-party TLS plugin that registers `saveProperty("TCP", "tcpConfigurationClass", …)` (e.g. NovaMap's TLS +Manager) is picked up **transparently** — none of our code is involved. Override the protocol and you +silently lose TLS-plugin compatibility. + +**Override-signature narrowing:** a stock override may *narrow* the `throws` clause (e.g. +`TcpDispatcher.send` drops `InterruptedException`). Your override then can't declare it either — on +interrupt, `Thread.currentThread().interrupt()` and return a QUEUED/ERROR response instead of rethrowing. + +### Properties: clone, copy-ctor, and the dirty-check + +The Administrator's "unsaved changes?" check compares `getProperties()` to the saved copy with +**reflection-equals**. So: + +- `clone()` / the copy-constructor must **deep-copy** your fields — lists especially. +- **Compensate stock copy-ctor omissions.** `TcpDispatcherProperties`' copy-constructor drops + `maxConnections`; our copy-ctor re-sets it, or the clone ≠ the original and the panel is "always dirty" + (and a field silently resets). +- Add unit tests: `clone().equals(original)` **and** a serialize → deserialize → `equals` round-trip. + +### Match the stock panel's look (users expect it) + +A connector settings panel sits right next to the stock ones, so it has to match them: + +- **Right-aligned `Label:` + Yes/No `MirthRadioButton` pairs** for booleans — not a row of checkboxes. Put a + related checkbox inline after its field (e.g. `Ignore Response` next to `Response Timeout`). +- `MirthRadioButton` / `MirthCheckBox` render with a **gray** fill on a white panel. Call + `setBackground(Color.WHITE)` on each (and on the panel). +- Style every table like the rest of OIE: + ```java + table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); + table.setRowHeight(UIConstants.ROW_HEIGHT); + table.setSortable(false); + table.setDragEnabled(false); + table.getTableHeader().setReorderingAllowed(false); + table.setShowGrid(true, true); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.setCustomEditorControls(true); + if (Preferences.userNodeForPackage(Mirth.class).getBoolean("highlightRows", true)) { + table.setHighlighters(HighlighterFactory.createAlternateStriping( + UIConstants.HIGHLIGHTER_COLOR, UIConstants.BACKGROUND_COLOR)); + } + ``` + …and set the scroll **viewport** white, or the empty area below the last row shows Swing gray: + `scrollPane.getViewport().setBackground(Color.WHITE);` +- `setToolTipText(...)` on every field (use `…` for multi-line). +- Gray out dependent fields to mirror stock (e.g. Send Timeout disabled unless Keep-Connection-Open = Yes). +- **Reuse the transmission-mode SPI** (`TransmissionMode*` public API) for the MLLP/Basic sub-panel. The + stock TCP sender's *other* sub-panels (remote/local address, Test Connection, server mode) are `private` + and cannot be reused — rebuild only what you actually need. + +### Test the connector live (deterministic, CI-able) + +Unit tests can't prove socket behavior. Stand up the real thing: + +- **Docker OIE** — pin the tag (`openintegrationengine/engine:`, **not** `:latest`, which floats to + the next release) — with your built zip mounted at `/opt/engine/custom-extensions/` (the entrypoint + auto-installs any `.zip` there on boot). +- A **controllable sink** — a tiny zero-dependency listener you can command to ACK / NACK / hang / go down — + as the test oracle. +- A **REST driver** that logs in (`POST /api/users/_login`, header `X-Requested-With` required), deploys a + channel, injects messages, and asserts against the sink. + +**Use an exported, known-good channel as the fixture — do NOT hand-serialize channel XML and POST it.** This +is the one that costs *days*: `POST /api/channels` **accepts a structurally-incomplete channel without +normalizing or rejecting it**, deploys it "healthy", and it *partially works*. A channel serialized +programmatically outside the engine (via `ObjectXMLSerializer`, no plugins on the classpath) uses base/default +classes — transmission mode comes out as `FrameModeProperties` instead of the real +`MLLPModeProperties`, connectors have no ``, `queueBufferSize` is `0`. With a **RAW** +datatype it works; with **HL7 v2.x** the destination sends the literal string **`undefined`** on the wire — +no error anywhere. The Administrator normalizes all of this on save. So: **build the channel once in the +Administrator, export it, strip the `` wrapper and `` block, and commit that XML as your +fixture.** (The REST API is fine once the config is complete — create *and* message-inject both work; and +you can freely edit *your own connector's* properties in the exported XML, e.g. the endpoint list.) + +Two path traps that still bite even with a good fixture: +1. **`POST /channels/{id}/messages` binds `destinationMetaDataId` to an *empty set* when omitted → routes to + ZERO destinations** (it dead-ends at the source; your connector never runs). Pass `?destinationMetaDataId=`. +2. **Redeploying a stuck channel can leave it source-only.** Do a clean `_undeploy` → `_deploy` and wait for + the **destination** child to report `STARTED`, not just the channel. + +### Observability (operators will ask "where did it go?") + +- **Log state transitions, not per message.** A per-message WARN floods under a sustained outage — log once + when an endpoint/target goes DOWN and once when it RECOVERS. +- **OIE ships `rootLogger = ERROR`.** Your connector's INFO/WARN is invisible until an operator adds a + `logger..name` / `.level` entry to `conf/log4j2.properties`. Document that in your README. +- **Stamp results where they show up.** Append to the response `statusMessage` and set a **connector-map** + key (e.g. which endpoint received the message) so it appears in the Response and Connector Map views. + +--- +_Field notes from building an OIE connector plugin (2026); complements the pacmano1 guide in the sibling +references. See also `packaging-signing-serialization.md` for the Administrator Launcher signing wall._ diff --git a/AI Skills/oie-plugin-development/references/database.md b/AI Skills/oie-plugin-development/references/database.md new file mode 100644 index 0000000..165f023 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/database.md @@ -0,0 +1,121 @@ +## Database Support (Migrator + MyBatis) + +If your plugin needs to store data in the OIE's internal database, you need a Migrator and MyBatis mappings. + +### Migrator class + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.server.migration.Migrator; +import com.mirth.connect.server.util.DatabaseUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.List; + +public class YourMigrator extends Migrator { + private static final Logger logger = LogManager.getLogger(YourMigrator.class); + + @Override + public void migrate() throws MigratableException { + try { + var dbType = getDatabaseType(); + var sqlFile = switch (dbType) { + case "postgres" -> "postgres-tables.sql"; + case "mysql" -> "mysql-tables.sql"; + case "oracle" -> "oracle-tables.sql"; + case "sqlserver" -> "sqlserver-tables.sql"; + default -> "derby-tables.sql"; + }; + + var sql = new String(getClass().getResourceAsStream("/" + sqlFile).readAllBytes()); + DatabaseUtil.executeScript(sql, false); + } catch (Exception e) { + // "already exists" errors are expected on subsequent startups + logger.debug("Migration note (may be expected): {}", e.getMessage()); + } + } + + @Override + public List getUninstallStatements() { + return List.of("DROP TABLE IF EXISTS your_table"); + } +} +``` + +### Multi-database SQL scripts + +Create one SQL file per database dialect in `server/src/main/resources/`: + +```sql +-- postgres-tables.sql +CREATE TABLE IF NOT EXISTS your_table ( + id CHAR(36) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP +); +``` + +Oracle is the most annoying: no `IF NOT EXISTS`, uses `NUMBER` instead of `INTEGER`, `CLOB` instead of `TEXT`, and `ADD` instead of `ADD COLUMN` for ALTER TABLE. + +### MyBatis sqlmap.xml + +Place in `package/resources/sqlmap.xml`: + +```xml + + + + + + + + + + + + INSERT INTO your_table (id, name, created_at) + VALUES (#id#, #name#, #createdAt#) + + + + + + + + DELETE FROM your_table WHERE id = #id# + + +``` + +### Accessing MyBatis from your repository + +```java +import com.mirth.connect.server.util.SqlConfig; + +public class YourRepository { + private final SqlSessionManager sqlSessionManager; + + public void init() { + this.sqlSessionManager = SqlConfig.getInstance().getSqlSessionManager(); + } + + public List getAll() { + return sqlSessionManager.selectList("YourPlugin.getAll"); + } + + public void create(YourModel model) { + sqlSessionManager.insert("YourPlugin.insert", model); + } +} +``` + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/gotchas-and-example.md b/AI Skills/oie-plugin-development/references/gotchas-and-example.md new file mode 100644 index 0000000..6dc1c11 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/gotchas-and-example.md @@ -0,0 +1,128 @@ +## Gotchas and Lessons Learned + +These are hard-won lessons from building multiple production OIE plugins. + +### Building a connector? Read the connector reference first + +Source/destination **connectors** are a different shape from service/servlet plugins and have their own +traps (subclass a stock connector, keep `getProtocol()` so TLS plugins are inherited, `ConnectorSettingsPanel` +styling, live testing). See **[`connector-plugins.md`](./connector-plugins.md)**. The highest-cost surprises: +- **Unsigned jars deploy but won't open in the Administrator Launcher** ("has no code signers") — sign + self-signed and launch with `-k`. See [`packaging-signing-serialization.md`](./packaging-signing-serialization.md). +- **`MirthRadioButton`/`MirthCheckBox` are gray on a white panel** — `setBackground(Color.WHITE)` on each; + set the table's scroll **viewport** white too. +- **`POST /channels/{id}/messages` with no `destinationMetaDataId` routes to *zero* destinations** — the + message dead-ends at the source and your connector never runs. +- **Don't hand-serialize channel XML for tests.** `POST /api/channels` accepts a structurally-incomplete + channel and it *partially* works (RAW passthrough is fine; HL7 v2.x sends the literal `undefined` on the + wire). Build the channel in the Administrator, **export** it, and use that as the fixture. + +### Dependencies and Classpath + +1. **Mark all OIE-provided dependencies as `provided` scope.** The engine bundles its own versions of Guava, Jackson, Log4j, SLF4j, commons-lang, XStream, MigLayout, SwingX, HikariCP, and many others. If you bundle your own copy, you get classpath conflicts that manifest as bizarre `NoSuchMethodError` or `ClassCastException` at runtime. + +2. **Use assembly.xml `` filters.** Without explicit includes, Maven's dependency plugin will copy every transitive dependency into your staging directory, and the assembly plugin will bundle them all. Your ZIP will contain dozens of JARs the engine already has. The include filter (`your-plugin-*.jar`) ensures only your JARs make it into the ZIP. + +3. **If you must bundle a third-party JAR**, declare it as a `` in plugin.xml and add it to your assembly includes. Test carefully for conflicts with engine-provided versions. + +4. **OGNL / javassist conflict**: If you depend on OGNL, exclude javassist from it (Mirth bundles a higher version). The simple-channel-history plugin works around this by unpacking OGNL classes directly into its client JAR. + +### Serialization + +5. **Call `ObjectXMLSerializer.getInstance().allowTypes()` in both server and client `start()` methods.** If you only do it on one side, the other side will throw `ForbiddenClassException` when deserializing your model classes. This is the single most common "it works on the server but crashes the Administrator" bug. + +6. **Use `serialVersionUID` on all model classes.** Without it, any recompilation can change the generated serial version UID, breaking deserialization of objects serialized by a previous build. + +### Server-Side + +7. **`ChannelPlugin.remove()` and `CodeTemplateServerPlugin.remove()` may receive partial objects.** The channel/template passed to `remove()` may only have its ID populated. If you need the full object (name, content, etc.), look it up via the appropriate controller before using it. + +8. **Don't do heavy work in constructors.** Use `start()` for initialization. The engine instantiates plugin classes early, and constructor failures can prevent the server from starting. + +9. **Fail silently for non-critical operations.** If your plugin intercepts channel saves (like saving history), log errors but don't throw. A bug in your plugin should never prevent a user from saving a channel. + +10. **Use SLF4j / Log4j for logging, never `System.out.println`.** The engine has a logging framework. Use it. + +11. **Be careful with singletons.** If you use a singleton pattern (common for repositories and managers), make sure `stop()` nulls out the instance and releases resources. The engine can restart plugins without restarting the JVM. + +### Client-Side (Swing) + +12. **Use `SwingWorker` for all REST calls from the client.** The Administrator is a Swing application. Blocking the EDT (Event Dispatch Thread) with a REST call will freeze the UI. Always do API calls in a SwingWorker background thread and update the UI in `done()`. + +13. **Extend `JDialog`, not `MirthDialog`.** Despite the name, MirthDialog has quirks. Plain JDialog works better and gives you more control. + +14. **Bind the Escape key to close dialogs.** Users expect this. Register an action on `JComponent.WHEN_IN_FOCUSED_WINDOW` for `KeyEvent.VK_ESCAPE`. + +15. **For large result sets, do a count query first.** The oie-source-code-search plugin does a count before fetching full results. If the count exceeds a threshold (e.g., 5000), show a confirmation dialog. This prevents the UI from locking up on huge result sets. + +### Database + +16. **Write separate SQL scripts for each database dialect.** PostgreSQL, MySQL, Oracle, SQL Server, and Derby all have syntax differences. Oracle is the worst offender: no `IF NOT EXISTS`, no `ADD COLUMN` (just `ADD`), uses `NUMBER` instead of `INTEGER`, and `CLOB` instead of `TEXT`. + +17. **Handle "already exists" errors gracefully in your Migrator.** The `migrate()` method runs every time the plugin starts. Your CREATE TABLE statements will fail on the second startup if you don't use `IF NOT EXISTS` (or catch the error for Oracle/Derby). + +18. **MyBatis uses iBATIS 2 syntax**, not the modern MyBatis 3 mapper syntax. Use `#property#` for parameter binding (not `#{property}`), and `parameterClass` / `resultMap` attributes. + +### REST API + +19. **The REST path is derived from plugin.xml's `path` attribute.** Your servlet endpoints are mounted at `/api/extensions//...`. Make sure the path in plugin.xml matches what your servlet interface declares. + +20. **Set `auditable = false` on read-only `@MirthOperation` annotations.** Otherwise every GET request generates an audit log entry, which creates noise and can slow down the server. + +### Packaging and Distribution + +21. **The plugin ZIP structure matters.** Inside the ZIP there must be a single directory named after your plugin's `path`, containing all JARs, plugin.xml, and any other resources. The structure is: + ``` + your-plugin.zip + └── your-plugin/ + ├── your-plugin-server-1.0.0.jar + ├── your-plugin-shared-1.0.0.jar + ├── your-plugin-client-1.0.0.jar + ├── plugin.xml + └── sqlmap.xml (if applicable) + ``` + +22. **Resource filtering must be enabled for plugin.xml.** The `${project.version}` placeholders in plugin.xml need Maven to substitute them during the build. The `maven-resources-plugin` with `true` handles this. + +23. **After installing a plugin, restart the OIE server.** Plugins are loaded at startup. There is no hot-reload. + +### Testing + +24. **JUnit 5 + Mockito is the standard for newer plugins.** The engine itself uses JUnit 4, but there's no reason your plugin can't use JUnit 5. + +25. **Mock the OIE controllers in tests.** You can't easily spin up a Mirth server in a test. Mock `ControllerFactory`, `SqlConfig`, and other engine singletons to test your logic in isolation. + +26. **Test your permission annotations.** Write a test that reflects over your servlet interface and verifies each method has the correct `@MirthOperation` with the expected permission. This catches permission misconfigurations early. + +### General + +27. **Use `var` freely.** Java 17 is the target. Switch expressions, text blocks, and `var` are all fine. + +28. **No Lombok.** It adds a build-time dependency and can cause issues with the engine's classloader. + +29. **Add SPDX license headers to all source files.** It's good practice and some organizations require it. + +30. **Encrypt sensitive data at rest.** If your plugin stores passwords or secrets, use `ConfigurationController.getEncryptor()` with the `{enc}` prefix convention. Check for the prefix before encrypting to avoid double-encryption. + +--- + +## Minimal Working Example + +Here's the absolute minimum to get a plugin that adds a "Hello" button to the Settings page: + +**shared/YourServletInterface.java** - One GET endpoint returning a string. + +**server/YourServerPlugin.java** - `ServicePlugin` with empty lifecycle methods. + +**server/YourServlet.java** - `MirthServlet` implementing your interface. + +**client/YourSettingsPanelPlugin.java** - `SettingsPanelPlugin` with a panel that calls the endpoint. + +**package/resources/plugin.xml** - Declares classes, JARs, and API provider. + +Build, install the ZIP, restart the server, and your tab appears in Settings. + +From there, add complexity incrementally: database storage, more endpoints, richer UI. + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/maven-poms.md b/AI Skills/oie-plugin-development/references/maven-poms.md new file mode 100644 index 0000000..0dfd559 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/maven-poms.md @@ -0,0 +1,274 @@ +## The Root POM + +The root POM is a parent that defines shared properties, dependency versions, and module order. + +```xml + + + 4.0.0 + + com.yourorg + your-plugin + ${revision} + pom + + Your Plugin Name + + + shared + server + client + package + + + + 1.0.0 + 4.5.2 + UTF-8 + + + + + mirth-libs + https://repo.repsy.io/mvn/kpalang/mirthconnect + + + + + + + + com.mirth.connect + mirth-server + ${mc.version} + provided + + + com.mirth.connect + donkey-server + ${mc.version} + provided + + + com.mirth.connect + mirth-client-core + ${mc.version} + provided + + + com.mirth.connect + mirth-client + ${mc.version} + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + javax.ws.rs + javax.ws.rs-api + 2.0.1 + provided + + + + + org.slf4j + slf4j-api + 1.7.32 + provided + + + + + com.thoughtworks.xstream + xstream + 1.4.18 + provided + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + + + +``` + +### Key points + +- **`${revision}` property**: Use Maven's flat versioning. Set the version once, all modules inherit it. +- **`provided` scope on everything from the engine**: The OIE server already has these JARs. If you bundle them, you get classpath conflicts. +- **Module order matters**: shared first, then server and client (which depend on shared), then package last. + +--- + +## Module POMs + +### shared/pom.xml + +```xml + + + com.yourorg + your-plugin + ${revision} + + + your-plugin-shared + + + + javax.ws.rs + javax.ws.rs-api + + + com.mirth.connect + mirth-client-core + + + com.thoughtworks.xstream + xstream + + + + io.swagger.core.v3 + swagger-annotations + 2.1.9 + provided + + + +``` + +### server/pom.xml + +```xml + + + com.yourorg + your-plugin + ${revision} + + + your-plugin-server + + + + com.yourorg + your-plugin-shared + ${revision} + + + com.mirth.connect + mirth-server + + + com.mirth.connect + donkey-server + + + javax.servlet + javax.servlet-api + + + org.slf4j + slf4j-api + + + +``` + +### client/pom.xml + +```xml + + + com.yourorg + your-plugin + ${revision} + + + your-plugin-client + + + + com.yourorg + your-plugin-shared + ${revision} + + + com.mirth.connect + mirth-client + + + com.mirth.connect + mirth-client-core + + + + com.miglayout + miglayout + 3.7.4 + provided + + + org.swinglabs + swingx-core + 1.6.2-2 + provided + + + com.fasterxml.jackson.core + jackson-databind + 2.14.3 + provided + + + +``` + +--- + + +## Build Commands + +```bash +# Standard build (produces package/target/your-plugin-1.0.0.zip) +mvn clean package + +# Build without tests +mvn clean package -DskipTests + +# Build with code signing +mvn clean package -Psigning -Dsigning.storepass=YOUR_PIN + +# Run tests only +mvn test +``` + +The installable ZIP ends up at `package/target/your-plugin-.zip`. Upload this through the OIE Administrator's Extension Manager. + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/packaging-signing-serialization.md b/AI Skills/oie-plugin-development/references/packaging-signing-serialization.md new file mode 100644 index 0000000..71aea87 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/packaging-signing-serialization.md @@ -0,0 +1,263 @@ +## Packaging and Assembly + +### package/pom.xml + +```xml + + + com.yourorg + your-plugin + ${revision} + + + your-plugin-package + pom + + + + com.yourorg + your-plugin-server + ${revision} + + + com.yourorg + your-plugin-shared + ${revision} + + + com.yourorg + your-plugin-client + ${revision} + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-plugin-xml + process-resources + copy-resources + + + ${project.build.directory}/to-be-packed + + + + resources + true + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-jars + prepare-package + copy + + + + com.yourorg + your-plugin-server + + ${project.build.directory}/to-be-packed + + + + com.yourorg + your-plugin-shared + + ${project.build.directory}/to-be-packed + + + + com.yourorg + your-plugin-client + + ${project.build.directory}/to-be-packed + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + build-zip + package + single + + false + + assembly.xml + + + + + + + + +``` + +### package/assembly.xml + +```xml + + plugin + + zip + + false + + + ${project.build.directory}/to-be-packed + your-plugin + + + your-plugin-*.jar + plugin.xml + + + + + + + + +``` + +**Important**: The `` filter in assembly.xml prevents transitive dependencies from leaking into the ZIP. Without it, Maven will copy every transitive dependency into the staging directory and they'll all end up in your ZIP, causing classpath conflicts. + +--- + + +## XStream Serialization + +If your models travel between server and client via the REST API, you must register them with XStream's type allowlist. Without this, the client will throw `ForbiddenClassException` when deserializing responses. + +```java +import com.mirth.connect.model.converters.ObjectXMLSerializer; + +// Call this in BOTH your server plugin start() AND client plugin start() +public static void registerSerializableTypes() { + ObjectXMLSerializer.getInstance().allowTypes( + YourModel.class, + YourOtherModel.class + ); +} +``` + +This is one of the most common "why does my plugin work on the server but crash the client?" issues. + +--- + +## Code Signing + +### Why you actually need this: the Administrator Launcher + +The **server loads unsigned jars fine** — but the **Administrator Launcher verifies jar signatures** and +refuses an unsigned extension: + +``` +Error verifying entry "META-INF/MANIFEST.MF" in JAR file your-plugin-shared-1.0.0.jar +… has no code signers. +``` + +So a connector/GUI plugin that isn't signed builds and deploys, then fails to open in the Administrator. +Stock OIE signs its own jars with a **self-signed** cert, so self-signed is enough — you just have to tell +the Launcher to accept it. + +### Self-signed dev signing (fast path) + +1. Generate a self-signed keystore — Mirth uses the legacy Java **JKS** keystore format: + ```bash + keytool -genkeypair -keyalg RSA -keysize 2048 -alias selfsigned \ + -keystore certificate/keystore.jks -storepass storepass -keypass storepass \ + -validity 3650 -storetype JKS -dname "CN=Your Plugin (dev self-signed), O=you, C=US" + ``` +2. Build with the `signing` profile pointed at it (see the POM below): + `mvn clean package -Psigning -Dsigning.keystore=certificate/keystore.jks -Dsigning.alias=selfsigned -Dsigning.storepass=storepass` +3. Launch the Administrator **Launcher** with `-k` (`--allow-self-signed`) — on Windows, append ` -k` to the + shortcut's *Target* field; on macOS/Linux, `java -jar mirth-client-launcher.jar -k`. (`-d`, + `--allow-incorrect-digest`, is the sibling flag for digest mismatches.) + +Two config gotchas for the self-signed path: +- **Do not set a ``** (timestamp authority) on a self-signed dev cert. It makes the *client* do slow, + often-failing OCSP/timestamp lookups. Add a `` back only for a real CA-signed release build. +- Add `${signing.storepass}` if your key and store share a password. +- **Don't commit the keystore.** Even a throwaway self-signed keystore holds a private key — committing it is + a credential/supply-chain smell and tends to get copied forward. Generate it locally (or in CI) and + `.gitignore` it; document the one `keytool` command (above) so anyone can regenerate. If you must commit + something for reproducibility, commit only the **public certificate**, not the keystore. + +### The signing POM + +Sign the **collected** jars in the package staging dir, right before the assembly zips them (bind to +`prepare-package`, after `copy-jars`). Add a `signing` profile: + +```xml + + + signing + + + + org.apache.maven.plugins + maven-jarsigner-plugin + 3.0.0 + + + sign-jars + prepare-package + sign + + + ${project.build.directory}/to-be-packed + + + your-plugin-*.jar + + + NONE + PKCS11 + sun.security.pkcs11.SunPKCS11 + yubikey-pkcs11.cfg + ${signing.storepass} + Certificate for PIV Authentication + certchain.pem + http://timestamp.digicert.com + + + + + + + + +``` + +Build with signing: `mvn clean package -Psigning -Dsigning.storepass=YOUR_PIN` + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/plugin-descriptor.md b/AI Skills/oie-plugin-development/references/plugin-descriptor.md new file mode 100644 index 0000000..284d5fb --- /dev/null +++ b/AI Skills/oie-plugin-development/references/plugin-descriptor.md @@ -0,0 +1,62 @@ +## Plugin Descriptor (plugin.xml) + +This is the most important file. It tells the OIE what your plugin provides. Lives in `package/resources/plugin.xml`. + +```xml + + Your Plugin Name + Your Name / Company + ${project.version} + 4.5.2 + https://github.com/yourorg/your-plugin + Brief description of what the plugin does. + + + + com.yourorg.yourplugin.YourServerPlugin + + + + + com.yourorg.yourplugin.YourClientPlugin + + + + com.yourorg.yourplugin.YourMigrator + + + + + all + sqlmap.xml + + + + + + + + + + + + + + + +``` + +### Key points + +- **`path` attribute** on `` becomes the URL path segment: `/extensions/your-plugin/...` +- **`${project.version}`** is substituted by Maven's resource filtering during the build. +- **Library `type`** values: `SERVER` (server classpath only), `CLIENT` (client classpath only), `SHARED` (both). +- If your plugin doesn't need a REST API, omit the `` elements. +- If your plugin doesn't need a database, omit `` and ``. + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/project-structure.md b/AI Skills/oie-plugin-development/references/project-structure.md new file mode 100644 index 0000000..9c7e50a --- /dev/null +++ b/AI Skills/oie-plugin-development/references/project-structure.md @@ -0,0 +1,85 @@ +## Prerequisites + +- **Java 17+** (the OIE server runs on Java 17) +- **Maven 3.8+** +- **OIE / Mirth Connect 4.5.2+** (for testing) +- Access to the Mirth Connect Maven repository for compile-time dependencies + +### Maven Repository + +Add this to your root POM to resolve Mirth dependencies: + +```xml + + + mirth-libs + https://repo.repsy.io/mvn/kpalang/mirthconnect + + +``` + +--- + +## Project Structure + +Every OIE plugin follows a four-module Maven layout: + +``` +your-plugin/ +├── pom.xml # Root parent POM +├── shared/ # Models, DTOs, servlet interface +│ ├── pom.xml +│ └── src/main/java/ +├── server/ # Server-side logic, REST servlet, services +│ ├── pom.xml +│ └── src/main/java/ +├── client/ # Swing UI (runs in the Mirth Administrator) +│ ├── pom.xml +│ └── src/main/java/ +├── package/ # Assembly module — builds the installable ZIP +│ ├── pom.xml +│ ├── assembly.xml +│ └── resources/ +│ └── plugin.xml # OIE plugin descriptor +├── LICENSE +├── README.md +└── .gitignore +``` + +**Why four modules?** + +- **shared** is on both the server and client classpath. Put your DTOs, servlet interface, and constants here. +- **server** runs inside the OIE server JVM. It has access to controllers, the database, and the full engine API. +- **client** runs inside the Mirth Administrator (a Swing application launched via Java Web Start or the bundled launcher). It can only talk to the server through the REST API. +- **package** doesn't contain code. It copies JARs, signs them (optionally), and builds the final ZIP. + +--- + +## Scaffolding a New Plugin + +### 1. Create the directory structure + +```bash +mkdir -p your-plugin/{shared,server,client,package/resources}/src/main/java/com/yourorg/yourplugin +mkdir -p your-plugin/{shared,server,client}/src/test/java/com/yourorg/yourplugin +mkdir -p your-plugin/package/resources +``` + +### 2. Choose your plugin type + +The OIE supports several plugin interfaces. Pick the one that matches your use case: + +| Interface | When to Use | +|-----------|-------------| +| `ServicePlugin` | General-purpose server plugin with lifecycle (start/stop). Good for plugins that need background services or manage their own state. | +| `ChannelPlugin` | Hooks into channel save/deploy/undeploy/remove events. Use when you need to react to channel lifecycle changes. | +| `CodeTemplateServerPlugin` | Hooks into code template save/remove events. | +| `SettingsPanelPlugin` (client) | Adds a tab to the Settings section of the Administrator. | +| `ClientPlugin` (client) | Adds actions to the channel list or channel editor task panes. | + +You can combine multiple interfaces in one plugin. For example, simple-channel-history uses both `ChannelPlugin` and `CodeTemplateServerPlugin` on the server side, and `ClientPlugin` plus `SettingsPanelPlugin` on the client side. + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._ diff --git a/AI Skills/oie-plugin-development/references/server-plugin.md b/AI Skills/oie-plugin-development/references/server-plugin.md new file mode 100644 index 0000000..af2b191 --- /dev/null +++ b/AI Skills/oie-plugin-development/references/server-plugin.md @@ -0,0 +1,217 @@ +## Server-Side Plugin + +### ServicePlugin (general-purpose) + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.plugins.ServicePlugin; +import java.util.Properties; + +public class YourServerPlugin implements ServicePlugin { + + private static final String PLUGIN_NAME = "Your Plugin Name"; + + @Override + public String getPluginPointName() { + return PLUGIN_NAME; + } + + @Override + public void init(Properties properties) { + // Called once during server initialization. + // Properties come from plugin settings (if any). + } + + @Override + public void start() { + // Called when the plugin starts. + // Initialize resources, register caches, start services. + } + + @Override + public void stop() { + // Called when the plugin stops or the server shuts down. + // Close connections, release resources. + } + + @Override + public void update(Properties properties) { + // Called when plugin properties change at runtime. + } + + @Override + public Properties getDefaultProperties() { + return new Properties(); + } + + @Override + public ExtensionPermission[] getExtensionPermissions() { + return new ExtensionPermission[0]; + } +} +``` + +### ChannelPlugin (react to channel events) + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.model.Channel; +import com.mirth.connect.plugins.ChannelPlugin; +import com.mirth.connect.server.controllers.ControllerFactory; + +public class YourChannelPlugin implements ChannelPlugin { + + @Override + public String getPluginPointName() { + return "Your Channel Plugin"; + } + + @Override + public void save(Channel channel, ServerEventContext context) { + // Called AFTER a channel is saved. + } + + @Override + public void remove(Channel channel, ServerEventContext context) { + // Called AFTER a channel is deleted. + // WARNING: The channel object may only have its ID populated. + // Look up the full object if you need other fields. + } + + @Override + public void deploy() { } + + @Override + public void undeploy() { } + + @Override + public void start() { } + + @Override + public void stop() { } +} +``` + +--- + + +## REST API (Servlet Pattern) + +### Servlet interface (in shared module) + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.client.core.api.MirthOperation; +import com.mirth.connect.client.core.api.Param; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.List; + +@Path("/extensions/your-plugin") +@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) +public interface YourServletInterface { + + @GET + @Path("/items") + @MirthOperation( + name = "getItems", + display = "Get items", + permission = Permissions.SERVER_SETTINGS_VIEW, + auditable = false + ) + List getItems() throws ClientException; + + @POST + @Path("/items") + @MirthOperation( + name = "createItem", + display = "Create item", + permission = Permissions.SERVER_SETTINGS_EDIT, + auditable = true + ) + YourModel createItem(@Param("item") YourModel item) throws ClientException; +} +``` + +### Servlet implementation (in server module) + +```java +package com.yourorg.yourplugin; + +import com.mirth.connect.server.api.MirthServlet; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +public class YourServlet extends MirthServlet implements YourServletInterface { + + public YourServlet( + @Context HttpServletRequest request, + @Context SecurityContext sc) { + super(request, sc, PLUGIN_NAME); + } + + @Override + public List getItems() throws ClientException { + try { + return repository.getAll(); + } catch (Exception e) { + logger.error("Failed to get items", e); + throw new MirthApiException(e); + } + } +} +``` + +--- + +## Permissions and Authorization + +The OIE has a built-in permission system. Use `@MirthOperation` annotations to declare what permission is required for each endpoint. + +Common permissions: +- `Permissions.SERVER_SETTINGS_VIEW` - Read settings data +- `Permissions.SERVER_SETTINGS_EDIT` - Modify settings data +- `Permissions.CHANNELS_VIEW` - View channel information +- `Permissions.CHANNELS_MANAGE` - Modify channels + +For channel-specific authorization, use the `@CheckAuthorizedChannelId` annotation on servlet methods that accept a channel ID parameter. + +Set `auditable = false` on read-only operations to avoid flooding the event log. + +--- + +## Event Logging + +For mutating operations (create, update, delete, refresh), log to the OIE event system: + +```java +import com.mirth.connect.model.ServerEvent; +import com.mirth.connect.server.controllers.EventController; + +private void logEvent(String action, String itemName, ServerEvent.Outcome outcome) { + var attributes = new LinkedHashMap(); + attributes.put("Plugin", PLUGIN_NAME); + attributes.put("Item", itemName); + attributes.put("Action", action); + + eventController.dispatchEvent(new ServerEvent( + serverId, + PLUGIN_NAME, + ServerEvent.Level.INFORMATION, + outcome, + attributes + )); +} +``` + +--- + +--- +_Source: adapted from the OIE Plugin Development Guide by [@pacmano1](https://github.com/pacmano1)._