Skip to content

feat: add optional dcm4che5 DICOM backend with runtime selection5 backend#299

Draft
jessesaga wants to merge 4 commits intoOpenIntegrationEngine:mainfrom
SagaHealthcareIT:feature/dcm4che5-backend
Draft

feat: add optional dcm4che5 DICOM backend with runtime selection5 backend#299
jessesaga wants to merge 4 commits intoOpenIntegrationEngine:mainfrom
SagaHealthcareIT:feature/dcm4che5-backend

Conversation

@jessesaga
Copy link
Copy Markdown

I realize this is a large changeset but I'm not sure how this can be done incrementally. I considered splitting it, but the abstraction layer is dead code without a second backend, and the variant-filtered JAR loading only exists to pick between the two. Open to restructuring if there's something that would make review easier.

Why: at Saga IT we've had to work around limitations in the dcm4che2 implementation and we believe the newer dcm4che5 version will have fewer bugs and potentially allow room for performance improvements - not to mention receiving security updates.

Caveat: only tested locally end-to-end, including mTLS DIMSE channels on both backends. This code has not been tested in real world workloads.

Summary

Adds an optional dcm4che5 backend to the DICOM Listener / Sender connector alongside the existing dcm4che2 backend. Selection is per-server via a new dicom.library property in mirth.properties. The default remains dcm4che2 - existing installations see zero behavioral changes on upgrade.

dcm4che2 has not had a release since 2015. dcm4che5 is actively maintained, has better support for modern Java and TLS, and receives security fixes. This PR gives users the option to move forward without forcing the migration.

Backward compatibility

The default codepath is entirely unchanged. For installations that don't opt in:

  • dicom.library, unset, defaults to dcm4che2
  • Channel XML unchanged - no field renames, no new required properties
  • Existing custom DICOMConfiguration classes continue to load (with fallback logic for pre-PR implementations)
  • dcm4che2 library JARs still load and behave identically
  • One minor API change on DICOMUtil: byteArrayToDicomObject() now returns OieDicomObject instead of DicomObject. Duck-typed scripts work unchanged thanks to Object-VR overloads. Scripts doing explicit (DicomObject) casts or instanceof DicomObject checks need .unwrap() - documented in the migration guide.

Approach

  • Version-neutral abstraction in com.mirth.connect.connectors.dimse.dicom: OieDicomObject, OieDicomElement, OieDicomSender, OieDicomReceiver, OieDicomConverter, OieVR. Decouples Mirth's DICOM code from a specific library version.
  • Two implementations: dcm2/ wraps existing MirthDcmSnd/MirthDcmRcv (unchanged in behavior). dcm5/ composes from Device + Connection + ApplicationEntity + service handlers in idiomatic dcm4che5 style.
  • Runtime selection: DicomLibraryFactory reads dicom.library at startup and instantiates the matching backend via reflection. Default is dcm4che2.
  • Variant-filtered JAR loading: Extension library entries gain an optional variant="dicom.library:<value>" attribute. MirthLauncher (server) and WebStartServlet (Administrator) both honor it, so only the active backend's JARs are loaded on the server and shipped to the Administrator.

Testing

  • Unit: 768 tests across the repo, 0 failures. Parity suites for converter, object, element, receiver, sender on both backends.
  • Integration: C-STORE, C-ECHO, storage commitment, TLS, and cross-library round-trip tests under server/test/.../dimse/dicom/integration/.
  • Manual: Two-channel DIMSE forwarding with mutual TLS, verified on both backends via the official Mirth Administrator Launcher.

Migration

Migration guide for users opting into dcm4che5: docs/dcm4che5-migration-guide.md. Covers enable/disable, element-name difference (keyword vs PS3.6), async C-STORE dispatch, TLS cipher configuration, and three UI settings that are no-ops on dcm5 (two bufSize tuning flags; dest, which is in fact a silent no-op on dcm4che2 upstream as well).

Dependencies

Adds dcm4che-core-5.34.3 and dcm4che-net-5.34.3 (MPL-1.1). License entries added to server/docs/thirdparty/THIRD-PARTY-README.txt.

Not in this PR

  • Does not remove dcm4che2 - default unchanged.
  • Does not add dcm4che5-specific features beyond the existing DICOM feature set.

