changeset 11:8d876a4f541b

added sample FHIR server
author Sebastien Jodogne <s.jodogne@gmail.com>
date Sat, 21 Oct 2023 09:53:25 +0200
parents 6b9433432ee0
children 31fc239e3481
files .hgignore .reuse/dep5 Samples/Basic/pom.xml Samples/Dcm4Che/pom.xml Samples/FHIR/NOTES.txt Samples/FHIR/pom.xml Samples/FHIR/src/main/java/EndpointProvider.java Samples/FHIR/src/main/java/FhirConfiguration.java Samples/FHIR/src/main/java/FhirRequestHandler.java Samples/FHIR/src/main/java/IOrthancConnection.java Samples/FHIR/src/main/java/ImagingStudyProvider.java Samples/FHIR/src/main/java/Main.java Samples/FHIR/src/main/java/OrthancPluginConnection.java Samples/FHIR/src/main/java/OrthancResource.java Samples/FHIR/src/main/java/PatientProvider.java Samples/FHIR/src/main/java/Toolbox.java
diffstat 16 files changed, 1208 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Oct 19 12:20:46 2023 +0200
+++ b/.hgignore	Sat Oct 21 09:53:25 2023 +0200
@@ -17,3 +17,6 @@
 Samples/Dcm4Che/.idea/
 Samples/Dcm4Che/OrthancStorage/
 Samples/Dcm4Che/target/
+Samples/FHIR/.idea/
+Samples/FHIR/OrthancStorage/
+Samples/FHIR/target/
--- a/.reuse/dep5	Thu Oct 19 12:20:46 2023 +0200
+++ b/.reuse/dep5	Sat Oct 21 09:53:25 2023 +0200
@@ -3,7 +3,7 @@
 Upstream-Contact: Sebastien Jodogne <s.jodogne@gmail.com>
 Source: https://orthanc.uclouvain.be/
 
-Files: NEWS README Samples/Basic/NOTES.txt Samples/Dcm4Che/NOTES.txt
+Files: NEWS README Samples/Basic/NOTES.txt Samples/Dcm4Che/NOTES.txt Samples/FHIR/NOTES.txt
 Copyright: 2023 Sebastien Jodogne, UCLouvain, Belgium
 License: CC0-1.0
 
--- a/Samples/Basic/pom.xml	Thu Oct 19 12:20:46 2023 +0200
+++ b/Samples/Basic/pom.xml	Sat Oct 21 09:53:25 2023 +0200
@@ -7,7 +7,9 @@
 
 -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <build>
     <plugins>
--- a/Samples/Dcm4Che/pom.xml	Thu Oct 19 12:20:46 2023 +0200
+++ b/Samples/Dcm4Che/pom.xml	Sat Oct 21 09:53:25 2023 +0200
@@ -7,7 +7,9 @@
 
 -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <build>
     <plugins>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/NOTES.txt	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,26 @@
