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}