> ## Documentation Index
> Fetch the complete documentation index at: https://hmis-docs.derrickmugabwa.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Orthanc stable-study poller

> Convert Orthanc stable-study changes into vendor-neutral study events.

## Purpose

This channel is the demo's Orthanc-specific inbound adapter. It polls Orthanc's changes feed, detects `StableStudy`, retrieves the study metadata, and emits one normalized event to the generic [PACS study events relay](./study-events-relay).

## Source configuration

| Setting       | Value                         |
| ------------- | ----------------------------- |
| Channel name  | `Orthanc Stable Study Poller` |
| Connector     | JavaScript Reader             |
| Poll interval | `5000` ms                     |
| Data type     | Raw                           |

Use this complete reader script:

```javascript theme={null}
function httpGetJson(url) {
    var client = new Packages.org.apache.commons.httpclient.HttpClient();
    client.getHttpConnectionManager().getParams().setConnectionTimeout(10000);
    client.getHttpConnectionManager().getParams().setSoTimeout(10000);

    var method = new Packages.org.apache.commons.httpclient.methods.GetMethod(url);
    method.setRequestHeader('Accept', 'application/json');

    try {
        var status = client.executeMethod(method);
        var body = String(method.getResponseBodyAsString());

        if (status < 200 || status >= 300) {
            throw new Error('Orthanc returned HTTP ' + status + ' for ' + url + ': ' + body);
        }

        return JSON.parse(body);
    } finally {
        method.releaseConnection();
    }
}

function dicomDate(value) {
    var input = String(value || '');
    return input.length === 8
        ? input.substring(0, 4) + '-' + input.substring(4, 6) + '-' + input.substring(6, 8)
        : null;
}

var baseUrlValue = configurationMap.get('orthanc_base_url');
var pacsCodeValue = configurationMap.get('orthanc_pacs_code');
if (baseUrlValue === null || pacsCodeValue === null) {
    throw new Error('Configure orthanc_base_url and orthanc_pacs_code in the OIE Configuration Map.');
}

var baseUrl = String(baseUrlValue).replace(/\/$/, '');
var pacsCode = String(pacsCodeValue);
var lastSequenceValue = globalChannelMap.get('orthanc_last_change_sequence');
var lastSequence = lastSequenceValue === null ? 0 : Number(lastSequenceValue);
var changes = httpGetJson(baseUrl + '/changes?since=' + lastSequence + '&limit=50');
var changeList = changes.Changes || [];

for (var index = 0; index < changeList.length; index++) {
    var change = changeList[index];
    globalChannelMap.put('orthanc_last_change_sequence', String(change.Seq));

    if (String(change.ChangeType) !== 'StableStudy') {
        continue;
    }

    var study = httpGetJson(baseUrl + '/studies/' + String(change.ID));
    var tags = study.MainDicomTags || {};
    var series = httpGetJson(baseUrl + '/studies/' + String(change.ID) + '/series');
    var firstSeriesTags = series.length > 0 ? (series[0].MainDicomTags || {}) : {};
    var accessionNumber = String(tags.AccessionNumber || '');
    var studyInstanceUid = String(tags.StudyInstanceUID || '');

    if (accessionNumber === '' || studyInstanceUid === '') {
        logger.error('Orthanc study ' + change.ID + ' has no accession number or Study Instance UID.');
        continue;
    }

    return JSON.stringify({
        message_id: 'orthanc-' + String(change.ID) + '-change-' + String(change.Seq),
        pacs_code: pacsCode,
        external_study_id: String(change.ID),
        study_instance_uid: studyInstanceUid,
        accession_number: accessionNumber,
        modality: String(firstSeriesTags.Modality || tags.ModalitiesInStudy || 'OT'),
        study_date: dicomDate(tags.StudyDate),
        received_at: new Date().toISOString(),
        metadata: {
            source: 'orthanc-changes',
            orthanc_change_sequence: Number(change.Seq),
            study_description: String(tags.StudyDescription || '')
        }
    });
}

return null;
```

## Destination

Add a JavaScript source transformer:

```javascript theme={null}
channelMap.put('normalized_study_event', String(connectorMessage.getRawData()));
```

Then configure an HTTP Sender:

| Setting      | Value                                 |
| ------------ | ------------------------------------- |
| URL          | `http://127.0.0.1:6671/pacs/studies/` |
| Method       | POST                                  |
| Content type | `application/json`                    |
| Data type    | Text                                  |
| Body         | `${normalized_study_event}`           |

Because this call originates inside the OIE container, `127.0.0.1` reaches the other OIE listener directly.

## Java 17 compatibility

Do not implement the HTTP request with `new java.net.URL(url).openConnection()` in this OIE/JRE combination. Rhino can receive an `IllegalAccessException` for the internal `sun.net.www.protocol.http.HttpURLConnection` class. The tested script uses OIE's bundled Apache Commons HttpClient.

## Cursor and replay behavior

The last Orthanc change sequence is stored in `globalChannelMap`. Redeploying or recreating the channel can reset this cursor and replay older changes. HMIS idempotency makes identical messages safe, but a production PACS adapter should persist its cursor durably and monitor replay volume.