Manual testing approaches taken beyond test cases

  • Fresh upgrade on an existing install (no dicom.library set): existing DIMSE channels deploy and process messages identically to pre-upgrade
  • Set dicom.library = dcm4che5, restart: channels re-deploy cleanly, C-STORE and C-ECHO behavior verified
  • Toggle back to dicom.library = dcm4che2, restart: behavior returns to default
  • Mutual-TLS DIMSE between two channels on each backend
  • Administrator Launcher connects cleanly and loads only the active backend's JARs

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Test Results

  128 files  + 17    248 suites  +34   7m 25s ⏱️ + 1m 45s
  765 tests +111    765 ✅ +111  0 💤 ±0  0 ❌ ±0 
1 530 runs  +222  1 530 ✅ +222  0 💤 ±0  0 ❌ ±0 

Results for commit c9de11f. ± Comparison against base commit 49b5884.

♻️ This comment has been updated with latest results.

jcdlbs added 3 commits April 17, 2026 10:16
Add a dcm4che5 backend for the DICOM connector alongside the existing
dcm4che2 backend. Backend selection is controlled at startup by the new
dicom.library property in mirth.properties ("dcm4che2" default, "dcm4che5"
to opt in). Existing channel configurations work unchanged on either
backend.

Architecture:
- A version-neutral abstraction layer (OieDicomObject, OieDicomElement,
  OieDicomSender, OieDicomReceiver, OieDicomConverter, OieVR,
  DicomLibraryFactory) decouples connector logic from library specifics.
- Dcm2* implementations wrap the existing dcm4che2 MirthDcmSnd/MirthDcmRcv.
- Dcm5* implementations build an equivalent topology from dcm4che5's
  Device/Connection/ApplicationEntity primitives with the same external
  behaviour -- source-map keys, C-STORE/C-ECHO handling, storage
  commitment, transfer-syntax defaults.
- MirthLauncher resolves a new variant="dicom.library:xxx" attribute on
  extension <library> entries so only the configured backend's JARs land
  on the classpath at runtime.

Build:
- server/build.xml splits the DICOM extension into three JARs:
    dicom-server.jar         (version-neutral, always loaded)
    dicom-server-dcm2.jar    (loaded when dicom.library=dcm4che2)
    dicom-server-dcm5.jar    (loaded when dicom.library=dcm4che5)
  The dcm2 JAR bundles the stock dcm4che-tool-dcmrcv/dcmsnd classes via
  zipfileset with duplicate="preserve" so patched classes take priority.
- Adds dcm4che-core-5.34.3.jar and dcm4che-net-5.34.3.jar to
  server/lib/extensions/dimse/ and updates THIRD-PARTY-README.txt with
  MPL 1.1 attribution for the new version.

Backward compatibility:
- DICOMUtil.byteArrayToDicomObject and dicomObjectToByteArray keep their
  public signatures; the return type becomes OieDicomObject, which mirrors
  the dcm4che2 DicomObject surface methods (contains, size, isEmpty,
  getBytes, getDate, getFloat/getDouble with defaults, getStrings, getInts,
  vrOf, nameOf, vm, getNestedDicomObject).
- OieDicomElement.vr() returns an OieVR adapter (code/padding/toString)
  matching dcm4che2 VR's runtime shape so scripts calling elem.vr().code()
  or String(elem.vr()) continue to work.
- Verified by running identical JavaScript transformer scripts on the
  pre-patch baseline and on the patched branch: existing scripts that
  work on dcm4che2 continue to work on both backends unchanged.

Tests:
- Unit and integration coverage for the abstraction layer, both backends,
  cross-library parity, loopback C-STORE, C-ECHO, error handling, storage
  commitment, and TLS with ephemeral self-signed keystores. Integration
  tests use ephemeral ports and localhost only; all complete within a
  couple of seconds.
- Ready-to-import manual test channels and an API regression script under
  server/tests/dicom-channels/ that exercise the full OieDicomObject and
  DICOMUtil surface through a live DICOM pipeline.

Documentation:
- docs/dcm4che5-migration-guide.md covers enabling the backend, behavioural
  differences, TLS configuration, custom DICOMConfiguration migration,
  JAR architecture, verification, and rollback.

Signed-off-by: Jesse Dowell <jesse.dowell@gmail.com>
MirthLauncher already filters extension libraries by their `variant`
attribute (e.g., `dicom.library:dcm4che5`) when building the server
classpath, but WebStartServlet served all CLIENT/SHARED libraries to
the Administrator regardless of variant. That left the Administrator
downloading both dcm4che2 and dcm4che5 library JARs whenever both
were declared — harmless (different package namespaces), but wasteful
and inconsistent with server-side selection.

