001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2026 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.test.app.clients;
011
012import ca.cdr.test.app.clients.common.ISmileTestHttpClient;
013import ca.cdr.test.app.clients.common.HttpClientOwnership;
014import ca.cdr.test.app.clients.common.RequestFactoryUtil;
015import ca.cdr.test.app.clients.common.SmileTestHttpClient;
016import ca.uhn.hl7v2.HL7Exception;
017import ca.uhn.hl7v2.model.Message;
018import ca.uhn.hl7v2.parser.PipeParser;
019import com.google.common.base.Charsets;
020import jakarta.annotation.Nonnull;
021import jakarta.annotation.Nullable;
022import org.apache.commons.lang3.StringUtils;
023import org.apache.commons.lang3.Validate;
024import org.springframework.web.client.RestClient;
025
026import java.util.Base64;
027import java.util.Objects;
028
029import static org.springframework.http.HttpHeaders.AUTHORIZATION;
030import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
031
032/**
033 * A REST client for sending HL7V2 messages to a Smile CDR server.
034 */
035public class HL7V2RestClient implements AutoCloseable {
036        private static final String HL7V2_MEDIA_TYPE = "application/hl7-v2";
037        private final RestClient myRestClient;
038        private final PipeParser myParser;
039
040        /**
041         * The client this one issues on, and whether closing this client should release it.
042         * {@literal null} for a client built from a {@link RestClient} directly, which brought no
043         * connection pool of its own.
044         */
045        private final HttpClientOwnership myOwnership;
046
047        /**
048         * Constructor with a pre-configured RestClient.
049         *
050         * @param theRestClient The RestClient to use
051         */
052        HL7V2RestClient(RestClient theRestClient) {
053                this(theRestClient, null);
054        }
055
056        private HL7V2RestClient(RestClient theRestClient, @Nullable HttpClientOwnership theOwnership) {
057                myRestClient = theRestClient;
058                myParser = new PipeParser();
059                myOwnership = theOwnership;
060        }
061
062        /**
063         * Opens a client over a connection pool of its own.
064         * <p>
065         * The caller owns the pool: open this in a try-with-resources block, or close it from an
066         * {@code @AfterAll} when it is a field. Use
067         * {@link #issuingOn(SmileTestHttpClient, String, String, String)} instead where a client to
068         * issue on already exists, so this client shares its session rather than starting another.
069         * <p>
070         * The client is configured the way every other Smile test client is: generous timeouts and
071         * <b>redirects disabled</b>, so a {@literal 3xx} is returned rather than followed.
072         *
073         * @param theBaseUrl  The base URL of the HL7V2 endpoint, including port if needed.
074         * @param theUsername The username for authentication (optional)
075         * @param thePassword The password for authentication (optional)
076         * @return A new HL7V2RestClient
077         */
078        public static @Nonnull HL7V2RestClient open(
079                        @Nonnull String theBaseUrl, @Nullable String theUsername, @Nullable String thePassword) {
080                return clientOn(HttpClientOwnership.resolve(null), theBaseUrl, theUsername, thePassword);
081        }
082
083        /**
084         * Builds a client that issues its requests on {@code theHttpClient}, sharing that client's
085         * connection pool and cookie store with everything else built over it.
086         * <p>
087         * The pool stays the caller's: {@link #close()} leaves it open, so there is nothing here for the
088         * caller to release. Use {@link #open(String, String, String)} to build a pool of your own.
089         *
090         * @param theHttpClient the client to issue on, which the caller keeps ownership of
091         * @param theBaseUrl  The base URL of the HL7V2 endpoint, including port if needed.
092         * @param theUsername The username for authentication (optional)
093         * @param thePassword The password for authentication (optional)
094         * @return A new HL7V2RestClient
095         * @see ca.cdr.test.app.clients.common.RequestFactoryUtil#wrap(ISmileTestHttpClient)
096         */
097        public static @Nonnull HL7V2RestClient issuingOn(
098                        @Nonnull SmileTestHttpClient theHttpClient,
099                        @Nonnull String theBaseUrl,
100                        @Nullable String theUsername,
101                        @Nullable String thePassword) {
102                return clientOn(HttpClientOwnership.resolve(theHttpClient), theBaseUrl, theUsername, thePassword);
103        }
104
105        /**
106         * @deprecated Use {@link #open(String, String, String)}, whose name says that the client owns the
107         *    connection pool it returns and that the caller has to close it.
108         */
109        @Deprecated(since = "2026.11.R01", forRemoval = true)
110        public static @Nonnull HL7V2RestClient build(
111                        @Nonnull String theBaseUrl, @Nullable String theUsername, @Nullable String thePassword) {
112                return open(theBaseUrl, theUsername, thePassword);
113        }
114
115        private static @Nonnull HL7V2RestClient clientOn(
116                        @Nonnull HttpClientOwnership theOwnership,
117                        @Nonnull String theBaseUrl,
118                        @Nullable String theUsername,
119                        @Nullable String thePassword) {
120                RestClient.Builder builder = RestClient.builder()
121                        .baseUrl(theBaseUrl)
122                        .defaultHeader(CONTENT_TYPE, HL7V2_MEDIA_TYPE)
123                        .requestFactory(RequestFactoryUtil.wrap(theOwnership.client()));
124                if (!StringUtils.isBlank(theUsername) && !StringUtils.isBlank(thePassword)) {
125                        builder.defaultHeader(
126                                AUTHORIZATION,
127                                "Basic "
128                                        + Base64.getEncoder()
129                                        .encodeToString((theUsername + ":" + thePassword).getBytes(Charsets.UTF_8)));
130                }
131                return new HL7V2RestClient(builder.build(), theOwnership);
132        }
133
134        /**
135         * Send an HL7V2 message to the server.
136         *
137         * @param theMessage The HL7V2 message to send
138         * @return The response message from the server
139         * @throws HL7Exception If there is an error parsing the response
140         */
141        @Nonnull
142        public Message sendMessage(String theMessage) throws HL7Exception {
143                Validate.notEmpty(theMessage, "Message is required");
144
145                String responseBody = myRestClient
146                        .post()
147                        .body(theMessage)
148                        .retrieve()
149                        .body(String.class);
150
151                return myParser.parse(Objects.requireNonNull(responseBody));
152        }
153
154        /**
155         * Send a pre-parsed HL7V2 message to the server.
156         *
157         * @param theMessage The pre-parsed HL7V2 message to send
158         * @return The response message from the server
159         * @throws HL7Exception If there is an error encoding the message or parsing the response
160         */
161        @Nonnull
162        public Message sendMessage(Message theMessage) throws HL7Exception {
163                Validate.notNull(theMessage, "Message is required");
164                return sendMessage(theMessage.encode());
165        }
166
167        /**
168         * Releases the connection pool this client built for itself. A no-op when the client was built
169         * over one supplied by a caller, which owns its own pool.
170         */
171        @Override
172        public void close() {
173                if (myOwnership != null) {
174                        myOwnership.closeIfOwned();
175                }
176        }
177}