+
+This sample Java plugin runs a FHIR server based on the HAPI library.
+Note that contrarily to the other samples that can run using Java 1.8,
+this Java plugin requires Java 11.
+
+(1) Make sure to build the C++ plugin:
+
+# cd ../../Plugin/
+# mkdir Build
+# cd Build
+# cmake .. -DCMAKE_BUILD_TYPE=Release
+# make -j4
+
+
+(2) Compile the Java plugin using Maven:
+
+# cd ../../Samples/FHIR
+# mvn package
+
+
+(3) Start Orthanc:
+
+On Ubuntu 22.04:
+
+# LD_LIBRARY_PATH=/usr/lib/jvm/java-11-openjdk-amd64/lib/server/ Orthanc ./configuration.json
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/pom.xml	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,100 @@
+<?xml version='1.0' encoding='UTF-8'?>
+
+<!--
+
+    SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+    SPDX-License-Identifier: GPL-3.0-or-later
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>11</source>
+          <target>11</target>
+          <compilerArgument>-Xlint:deprecation</compilerArgument>
+        </configuration>
+      </plugin>
+
+      <!-- https://stackoverflow.com/a/1729094 -->
+      <plugin>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+        </configuration>
+      </plugin>
+
+      <!-- Include the Orthanc Java SDK -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>add-source</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${project.basedir}/../../JavaSDK/be/uclouvain/orthanc</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <groupId>OrthancJava</groupId>
+  <artifactId>OrthancFHIR</artifactId>
+  <version>mainline</version>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>20230618</version>
+    </dependency>
+    <dependency>
+      <groupId>ca.uhn.hapi.fhir</groupId>
+      <artifactId>hapi-fhir-structures-r5</artifactId>
+      <version>6.4.3</version>
+    </dependency>
+    <dependency>
+      <groupId>ca.uhn.hapi.fhir</groupId>
+      <artifactId>hapi-fhir-server</artifactId>
+      <version>6.4.3</version>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <version>5.3.29</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>3.1.0</version>
+    </dependency>
+
+  </dependencies>
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/EndpointProvider.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,84 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import ca.uhn.fhir.rest.annotation.IdParam;
+import ca.uhn.fhir.rest.annotation.Read;
+import ca.uhn.fhir.rest.annotation.Search;
+import ca.uhn.fhir.rest.server.IResourceProvider;
+import org.hl7.fhir.r5.model.Endpoint;
+import org.hl7.fhir.r5.model.IdType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EndpointProvider implements IResourceProvider {
+    public static final String ID = "wado-rs";
+    private IOrthancConnection orthanc;
+    private FhirConfiguration configuration;
+
+    EndpointProvider(IOrthancConnection orthanc) {
+        this.orthanc = orthanc;
+        this.configuration = new FhirConfiguration();
+    }
+
+    private Endpoint createEndpoint() {
+        Endpoint endpoint = new Endpoint();
+
+        // https://build.fhir.org/endpoint-example-wadors.json.html
+
+        endpoint.setAddress(configuration.getDicomWebBaseUrl());
+        endpoint.setId(ID);
+        endpoint.setStatus(Endpoint.EndpointStatus.ACTIVE);
+        endpoint.addConnectionType(Toolbox.createCodeableConcept("http://terminology.hl7.org/CodeSystem/endpoint-connection-type", "dicom-wado-rs"));
+        endpoint.setName("Orthanc DICOMweb server");
+
+        return endpoint;
+    }
+
+    @Override
+    public Class<Endpoint> getResourceType() {
+        return Endpoint.class;
+    }
+
+    @Read()
+    public Endpoint getResourceById(@IdParam IdType theId) {
+        if (theId.getIdPart().equals(ID) &&
+                IOrthancConnection.hasPluginInstalled(orthanc, "dicom-web")) {
+            return createEndpoint();
+        } else {
+            return null;
+        }
+    }
+
+    @Search()
+    public List<Endpoint> getEndpoints() {
+        List<Endpoint> endpoints = new ArrayList<>();
+
+        if (IOrthancConnection.hasPluginInstalled(orthanc, "dicom-web")) {
+            endpoints.add(createEndpoint());
+        }
+
+        return endpoints;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/FhirConfiguration.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,82 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import be.uclouvain.orthanc.Functions;
+
+import org.json.JSONObject;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+public class FhirConfiguration {
+    static private final String KEY_FHIR = "FHIR";
+    static private final String KEY_DICOM_WEB = "DicomWeb";
+    static private final String KEY_ROOT = "Root";
+    static private final String KEY_BASE_URL = "BaseUrl";
+
+    private String serverBaseUrl = "http://localhost:8042/fhir";
+    private String dicomWebRelativeUri = "../dicom-web";
+
+    FhirConfiguration() {
+        JSONObject configuration = new JSONObject(Functions.getConfiguration());
+
+        if (configuration.has(KEY_FHIR)) {
+            JSONObject fhir = configuration.getJSONObject(KEY_FHIR);
+            if (fhir.has(KEY_BASE_URL)) {
+                serverBaseUrl = fhir.getString(KEY_BASE_URL);
+            }
+        }
+
+        if (configuration.has(KEY_DICOM_WEB)) {
+            JSONObject dicomWeb = configuration.getJSONObject(KEY_DICOM_WEB);
+            if (dicomWeb.has(KEY_ROOT)) {
+                dicomWebRelativeUri = "../" + dicomWeb.getString(KEY_ROOT);
+            }
+        }
+
+        if (!serverBaseUrl.endsWith("/")) {
+            serverBaseUrl = serverBaseUrl + "/";
+        }
+    }
+
+    public String getServerBaseUrl() {
+        return serverBaseUrl;
+    }
+
+    public String getDicomWebBaseUrl() {
+        /**
+         * From URL: <scheme>://<authority><path>?<query>#<fragment>
+         */
+
+        try {
+            URL url = new URL(serverBaseUrl);
+            URI uri = new URI(url.getPath());
+            return url.getProtocol() + "://" + url.getAuthority() + uri.resolve(dicomWebRelativeUri);
+        } catch (MalformedURLException | URISyntaxException e) {
+            throw new IllegalArgumentException("Bad URL for DICOMweb: " + serverBaseUrl + dicomWebRelativeUri);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/FhirRequestHandler.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,101 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import ca.uhn.fhir.rest.server.RestfulServer;
+import be.uclouvain.orthanc.Callbacks;
+import be.uclouvain.orthanc.RestOutput;
+import be.uclouvain.orthanc.HttpMethod;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.Map;
+
+public class FhirRequestHandler implements Callbacks.OnRestRequest {
+    private static RestfulServer server;
+
+    FhirRequestHandler(RestfulServer server) {
+        this.server = server;
+    }
+
+    @Override
+    public void call(RestOutput output,
+                     HttpMethod method,
+                     String uri,
+                     String[] regularExpressionGroups,
+                     Map<String, String> headers,
+                     Map<String, String> getParameters, byte[] body) {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+
+        if (regularExpressionGroups.length == 0) {
+            request.setRequestURI("/");
+        } else {
+            request.setRequestURI(regularExpressionGroups[0]);
+        }
+        request.setContent(body);
+
+        switch (method) {
+            case GET:
+                request.setMethod("GET");
+                break;
+            case POST:
+                request.setMethod("POST");
+                break;
+            case PUT:
+                request.setMethod("PUT");
+                break;
+            case DELETE:
+                request.setMethod("DELETE");
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown HTTP method");
+        }
+
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            request.addHeader(entry.getKey(), entry.getValue());
+        }
+
+        for (Map.Entry<String, String> entry : getParameters.entrySet()) {
+            request.setParameter(entry.getKey(), entry.getValue());
+        }
+
+        try {
+            MockHttpServletResponse response = new MockHttpServletResponse();
+            server.service(request, response);
+
+            for (String header : response.getHeaderNames()) {
+                if (header != "Content-Type") {
+                    output.setHttpHeader(header, response.getHeader(header));
+                }
+            }
+
+            output.answerBuffer(response.getContentAsByteArray(), response.getContentType());
+        } catch (IOException e) {
+            output.sendHttpStatusCode((short) 500);
+        } catch (ServletException e) {
+            output.sendHttpStatusCode((short) 500);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/IOrthancConnection.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,87 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public interface IOrthancConnection {
+    public byte[] doGet(String uri,
+                        Map<String, String> parameters);
+
+    public byte[] doPost(String uri,
+                         byte[] body);
+
+    public static String formatGetParameters(Map<String, String> parameters) {
+        String result = "";
+
+        for (Map.Entry<String, String> entry : parameters.entrySet()) {
+            String item;
+            if (entry.getValue().isEmpty()) {
+                item = entry.getKey();
+            } else {
+                item = entry.getKey() + "=" + entry.getValue();
+            }
+
+            if (result.isEmpty()) {
+                result = "?" + item;
+            } else {
+                result = result + "&" + item;
+            }
+        }
+
+        return result;
+    }
+
+    public static JSONObject getJSONObject(IOrthancConnection orthanc,
+                                           String uri,
+                                           Map<String, String> parameters) {
+        byte[] a = orthanc.doGet(uri, parameters);
+        return new JSONObject(new String(a, StandardCharsets.UTF_8));
+    }
+
+    public static JSONArray getJSONArray(IOrthancConnection orthanc,
+                                         String uri,
+                                         Map<String, String> parameters) {
+        byte[] a = orthanc.doGet(uri, parameters);
+        return new JSONArray(new String(a, StandardCharsets.UTF_8));
+    }
+
+    public static boolean hasPluginInstalled(IOrthancConnection orthanc,
+                                             String plugin) {
+        Map<String, String> empty = new HashMap<>();
+        JSONArray plugins = IOrthancConnection.getJSONArray(orthanc, "/plugins", empty);
+
+        for (int i = 0; i < plugins.length(); i++) {
+            if (plugins.getString(i).equals(plugin)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/ImagingStudyProvider.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,94 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import ca.uhn.fhir.rest.annotation.IdParam;
+import ca.uhn.fhir.rest.annotation.OptionalParam;
+import ca.uhn.fhir.rest.annotation.Read;
+import ca.uhn.fhir.rest.annotation.Search;
+import ca.uhn.fhir.rest.param.ReferenceParam;
+import ca.uhn.fhir.rest.param.StringParam;
+import ca.uhn.fhir.rest.server.IResourceProvider;
+import org.hl7.fhir.r5.model.IdType;
+import org.hl7.fhir.r5.model.ImagingStudy;
+import org.hl7.fhir.r5.model.Patient;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ImagingStudyProvider implements IResourceProvider {
+    private IOrthancConnection orthanc;
+
+    ImagingStudyProvider(IOrthancConnection orthanc) {
+        this.orthanc = orthanc;
+    }
+
+    @Override
+    public Class<ImagingStudy> getResourceType() {
+        return ImagingStudy.class;
+    }
+
+    @Read
+    public ImagingStudy getResourceById(@IdParam IdType theId) {
+        Map<String, String> tags = new HashMap<>();
+        tags.put(Toolbox.TAG_STUDY_INSTANCE_UID, theId.getIdPart());
+
+        List<OrthancResource> resources = OrthancResource.find(orthanc, be.uclouvain.orthanc.ResourceType.STUDY, tags, true);
+        if (resources.size() == 0) {
+            return null;
+        } else if (resources.size() > 1) {
+            throw new RuntimeException("Too many matching resources");
+        } else {
+            return resources.get(0).getFhirStudy(orthanc);
+        }
+    }
+
+    @Search()
+    public List<ImagingStudy> getImagingStudies(@OptionalParam(name = ImagingStudy.SP_IDENTIFIER) StringParam theIdentifier,
+                                                @OptionalParam(name = ImagingStudy.SP_SUBJECT) ReferenceParam theSubject) {
+        Map<String, String> tags = new HashMap<>();
+
+        if (theIdentifier != null) {
+            tags.put(Toolbox.TAG_STUDY_INSTANCE_UID, theIdentifier.getValue());
+        }
+
+        if (theSubject != null) {
+            // https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_search.html#search-parameters-resource-reference
+            tags.put(Toolbox.TAG_PATIENT_ID, theSubject.getValue());
+        }
+
+        List<OrthancResource> resources = OrthancResource.find(orthanc, be.uclouvain.orthanc.ResourceType.STUDY, tags, false);
+
+        List<ImagingStudy> studies = new ArrayList<>();
+        for (OrthancResource resource : resources) {
+            ImagingStudy study = resource.getFhirStudy(orthanc);
+            if (study.hasId()) {
+                studies.add(study);
+            }
+        }
+
+        return studies;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/Main.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,74 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import be.uclouvain.orthanc.Callbacks;
+import be.uclouvain.orthanc.Functions;
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
+import ca.uhn.fhir.rest.server.IResourceProvider;
+import ca.uhn.fhir.rest.server.RestfulServer;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.springframework.mock.web.MockServletConfig;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.ServletException;
+
+
+public class Main extends RestfulServer {
+    Main() throws ServletException {
+        setFhirContext(FhirContext.forR5());
+
+        FhirConfiguration configuration = new FhirConfiguration();
+        setServerAddressStrategy(new HardcodedServerAddressStrategy(configuration.getServerBaseUrl()));
+
+        configuration.getDicomWebBaseUrl();
+
+        IOrthancConnection connection = new OrthancPluginConnection();
+
+        List<IResourceProvider> resourceProviders = new ArrayList<IResourceProvider>();
+        resourceProviders.add(new PatientProvider(connection));
+        resourceProviders.add(new ImagingStudyProvider(connection));
+        resourceProviders.add(new EndpointProvider(connection));
+        setResourceProviders(resourceProviders);
+    }
+
+    static {
+        Main main;
+
+        try {
+            main = new Main();
+            main.init(new MockServletConfig());
+        } catch (ServletException e) {
+            throw new RuntimeException("Cannot start the HAPI FHIR server");
+        }
+
+        Callbacks.register("/fhir(/.*)", new FhirRequestHandler(main));
+        Callbacks.register("/fhir", new FhirRequestHandler(main));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/OrthancPluginConnection.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,38 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import java.util.Map;
+
+public class OrthancPluginConnection implements IOrthancConnection {
+
+    @Override
+    public byte[] doGet(String uri, Map<String, String> parameters) {
+        return be.uclouvain.orthanc.Functions.restApiGet(uri + IOrthancConnection.formatGetParameters(parameters));
+    }
+
+    @Override
+    public byte[] doPost(String uri, byte[] body) {
+        return be.uclouvain.orthanc.Functions.restApiPost(uri, body);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/OrthancResource.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,296 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import be.uclouvain.orthanc.ResourceType;
+import org.hl7.fhir.r5.model.*;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OrthancResource {
+    private ResourceType type;
+    private String id;
+    private String lastUpdate;
+    private Map<String, String> tags;
+    private List<String> children;
+
+    public OrthancResource(JSONObject info) {
+        String s = info.getString("Type");
+        if (s.equals("Patient")) {
+            type = ResourceType.PATIENT;
+        } else if (s.equals("Study")) {
+            type = ResourceType.STUDY;
+        } else if (s.equals("Series")) {
+            type = ResourceType.SERIES;
+        } else if (s.equals("Instance")) {
+            type = ResourceType.INSTANCE;
+        } else {
+            throw new RuntimeException("Unknown resource type");
+        }
+
+        id = info.getString("ID");
+        lastUpdate = info.optString("LastUpdate");
+        tags = new HashMap<>();
+        addToDictionary(tags, info.getJSONObject("MainDicomTags"));
+
+        if (type != ResourceType.INSTANCE) {
+            String childKey;
+            switch (type) {
+                case PATIENT:
+                    childKey = "Studies";
+                    break;
+                case STUDY:
+                    childKey = "Series";
+                    addToDictionary(tags, info.getJSONObject("PatientMainDicomTags"));
+                    break;
+                case SERIES:
+                    childKey = "Instances";
+                    break;
+                default:
+                    throw new RuntimeException();
+            }
+
+            children = new ArrayList<>();
+            addToListOfStrings(children, info.getJSONArray(childKey));
+        }
+    }
+
+    static public void addToDictionary(Map<String, String> target,
+                                       JSONObject source) {
+        for (String key : source.keySet()) {
+            target.put(key, source.getString(key));
+        }
+    }
+
+    static public void addToListOfStrings(List<String> target,
+                                          JSONArray source) {
+        for (int i = 0; i < source.length(); i++) {
+            target.add(source.getString(i));
+        }
+    }
+
+    public ResourceType getType() {
+        return type;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getLastUpdate() {
+        return lastUpdate;
+    }
+
+    public Map<String, String> getTags() {
+        return tags;
+    }
+
+    public List<String> getChildren() {
+        if (type == ResourceType.INSTANCE) {
+            throw new RuntimeException("A DICOM instance has no child");
+        } else {
+            return children;
+        }
+    }
+
+    public static List<OrthancResource> find(IOrthancConnection connection,
+                                             ResourceType type,
+                                             Map<String, String> tags,
+                                             boolean caseSensitive) {
+        JSONObject query = new JSONObject();
+        for (Map.Entry<String, String> entry : tags.entrySet()) {
+            query.put(entry.getKey(), entry.getValue());
+        }
+
+        JSONObject request = new JSONObject();
+        request.put("Expand", true);
+        request.put("Query", query);
+        request.put("Short", true);
+        request.put("CaseSensitive", caseSensitive);
+
+        switch (type) {
+            case PATIENT:
+                request.put("Level", "Patient");
+                break;
+            case STUDY:
+                request.put("Level", "Study");
+                break;
+            case SERIES:
+                request.put("Level", "Series");
+                break;
+            case INSTANCE:
+                request.put("Level", "Instance");
+                break;
+            default:
+                throw new RuntimeException();
+        }
+
+        byte[] response = connection.doPost("/tools/find", request.toString().getBytes(StandardCharsets.UTF_8));
+
+        JSONArray arr = new JSONArray(new String(response, StandardCharsets.UTF_8));
+
+        List<OrthancResource> result = new ArrayList<>();
+        for (int i = 0; i < arr.length(); i++) {
+            result.add(new OrthancResource(arr.getJSONObject(i)));
+        }
+
+        return result;
+    }
+
+
+    public Patient getFhirPatient() {
+        if (type != ResourceType.PATIENT) {
+            throw new RuntimeException("Not a patient");
+        }
+
+        Patient patient = new Patient();
+        patient.setId(getTags().getOrDefault(Toolbox.TAG_PATIENT_ID, ""));
+
+        String birthDate = getTags().getOrDefault(Toolbox.TAG_PATIENT_BIRTH_DATE, "");
+        if (birthDate != null) {
+            patient.setBirthDate(Toolbox.parseDicomDate(birthDate));
+        }
+
+        String patientName = getTags().getOrDefault(Toolbox.TAG_PATIENT_NAME, "");
+        if (!patientName.isEmpty()) {
+            patient.addName();
+            HumanName name = patient.getName().get(0);
+
+            String[] parts = patientName.split("\\^");
+
+            // https://dicom.nema.org/medical/dicom/current/output/chtml/part19/sect_10.2.html
+            // https://www.hl7.org/fhir/datatypes.html#HumanName
+            if (parts.length > 0) {
+                name.setFamily(parts[0]);
+            }
+            for (int i = 1; i < parts.length; i++) {
+                name.addGiven(parts[i]);
+            }
+        }
+
+        String sex = getTags().getOrDefault(Toolbox.TAG_PATIENT_SEX, "");
+        if (sex.equals("M")) {
+            patient.setGender(Enumerations.AdministrativeGender.MALE);
+        } else if (sex.equals("F")) {
+            patient.setGender(Enumerations.AdministrativeGender.FEMALE);
+        }
+
+        return patient;
+    }
+
+    public ImagingStudy getFhirStudy(IOrthancConnection orthanc) {
+        if (type != ResourceType.STUDY) {
+            throw new RuntimeException("Not a study");
+        }
+
+        boolean hasDicomWeb = IOrthancConnection.hasPluginInstalled(orthanc, "dicom-web");
+
+        // https://build.fhir.org/imagingstudy-example.json.html
+
+        ImagingStudy study = new ImagingStudy();
+        study.setId(getTags().getOrDefault(Toolbox.TAG_STUDY_INSTANCE_UID, ""));
+        study.setStatus(ImagingStudy.ImagingStudyStatus.AVAILABLE);
+
+        if (hasDicomWeb) {
+            study.addEndpoint(Toolbox.createLocalReference("Endpoint", EndpointProvider.ID));
+        }
+
+        study.setSubject(Toolbox.createLocalReference("Patient", getTags().getOrDefault(Toolbox.TAG_PATIENT_ID, "")));
+
+        String studyDate = getTags().getOrDefault(Toolbox.TAG_STUDY_DATE, "");
+        if (!studyDate.isEmpty()) {
+            study.setStarted(Toolbox.parseDicomDate(studyDate));
+        }
+
+        study.addIdentifier();
+        study.getIdentifier().get(0).setSystem("urn:dicom:uid");
+        study.getIdentifier().get(0).setValue("urn:oid:" + study.getId());
+
+        study.setNumberOfSeries(getChildren().size());
+
+        int countInstances = 0;
+
+        Map<String, String> shortTags = new HashMap<>();
+        shortTags.put("short", "");
+
+        Map<String, String> expand = new HashMap<>();
+        expand.put("expand", "");
+
+        for (int i = 0; i < getChildren().size(); i++) {
+            String seriesUri = "/series/" + getChildren().get(i);
+            OrthancResource orthancSeries = new OrthancResource(IOrthancConnection.getJSONObject(orthanc, seriesUri, shortTags));
+
+            ImagingStudy.ImagingStudySeriesComponent fhirSeries = study.addSeries();
+            fhirSeries.setUid(orthancSeries.getTags().getOrDefault(Toolbox.TAG_SERIES_INSTANCE_UID, ""));
+
+            String modality = orthancSeries.getTags().getOrDefault(Toolbox.TAG_MODALITY, "");
+            if (!modality.isEmpty()) {
+                fhirSeries.setModality(Toolbox.createDicomCodeableConcept(modality));
+            }
+
+            String seriesDescription = orthancSeries.getTags().getOrDefault(Toolbox.TAG_SERIES_DESCRIPTION, "");
+            if (!seriesDescription.isEmpty()) {
+                fhirSeries.setDescription(seriesDescription);
+            }
+
+            String seriesNumber = orthancSeries.getTags().getOrDefault(Toolbox.TAG_SERIES_NUMBER, "");
+            if (!seriesNumber.isEmpty()) {
+                fhirSeries.setNumber(Integer.parseInt(seriesNumber));
+            }
+
+            fhirSeries.setNumberOfInstances(orthancSeries.getChildren().size());
+
+            for (int j = 0; j < orthancSeries.getChildren().size(); j++) {
+                String instanceUri = "/instances/" + orthancSeries.getChildren().get(j);
+                OrthancResource orthancInstance = new OrthancResource(IOrthancConnection.getJSONObject(orthanc, instanceUri, shortTags));
+
+                JSONObject instanceMetadata = IOrthancConnection.getJSONObject(orthanc, instanceUri + "/metadata", expand);
+
+                ImagingStudy.ImagingStudySeriesInstanceComponent fhirInstance = fhirSeries.addInstance();
+                fhirInstance.setUid(orthancInstance.getTags().getOrDefault(Toolbox.TAG_SOP_INSTANCE_UID, ""));
+
+                String instanceNumber = orthancInstance.getTags().getOrDefault(Toolbox.TAG_INSTANCE_NUMBER, "");
+                if (!instanceNumber.isEmpty()) {
+                    fhirInstance.setNumber(Integer.parseInt(instanceNumber));
+                }
+
+                String sopClassUid = instanceMetadata.optString("SopClassUid", "");
+                if (!sopClassUid.isEmpty()) {
+                    fhirInstance.setSopClass(new Coding("urn:ietf:rfc:3986", "urn:oid:" + sopClassUid, ""));
+                }
+            }
+
+            countInstances += orthancSeries.getChildren().size();
+        }
+
+        study.setNumberOfInstances(countInstances);
+
+        return study;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/PatientProvider.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,104 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import be.uclouvain.orthanc.ResourceType;
+import ca.uhn.fhir.rest.annotation.IdParam;
+import ca.uhn.fhir.rest.annotation.OptionalParam;
+import ca.uhn.fhir.rest.annotation.Read;
+import ca.uhn.fhir.rest.annotation.Search;
+import ca.uhn.fhir.rest.param.DateParam;
+import ca.uhn.fhir.rest.param.StringParam;
+import ca.uhn.fhir.rest.server.IResourceProvider;
+import org.hl7.fhir.r5.model.IdType;
+import org.hl7.fhir.r5.model.Patient;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PatientProvider implements IResourceProvider {
+    private IOrthancConnection orthanc;
+
+    PatientProvider(IOrthancConnection orthanc) {
+        this.orthanc = orthanc;
+    }
+
+    @Override
+    public Class<Patient> getResourceType() {
+        return Patient.class;
+    }
+
+    @Read()
+    public Patient getResourceById(@IdParam IdType theId) {
+        Map<String, String> tags = new HashMap<>();
+        tags.put(Toolbox.TAG_PATIENT_ID, theId.getIdPart());
+
+        List<OrthancResource> resources = OrthancResource.find(orthanc, ResourceType.PATIENT, tags, true);
+        if (resources.size() == 0) {
+            return null;
+        } else if (resources.size() > 1) {
+            throw new RuntimeException("Too many matching resources");
+        } else {
+            return resources.get(0).getFhirPatient();
+        }
+    }
+
+    @Search()
+    public List<Patient> getPatients(@OptionalParam(name = Patient.SP_FAMILY) StringParam theFamilyName,
+                                     @OptionalParam(name = Patient.SP_GIVEN) StringParam theGivenName,
+                                     @OptionalParam(name = Patient.SP_IDENTIFIER) StringParam theIdentifier,
+                                     @OptionalParam(name = Patient.SP_BIRTHDATE) DateParam theBirthDate) {
+        Map<String, String> tags = new HashMap<>();
+
+        if (theFamilyName != null &&
+                theGivenName != null) {
+            tags.put(Toolbox.TAG_PATIENT_NAME, "*" + theFamilyName.getValue() + "*" + theGivenName.getValue() + "*");
+        } else if (theFamilyName != null) {
+            tags.put(Toolbox.TAG_PATIENT_NAME, "*" + theFamilyName.getValue() + "*");
+        } else if (theGivenName != null) {
+            tags.put(Toolbox.TAG_PATIENT_NAME, "*" + theGivenName.getValue() + "*");
+        }
+
+        if (theIdentifier != null) {
+            tags.put(Toolbox.TAG_PATIENT_ID, theIdentifier.getValue());
+        }
+
+        if (theBirthDate != null) {
+            tags.put(Toolbox.TAG_PATIENT_BIRTH_DATE, Toolbox.formatDicomDate(theBirthDate.getValue()));
+        }
+
+        List<OrthancResource> resources = OrthancResource.find(orthanc, ResourceType.PATIENT, tags, false);
+
+        List<Patient> patients = new ArrayList<>();
+        for (OrthancResource resource : resources) {
+            Patient patient = resource.getFhirPatient();
+            if (patient.hasId()) {
+                patients.add(patient);
+            }
+        }
+
+        return patients;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/FHIR/src/main/java/Toolbox.java	Sat Oct 21 09:53:25 2023 +0200
@@ -0,0 +1,112 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Java plugin for Orthanc
+ * Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+import org.hl7.fhir.r5.model.CodeableConcept;
+import org.hl7.fhir.r5.model.Reference;
+
+import java.time.LocalDate;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Toolbox {
+    public static String TAG_PATIENT_NAME = "0010,0010";
+    public static String TAG_PATIENT_ID = "0010,0020";
+    public static String TAG_PATIENT_BIRTH_DATE = "0010,0030";
+    public static String TAG_PATIENT_SEX = "0010,0040";
+    public static String TAG_STUDY_INSTANCE_UID = "0020,000d";
+    public static String TAG_STUDY_DATE = "0008,0020";
+    public static String TAG_SERIES_DESCRIPTION = "0008,103e";
+    public static String TAG_SERIES_INSTANCE_UID = "0020,000e";
+    public static String TAG_MODALITY = "0008,0060";
+    public static String TAG_SERIES_NUMBER = "0020,0011";
+    public static String TAG_SOP_INSTANCE_UID = "0008,0018";
+    public static String TAG_INSTANCE_NUMBER = "0020,0013";
+
+    public static Date parseDicomDate(String date) {
+        Pattern pattern = Pattern.compile("^([0-9]{4})([0-9]{2})([0-9]{2})$");
+        Matcher matcher = pattern.matcher(date);
+        if (matcher.matches()) {
+            GregorianCalendar calendar = new GregorianCalendar(
+                    Integer.parseInt(matcher.group(1)),
+                    Integer.parseInt(matcher.group(2)) - 1,  // Month is 1-based in DICOM
+                    Integer.parseInt(matcher.group(3)));
+            return calendar.getTime();
+        } else {
+            throw new IllegalArgumentException("Badly formatted DICOM date: " + date);
+        }
+    }
+
+    public static Date parseFhirDate(String date) {
+        Pattern pattern = Pattern.compile("^([0-9]{4})-([0-9]{2})-([0-9]{2})$");
+        Matcher matcher = pattern.matcher(date);
+        if (matcher.matches()) {
+            Calendar c = Calendar.getInstance();
+            c.set(Integer.parseInt(matcher.group(1)),
+                    Integer.parseInt(matcher.group(2)) - 1,  // Month is 1-based in FHIR
+                    Integer.parseInt(matcher.group(3)));
+            return c.getTime();
+        } else {
+            throw new IllegalArgumentException("Badly formatted FHIR date: " + date);
+        }
+    }
+
+    public static String formatDicomDate(Date date) {
+        Calendar calendar = new GregorianCalendar();
+        calendar.setTime(date);
+        return String.format("%04d%02d%02d", calendar.get(Calendar.YEAR),
+                calendar.get(Calendar.MONTH) + 1,  // Month is 1-based in DICOM
+                calendar.get(Calendar.DAY_OF_MONTH));
+    }
+
+    public static String formatFhirDate(Date date) {
+        Calendar calendar = new GregorianCalendar();
+        calendar.setTime(date);
+        return String.format("%04d-%02d-%02d", calendar.get(Calendar.YEAR),
+                calendar.get(Calendar.MONTH) + 1,  // Month is 1-based in FHIR
+                calendar.get(Calendar.DAY_OF_MONTH));
+    }
+
+    public static CodeableConcept createCodeableConcept(String system,
+                                                        String code) {
+        CodeableConcept codeable = new CodeableConcept();
+        codeable.addCoding();
+        codeable.getCoding().get(0).setSystem(system);
+        codeable.getCoding().get(0).setCode(code);
+        return codeable;
+    }
+
+    public static CodeableConcept createDicomCodeableConcept(String code) {
+        return createCodeableConcept("http://dicom.nema.org/resources/ontology/DCM", code);
+    }
+
+    public static Reference createLocalReference(String type,
+                                                 String id) {
+        Reference reference = new Reference();
+        reference.setReference(type + "/" + id);
+        return reference;
+    }
+}