Mirror MirthLauncher.shouldLoadLibrary in WebStartServlet so the
Administrator receives only the libraries matching the server's
current configuration.

Signed-off-by: Jesse Dowell <jesse.dowell@gmail.com>
Followups from a pre-PR audit of the dcm4che5 backend:

- Add Object-VR overloads on OieDicomObject for putString / putInt /
  putBytes / putFragments, delegating via toString(). Preserves backward
  compatibility for transformer scripts passing a library-specific VR
  constant (e.g., dcm4che2's VR.PN) when the method surface is the
  version-neutral wrapper.

- Simplify OieDicomObject.getInt(int, int) default from a two-step
  get(tag) + getInt(tag) lookup to a single contains(tag) + getInt(tag).

- Upgrade trace->warn for Dcm5 no-op setters (setFileBufferSize,
  setTranscoderBufferSize, setDestination). These paths are only reached
  when the user explicitly set a non-default value, so the warn has zero
  noise on default configs and surfaces an otherwise-silent ignored
  setting. Also documents the long-standing upstream behavior of
  DICOMReceiver 'dest' being ignored on both backends (MirthDcmRcv's
  onCStoreRQ override streams directly to the channel and never consults
  DcmRcv.setDestination).

- Document no-op settings and DICOMUtil API migration in
  docs/dcm4che5-migration-guide.md.

Signed-off-by: Jesse Dowell <jesse.dowell@gmail.com>
@jcdlbs jcdlbs force-pushed the feature/dcm4che5-backend branch from 7b6603c to c9de11f Compare April 17, 2026 16:16
Copy link
Copy Markdown
Contributor

@jonbartels jonbartels left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the copyright headers be updated? If the code comes from Mirth, then they are still Mirths copyright. If they are new files generated by the contributors, then having the contributor copyright would be appropriate.

The headers should also be updated to the SPDX format seen elsewhere in the project (bonus points for adding a linter check for this :D )

What real world tests would be most valuable for testing this code?

@jessesaga
Copy link
Copy Markdown
Author

Good point on the Copyright and SPDX format. I'll update the newly added files with the SPDX format and the Saga IT, LLC copyright.

What's the policy for the existing files? Convert to SPDX and add an additional FileCopyrightText line?

Not sure about the linter but I can take a look :)

In terms of real world tests, I'd say the biggest current gaps are around load testing and interop with other PACS.

I've done small local load tests but nothing like the volume a real world system might see. It's possible that sustained throughput could reveal some issues. (especially with different threading and queueing settings)

I have not yet tested against any real PACS - just Mirth to Mirth communication. It would be good to do some testing against at least Orthanc and dcm4chee-arc.

@mgaffigan
Copy link
Copy Markdown
Contributor

I'll be the first to call out my ignorance regarding dicom, but is there a reason this is all in the same plugin?

Seems like only one version can run at a time, and there's a bunch of proxies to select between the two at runtime (vs loading the legacy dcm4che2 plugin, or loading the alternate dcm4che5 plugin). That presumably introduces a fair bit of debt for go-forward, and change risk for back-compat.

@jessesaga
Copy link
Copy Markdown
Author

The main reason is to provide an easy upgrade path from the ancient unmaintained dcm4che2 libraries and this seemed like a good way to do it. I'm open to other approaches but this way makes the upgrade relatively painless, the abstraction layer provides a little defense against future dcm4che api changes, and the mirth.properties and runtime library resolution keeps only one of the implementations on the server classpath and client classpath at a time.

@mgaffigan
Copy link
Copy Markdown
Contributor

mgaffigan commented Apr 21, 2026

Sure. I think the salient questions would be:

  1. Are any changes in behavior intended for dch4che2?
  2. Can this PR be substantially simplified by avoiding the proxies and runtime selection?
  3. Is it required/realistic to expect new development to operate compatible to dcm4che2/dcm4che5 (with the new wrappers)? Or are the differences practically sufficient to make any code coupled to one version or the other.

The user effect seems neutral: mirth.properties change vs plugin change.

@mgaffigan
Copy link
Copy Markdown
Contributor

From some hacking around by stripping out the wrappers and dcm4che2 and making v2 vs v5 a separate plugin:

  • PR to extract DICOM from core server build (remove dep on dcm4che2 et al)
    +126/-47
  • PR to add separate dcm4che5 plugin without wrappers
    +1,800/-336

Vs this PR which is at +4,616/-399 for implementation.

I call out again: I don't have the DICOM background to evaluate how helpful the wrappers are. My own experience leans towards avoiding wrappers unless they add significant value (since it makes it "OIE DICOM" instead of standard types) - but I can't estimate the value here.

@jessesaga
Copy link
Copy Markdown
Author

@mgaffigan - The primary goal with the current approach is to ensure that it is perfectly backward compatible to make sure it's an uneventful upgrade process for users.

Are any changes in behavior intended for dch4che2?

No, a lot of care was taken to ensure that users that not only just receive and send DICOM can simply upgrade and have things work with dcm4che5. The migration guide in the PR outlines a couple of minor API changes. Most users will be able to switch to dcm4che5 from 2 and have it work without changes. If they are extensively using the mirth DICOM API then it's possible they might have to make very minor adjustments.

To find the relevant section in the guide you can search for this phrase - "Only these specific patterns require a one-line change"

Can this PR be substantially simplified by avoiding the proxies and runtime selection?

Yes, some, but we'd lose Mirth's excellent backward compatibility story. If we just ripped out dcm4che2 and plugged in dcm4che5 the most minimal changes possible many DICOM listener/destination users would be forced to rework their code.

Is it required/realistic to expect new development to operate compatible to dcm4che2/dcm4che5 (with the new wrappers)? Or are the differences practically sufficient to make any code coupled to one version or the other.

There are very minimal changes that may be required depending on how extensively the DCIOM API is being excercised. The main difference is around java types that are passed but each type is actually duck-type compatible and the javascript engine will be able to resolve correctly. Any explicit casts would have to be updated if they're being used.

@mgaffigan
Copy link
Copy Markdown
Contributor

That all still seems compatible with having alternate v2 and v5 plugins (with only one installed at a time). I'd suggest this be done that way to avoid reinventing the wheel and making yet another dicom-specific modification to the main server.

Using actual plugins has the benefit of:

  1. Making the risk to any dcm4che2 users nearly zero
  2. Avoiding new complications to MirthLauncher and WebStartServlet
  3. Avoiding the runtime indirection between v2 and v5 (instead using standard plugin loading)
  4. Using standard plugin add/remove processes to pick between v2 and v5

I defer to your judgement that the wrappers themselves are required to maintain backwards compatibility with user scripts.

I'm happy to talk through that in more detail if you would like. Rough process would be:

  1. Pull DICOM build out of the main server build (I'd propose plugins/dicom/build.xml as a new home).
    • Move the source and libs out of server/
    • Add a separate javac call and move the jar calls
    • Fix the inverted dependency the server has on the DICOM plugin
  2. Add the new wrappers and code from this PR as a new plugin (I'd propose plugins/dicom5/ or similar). The dcm4che2 indirection should be omitted, since those are handled in the separate legacy plugin codebase. Only the implementation and dcm4che5 backcompat shims would be required.
  3. Update installer to update the version found (v2 for v2, v5 for new installs or if v5 is found)

Per PR review feedback from jonbartels: convert all touched files in
this PR to the project's SPDX header style (matching the format already
in use in LoginStatus.java, DelimitedReader.java, EDIReader.java, etc.).

Three patterns applied based on file provenance:

* New files contributed by this PR (45 files, mostly under
  server/src/com/mirth/connect/connectors/dimse/dicom/ and the
  corresponding test tree): a single Saga IT, LLC copyright line.
  Replaces the legacy Mirth Corporation block header that was
  copy-pasted from existing files when these were created.

* Existing Mirth files modified by this PR (15 files, including
  DICOMReceiver, DICOMDispatcher, DICOMUtil, MirthLauncher, etc.):
  Mirth Corporation copyright preserved, Saga IT, LLC added as a
  contributor copyright line.

* Vendored dcm4che2 tool files modified by this PR (MirthDcmRcv.java,
  MirthDcmSnd.java): dcm4che project + Mirth Corporation + Saga IT, LLC.
  Note: the upstream dcm4che2 source carries an MPL-1.1 OR GPL-2.0-only
  OR LGPL-2.1-only tri-license; we use MPL-2.0 here to match the rest
  of the project. Surfacing the actual upstream license is a separate
  concern.

Header format matches existing project examples:
  // SPDX-License-Identifier: MPL-2.0
  // SPDX-FileCopyrightText: <holder>

CRLF line endings preserved on files that originally used CRLF; LF
files unchanged. Per-file diffs are header-only — no source code
changed.

Signed-off-by: Jesse Dowell <jesse.dowell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants