diff --git a/.classpath b/.classpath
index e944152..b50339b 100644
--- a/.classpath
+++ b/.classpath
@@ -11,6 +11,7 @@
+
@@ -22,6 +23,7 @@
+
diff --git a/pom.xml b/pom.xml
index 7e97c67..6a40781 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.3.4
+ 3.3.5
@@ -16,9 +16,9 @@
release branch is labeled ..-izgw-core-SNAPSHOT
main branch is labeled ..-izgw-core-RELEASE
-->
- 2.1.3-izgw-core-SNAPSHOT
+ 2.1.6-izgw-core-SNAPSHOT
jar
- IZ Gateway Core 2.0
+ IZ Gateway Core 2.1.6
IZ Gateway Core contains the core code for the IZ Gateway Hub and Transformation services
@@ -35,9 +35,10 @@
${project.artifactId}-${project.version}-${timestamp}
UTF-8
${project.groupId}
-
- ${maven.build.timestamp}
- false
+ 6.3.4
+ 6.2.0
+ 6.2.0
+ 4.1.115.Final
@@ -147,17 +148,17 @@
org.bouncycastle
bc-fips
- 1.0.2.5
+ 2.0.0
org.bouncycastle
bcpkix-fips
- 1.0.7
+ 2.0.7
org.bouncycastle
bctls-fips
- 1.0.19
+ 2.0.19
org.codehaus.janino
@@ -175,12 +176,12 @@
org.springdoc
springdoc-openapi-starter-webflux-ui
- [2.2.0,)
+ 2.6.0
org.springdoc
springdoc-openapi-starter-webmvc-ui
- [2.2.0,)
+ 2.6.0
org.springframework
diff --git a/src/main/java/gov/cdc/izgateway/common/HealthService.java b/src/main/java/gov/cdc/izgateway/common/HealthService.java
index 16b97c0..5bd7780 100644
--- a/src/main/java/gov/cdc/izgateway/common/HealthService.java
+++ b/src/main/java/gov/cdc/izgateway/common/HealthService.java
@@ -37,4 +37,12 @@ public static void setServerName(String serverName) {
public static void setBuildName(String build) {
health.setBuildName(build);
}
+
+ public static void setDatabase(String url) {
+ if (health.getDatabase() == null) {
+ health.setDatabase(url);
+ } else {
+ health.setDatabase(health.getDatabase() + ", " + url);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java b/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java
index 5c2820a..227a503 100644
--- a/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java
+++ b/src/main/java/gov/cdc/izgateway/configuration/AppProperties.java
@@ -21,6 +21,11 @@ public class AppProperties {
@Getter
@Value("${server.mode:prod}")
private String serverMode;
+
+ @Getter
+ @Value("${spring.database:jpa}")
+ private String databaseType;
+
@Getter
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "Background-Scheduler"));
diff --git a/src/main/java/gov/cdc/izgateway/logging/event/Health.java b/src/main/java/gov/cdc/izgateway/logging/event/Health.java
index 123f557..c880e32 100644
--- a/src/main/java/gov/cdc/izgateway/logging/event/Health.java
+++ b/src/main/java/gov/cdc/izgateway/logging/event/Health.java
@@ -85,7 +85,11 @@ public class Health {
@JsonProperty
@Schema(description="Host name as known by the operating system")
private String hostname;
-
+
+ @JsonProperty
+ @Schema(description="The database in use")
+ private String database;
+
public Health() {
started = new Date(ManagementFactory.getRuntimeMXBean().getStartTime());
environment = SystemUtils.getDestTypeAsString();
@@ -113,6 +117,7 @@ private Health(Health that) {
this.requestVolume = that.requestVolume;
this.successVolume = that.successVolume;
this.hostname = that.hostname;
+ this.database = that.database;
}
public Health copy() {
diff --git a/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java b/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java
index 3be36a2..c7856af 100644
--- a/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java
+++ b/src/main/java/gov/cdc/izgateway/logging/event/TransactionData.java
@@ -30,7 +30,6 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
@@ -653,12 +652,13 @@ private String getFirstFieldComponent(String[] parts, int index) {
}
public void setProcessError(String summary, String detail) {
- processErrorSummary = summary;
- processErrorDetail = detail;
+ processErrorSummary = HL7Utils.maskSegments(summary);
+ processErrorDetail = HL7Utils.maskSegments(detail);
hasProcessError = !StringUtils.isAllEmpty(summary, detail);
}
- public void setProcessError(Exception fault) {
+
+ public void setProcessError(Exception fault) {
FaultSupport s = null;
if (fault instanceof UnsupportedOperationFault f) {
setMessageType(MessageType.INVALID_REQUEST);
diff --git a/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java b/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java
index e8f2939..20a2eee 100644
--- a/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java
+++ b/src/main/java/gov/cdc/izgateway/model/ICertificateStatus.java
@@ -5,6 +5,7 @@
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.sql.Timestamp;
+import java.util.Date;
import java.util.ServiceConfigurationError;
import jakarta.xml.bind.DatatypeConverter;
@@ -23,13 +24,13 @@ public interface ICertificateStatus {
void setCertSerialNumber(String certificateSerialNumber);
- Timestamp getLastCheckedTimeStamp();
+ Date getLastCheckedTimeStamp();
- void setLastCheckedTimeStamp(Timestamp lastCheckedTimeStamp);
+ void setLastCheckedTimeStamp(Date lastCheckedTimeStamp);
- Timestamp getNextCheckTimeStamp();
+ Date getNextCheckTimeStamp();
- void setNextCheckTimeStamp(Timestamp nextCheckTimeStamp);
+ void setNextCheckTimeStamp(Date nextCheckTimeStamp);
String getLastCheckStatus();
diff --git a/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java b/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java
index d688300..17d6109 100644
--- a/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java
+++ b/src/main/java/gov/cdc/izgateway/model/IEndpointStatus.java
@@ -25,6 +25,8 @@ public interface IEndpointStatus extends IEndpoint {
int getStatusId();
void setDestId(String destId);
+
+ int getDestTypeId();
void setDestUri(String destUri);
diff --git a/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java b/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java
index 165273d..d3fa67a 100644
--- a/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java
+++ b/src/main/java/gov/cdc/izgateway/model/RetryStrategy.java
@@ -24,7 +24,7 @@ public enum RetryStrategy {
* The IIS is not responsive for some reason. This may be due to networking infrastructure (Internet)
* failures between the IZ Gateway and the IIS, or it may be related to routine or emergency IIS
* maintenance. Check the IIS Status before attempting a retry. Some errors (e.g. DNS not found,
- * expired certificates) will not disappear without human intervention.
+ * expired certificates, or invalid response data) will not disappear without human intervention.
*/
CHECK_IIS_STATUS("Check IIS Status before Retry", HttpStatus.BAD_GATEWAY),
diff --git a/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java b/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java
index 3d9afe8..846c3de 100644
--- a/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java
+++ b/src/main/java/gov/cdc/izgateway/repository/EndpointStatusRepository.java
@@ -12,13 +12,14 @@
public interface EndpointStatusRepository {
public static final String[] INCLUDE_ALL = new String[0];
- List findAll();
+ List extends IEndpointStatus> findAll();
IEndpointStatus findById(String id);
IEndpointStatus saveAndFlush(IEndpointStatus status);
boolean removeById(String id);
- List find(int maxQuarterHours, String[] include);
+ List extends IEndpointStatus> find(int maxQuarterHours, String[] include);
boolean refresh();
+ void resetCircuitBreakers();
IEndpointStatus newEndpointStatus();
IEndpointStatus newEndpointStatus(IDestination dest);
}
diff --git a/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java b/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java
index 77065e5..01f69fe 100644
--- a/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java
+++ b/src/main/java/gov/cdc/izgateway/security/AccessControlValve.java
@@ -122,7 +122,7 @@ public boolean accessAllowed(HttpServletRequest req, HttpServletResponse resp) {
if (Boolean.FALSE.equals(check)) { // NOSONAR Null is still possible here, SONAR flags it as always true
log.error("Access denied to protected URL {} address by {} at {}", path, user, host);
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- return true;
+ return false;
}
if (isSwagger(path, user)) {
diff --git a/src/main/java/gov/cdc/izgateway/security/AccessController.java b/src/main/java/gov/cdc/izgateway/security/AccessController.java
index 52d5dfa..cefe9b9 100644
--- a/src/main/java/gov/cdc/izgateway/security/AccessController.java
+++ b/src/main/java/gov/cdc/izgateway/security/AccessController.java
@@ -91,15 +91,13 @@ public Set getUsersInGroup(@PathVariable String group) {
@Operation(summary="Add a user to blacklist", description="Add the specified user to the blacklist")
@PostMapping("/access/blacklist")
- public IAccessControl addUserToBlackList(@RequestParam String user
- ) {
+ public IAccessControl addUserToBlackList(@RequestParam String user) {
return service.addUserToBlacklist(user);
}
@Operation(summary="Delete a user from the blacklist", description="Delete the specified user from the blacklist")
@DeleteMapping("/access/blacklist")
- public IAccessControl removeUserFromBlackList(@RequestParam String user
- ) {
+ public IAccessControl removeUserFromBlackList(@RequestParam String user) {
return service.removeUserFromBlacklist(user);
}
diff --git a/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java b/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java
index 666fef5..095420e 100644
--- a/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java
+++ b/src/main/java/gov/cdc/izgateway/service/impl/EndpointStatusService.java
@@ -29,10 +29,10 @@ public List getHosts() {
return l;
}
- public List findAll() {
+ public List extends IEndpointStatus> findAll() {
return endpointStatusRepository.find(1, EndpointStatusRepository.INCLUDE_ALL);
}
- public List find(int count, String[] include) {
+ public List extends IEndpointStatus> find(int count, String[] include) {
return endpointStatusRepository.find(count, include);
}
@@ -62,4 +62,8 @@ public boolean refresh() {
public boolean removeById(String id) {
return endpointStatusRepository.removeById(id);
}
+
+ public void resetCircuitBreakers() {
+ endpointStatusRepository.resetCircuitBreakers();
+ }
}
diff --git a/src/main/java/gov/cdc/izgateway/soap/MockMessage.java b/src/main/java/gov/cdc/izgateway/soap/MockMessage.java
index 18280eb..9ff1a7d 100644
--- a/src/main/java/gov/cdc/izgateway/soap/MockMessage.java
+++ b/src/main/java/gov/cdc/izgateway/soap/MockMessage.java
@@ -153,9 +153,14 @@ public enum MockMessage {
HttpStatus.BAD_REQUEST),
TC_24H(MediaType.APPLICATION_XML, MockMessageText.TC_24H_TEXT,
- HttpStatus.BAD_REQUEST), TC_UNKF(MockMessage::simulateFault,
+ HttpStatus.BAD_REQUEST),
+
+ TC_24I(MockMessageText.TC_24I_TEXT),
+
+ TC_UNKF(MockMessage::simulateFault,
MockMessageText.TC_UNKF_TEXT);
+
public static final MockMessage TC_FORCE_TIMEOUT = TC_13C;
private static int retryableRequestCount = 0;
/**
@@ -596,19 +601,21 @@ private MockMessageText() {
+ "soap:Receiver"
+ "Invalid Username, Password or FacilityID"
+ ""
- + "401" + "Security" // Change
- // from
- // required
- // "Security"
- // to
- // "Security
- // Fault"
+ + "401Security"
+ "Invalid Username, Password or FacilityID"
+ ""
+ "";
static final String TC_24H_TEXT = "This is not a SOAP Fault nor is it XML";
+ static final String TC_24I_TEXT = "MSH|^~\\&|IRIS IIS|IRIS||IZG|20220205||RSP^K11^RSP_K11|20210330093013AZQ231|P|2.5.1|||||||||Z32^CDCPHINVS\r"
+ + "MSA|AA|20210330093013AZQ231||0||0^Message Accepted^HL70357" + Character.valueOf((char)0x13)
+ + "QAK|20210330093013AZQ231|NF|Z34^Request Complete Immunization history^CDCPHINVS\r"
+ + "QPD|Z34^Request Immunization History^CDCPHINVS|20210330093013IA231|112258-9^^^IA^MR|"
+ + "JohnsonIZG^JamesIZG^AndrewIZG^^^^L|LeungIZG^SarahIZG^^^^^M|20160414|M|"
+ + "Main Street&&123^^Adel^IA^50003^^L|^PRN^PH^^^555^5551111|Y|1\r";
+
+
static final String TC_UNKF_TEXT = "Unknown Exception";
static final String TC_22B_TEXT = ENVELOPE + "" + ""
diff --git a/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java b/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java
index 05d7c79..8485c12 100644
--- a/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java
+++ b/src/main/java/gov/cdc/izgateway/soap/SoapControllerBase.java
@@ -580,6 +580,7 @@ protected ResponseEntity handleFault(Fault fault) {
logFault(fault);
FaultMessage faultMessage = new FaultMessage(fault, messageNamespace);
faultMessage.updateAction(isHub());
+ logResponseMessage(faultMessage);
return new ResponseEntity<>(faultMessage, HttpStatus.INTERNAL_SERVER_ERROR);
}
diff --git a/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java b/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java
index ae93c4f..a3fba07 100644
--- a/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java
+++ b/src/main/java/gov/cdc/izgateway/soap/fault/HubClientFault.java
@@ -24,6 +24,11 @@
import java.util.List;
import java.util.Map;
+/**
+ * The HubClientFault class is used to report errors when the client reports a SOAP Fault or other connection error.
+ *
+ * @author Audacious Inquiry
+ */
public class HubClientFault extends Fault implements HasDestinationUri {
private static final long serialVersionUID = 1L;
@@ -61,7 +66,7 @@ public class HubClientFault extends Fault implements HasDestinationUri {
new MessageSupport(FAULT_NAME, "227", "Destination Threw Fault", null,
"The destination returned a generic fault. See the fault details.", RetryStrategy.CONTACT_SUPPORT),
new MessageSupport(FAULT_NAME, "228", "Destination Returned Invalid Response", null,
- "The destination returned an invalid response to the message", RetryStrategy.CHECK_IIS_STATUS),
+ "The destination returned an invalid response to the message. Please contact the destination IIS.", RetryStrategy.CHECK_IIS_STATUS),
new MessageSupport(FAULT_NAME, "201", "HTTP Bad Request Error", null,
"The Destination sent a 'Bad Request' HTTP Error code in response to the request. "
@@ -92,9 +97,9 @@ public class HubClientFault extends Fault implements HasDestinationUri {
RetryStrategy.CHECK_IIS_STATUS, Arrays.asList(HttpStatus.INTERNAL_SERVER_ERROR)),
new MessageSupport(FAULT_NAME, "206", "HTTP Gateway Error", null,
"The Destination sent an internal infrastructure HTTP Error code in response to the request. "
- + "This error code usually indicates the destination is offline for maintenance.",
+ + "This error code usually indicates that a destination component is offline for maintenance.",
RetryStrategy.CHECK_IIS_STATUS,
- Arrays.asList(HttpStatus.BAD_GATEWAY, HttpStatus.SERVICE_UNAVAILABLE, HttpStatus.GATEWAY_TIMEOUT)),
+ Arrays.asList(HttpStatus.BAD_GATEWAY, HttpStatus.SERVICE_UNAVAILABLE, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.INTERNAL_SERVER_ERROR)),
new MessageSupport(FAULT_NAME, "207", "HTTP Unknown Error", null,
"The Destination sent an unexpected HTTP Error code. "
+ "This error code do not make sense in the context of the IZ Gateway integration, and may be a result of "
@@ -136,17 +141,40 @@ private HubClientFault(MessageSupport messageSupport, IDestination destination,
this.statusCode = statusCode;
}
- // Client returned something, but it didn't parse, go figure it out
- public static HubClientFault invalidMessage(Throwable rootCause, IDestination dest, int statusCode, InputStream body,
- SoapMessage result) {
+ /**
+ * Client returned something, but it didn't parse, go figure it out
+ * @param rootCause The root cause of the error
+ * @param dest The destination
+ * @param statusCode The status code of the response
+ * @param body The message body
+ * @return The hub client fault
+ */
+ public static HubClientFault invalidMessage(Throwable rootCause, IDestination dest, int statusCode, InputStream body) {
String bodyString = XmlUtils.toString(body);
if (statusCode != 500 && statusCode != 200) {
- return new HubClientFault(getHttpMessageSupport(statusCode), dest, rootCause, statusCode, bodyString, result);
+ return new HubClientFault(getHttpMessageSupport(statusCode), dest, rootCause, statusCode, bodyString, null);
+ }
+ if (statusCode == 200) {
+ // These are dangerous. The client thought it had successfully shipped something, but it wasn't valid.
+ // DO NOT REPORT the original response as it may have a corrupted HL7 message containing PHI.
+ bodyString = null;
+ }
+ while (rootCause.getCause() != null) {
+ rootCause = rootCause.getCause();
}
- return new HubClientFault(MESSAGE_TEMPLATES[8], dest, rootCause, statusCode, bodyString, result);
+ return new HubClientFault(MESSAGE_TEMPLATES[8].setDetail(rootCause.getMessage()), dest, rootCause, statusCode, bodyString, null);
}
// Client returns 500 (or 400) with a fault message
+ /**
+ * Construct a HubClientFault
+ * @param rootCause The root cause of the fault
+ * @param dest The destination throwing the fault
+ * @param statusCode The status code returned
+ * @param body The body of the fault content
+ * @param result The resulting soap message
+ * @return The hub client fault
+ */
public static HubClientFault clientThrewFault(Throwable rootCause, IDestination dest, int statusCode,
InputStream body, SoapMessage result) {
String bodyString = XmlUtils.toString(body);
@@ -167,17 +195,33 @@ public static HubClientFault clientThrewFault(Throwable rootCause, IDestination
+ ""
+ "";
+ /**
+ * Construct a developer selected fault for error response testing
+ * @param dest The destination throwing the fault
+ * @return The Hub Client Fault
+ */
public static HubClientFault devAction(IDestination dest) {
return clientThrewFault(UnsupportedOperationFault.devAction(), dest, 420,
new ByteArrayInputStream(FAULT_XML.getBytes(StandardCharsets.UTF_8)),
new SoapMessage());
}
- // Client returned an Http status code without a valid fault message.
+ /**
+ * Construct a HubClientFault when the Client returns an Http status code without a valid fault message.
+ * @param dest The destination
+ * @param statusCode The status code
+ * @param error The error message
+ * @return The hub client fault
+ */
public static HubClientFault httpError(IDestination dest, int statusCode, String error) {
return new HubClientFault(getHttpMessageSupport(statusCode), dest, null, statusCode, error, null);
}
+ /**
+ * Create a generic simulated fault
+ * @param dest The destination
+ * @return The simulated fault
+ */
public static HubClientFault devHttpAction(IDestination dest) {
return httpError(dest, 420, "This is a simulated fault");
}
diff --git a/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java b/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java
index bc986ec..17da14a 100644
--- a/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java
+++ b/src/main/java/gov/cdc/izgateway/soap/message/SoapMessage.java
@@ -243,10 +243,10 @@ public String findTestCaseIdentifier() {
*/
public int length() {
if (this instanceof HasEchoBack echo) {
- return echo.getEchoBack().length();
+ return echo.getEchoBack() == null ? 0 : echo.getEchoBack().length();
}
if (this instanceof HasHL7Message hl7) {
- return hl7.getHl7Message().length();
+ return hl7.getHl7Message() == null ? 0 : hl7.getHl7Message().length();
}
return 0;
}
diff --git a/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java b/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java
index 82898a7..991ffae 100644
--- a/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java
+++ b/src/main/java/gov/cdc/izgateway/soap/net/MessageSender.java
@@ -59,6 +59,7 @@
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.xml.stream.XMLStreamException;
/**
* MessageSender is responsible for sending messages to a given destination,
@@ -183,7 +184,7 @@ public SubmitSingleMessageResponse sendSubmitSingleMessage(
private void checkRetries(IDestination dest, IEndpointStatus status,
int retryCount, Fault f) throws Fault {
- if (f.getRetry() != RetryStrategy.CHECK_IIS_STATUS) {
+ if (f.getRetry() != RetryStrategy.CHECK_IIS_STATUS || f.getCause() instanceof XMLStreamException) {
// This is not a retry-able failure.
RequestContext.getTransactionData().setRetries(retryCount);
throw f;
@@ -377,7 +378,6 @@ private HttpURLConnection setupConnection(
private T readResult(Class clazz, IDestination dest, HttpURLConnection con, long started)
throws Fault {
- SoapMessage result = null;
int statusCode = -1;
try {
statusCode = con.getResponseCode();
@@ -394,6 +394,7 @@ private T readResult(Class clazz, IDestination dest,
InputStream body = null;
Exception savedEx;
try {
+ SoapMessage result = null;
// Mark the buffer so we can reread on error.
m = new HttpUrlConnectionInputMessage(con, clientConfig.getMaxBufferSize());
statusCode = m.getStatusCode();
@@ -425,7 +426,8 @@ private T readResult(Class clazz, IDestination dest,
if (m != null) {
m.reset();
}
- throw HubClientFault.invalidMessage(savedEx, dest, statusCode, body, result);
+ // There can be no result here.
+ throw HubClientFault.invalidMessage(savedEx, dest, statusCode, body);
}
private HubClientFault processHttpError(IDestination dest, int statusCode, InputStream err) {
diff --git a/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java b/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java
index bb9535b..f5d7928 100644
--- a/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java
+++ b/src/main/java/gov/cdc/izgateway/soap/net/SoapMessageReader.java
@@ -89,14 +89,35 @@ public class SoapMessageReader {
/** You can set a writer to copy events to on the reader. */
private XMLStreamWriter writer;
+ /**
+ * Wraps exceptions found during parsing
+ *
+ * @author Audacious Inquiry
+ */
public static class SoapParseException extends XMLStreamException {
private static final long serialVersionUID = 1L;
@Getter
private final String exchange;
- SoapParseException(String msg, Location location, Throwable th, String exchange) {
- super(msg, location == null ? XMLStreamLocation2.NOT_AVAILABLE : location, th);
+ SoapParseException(Throwable th, String exchange) {
+ super(getMessage(th),
+ th instanceof XMLStreamException xex ? xex.getLocation() : XMLStreamLocation2.NOT_AVAILABLE, th);
this.exchange = exchange;
}
+ /**
+ * Remove extra text in the produced message.
+ * @param th The original exception.
+ * @return Just the message without location and Message: prefix
+ */
+ private static String getMessage(Throwable th) {
+ if (th instanceof XMLStreamException) {
+ String msg = StringUtils.substringAfter(th.getMessage(), "Message: ");
+ if (StringUtils.isBlank(msg)) {
+ msg = th.getMessage();
+ }
+ return msg;
+ }
+ return th.getMessage();
+ }
}
/**
* Read a soap message
@@ -172,15 +193,15 @@ public SoapMessage read() throws SecurityFault, XMLStreamException {
}
} catch (XMLStreamException ex) { // NOSONAR Exception handling is OK
log.error(Markers2.append(ex), "XMLStreamException parsing SOAP Message");
- throw new SoapParseException(ex.getMessage(), ex.getLocation(), ex, req == null ? null : req.getClass().getSimpleName());
+ throw new SoapParseException(ex, req == null ? null : req.getClass().getSimpleName());
} catch (BadRequestException ex) { // NOSONAR Exception handling is OK
log.error(Markers2.append(ex), "Invalid XML input in SOAP Message");
- throw new SoapParseException(ex.getMessage(), null, ex, req == null ? null : req.getClass().getSimpleName());
+ throw new SoapParseException(ex, req == null ? null : req.getClass().getSimpleName());
} catch (SecurityFault sf) {
throw sf;
} catch (Exception ex) { // NOSONAR Exception handling is OK
log.error(Markers2.append(ex), "Unexpected exception parsing SOAP Message");
- throw new SoapParseException(ex.getMessage(), null, ex, req == null ? null : req.getClass().getSimpleName());
+ throw new SoapParseException(ex, req == null ? null : req.getClass().getSimpleName());
}
req.updateAction(isHub());
return req;
diff --git a/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java b/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java
index 003dc54..4e012c0 100644
--- a/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java
+++ b/src/main/java/gov/cdc/izgateway/utils/HL7Utils.java
@@ -7,6 +7,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.UnaryOperator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
@@ -24,8 +26,25 @@ public class HL7Utils {
static {
DEFAULT_ALLOWED_SEGMENTS.put("MSH", Arrays.asList(1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 21));
DEFAULT_ALLOWED_SEGMENTS.put("MSA", Arrays.asList(1, 2, 3));
+ DEFAULT_ALLOWED_SEGMENTS.put("QAK", Arrays.asList(1, 2, 3));
DEFAULT_ALLOWED_SEGMENTS.put("ERR", Arrays.asList(1, 2, -3, 4));
}
+
+ /**
+ * Remove any potential PHI from the text of the message
+ * This function ensures that HL7 message content potentially containing PHI is removed from the message.
+ * Sadly, some systems report detailed message content in fault responses.
+ *
+ * MSH|^~\\&|WebIZ.1.0.9035.29972|PW0000|AS0000|AS0000|20241116123011.989+0900||RSP^K11^RSP_K11|PW000020241116301198|P|2.5.1|||NE|NE|||||Z33^CDCPHINVS\r
+ * MSA|AE|AS000020241115301090\rERR||MSH^1^6^1^1|999^ApplicationError^HL70357|E|4^Invalid value^HL70533^WEBIZ-AUTH-625^Facility is inactive or suspended^L||,
+ * Next Diagnostic: |Facility is inactive or suspended., Next Message: One or more errors/warnings occured that may effect query results.\r
+ * QAK|755718|AE|Z34^Request Immunization History^CDCPHINVS\r
+ * QPD|Z34^Request Immunization History^CDCPHINVS|755718|84579^^^AS0000^MR~000000002^^^PW0000^SR|SIMPSON^BART^M^^^^L||19990101|M|2000 AS ST^^AENKAN^AS^96960^^P|^NET^X.400^BERSERY-KEMP@ENVISIONTECHNOLOGY.COM~^PRN^PH^^^864^1309701|N\r
+ * ZSA|AF^Application Fail - Message Failed to Execute. Generally this occurs for critical errors, which preve...^ENV0008|1853^^^PW0000^HL7LogIdIncomming~701265^^^PW0000^WebServiceLogIdIncomming~AS000020241115301090^^^AS0000^MessageControlId\r
+ * @param message The message text
+ * @return The masked message text
+ */
+ public static final Pattern SEGMENT_STARTS = Pattern.compile("((^|[^\\w])(\\w{3})\\|[^\\r\\n]*([\\r\\n]|$))");
private HL7Utils() {
// Do nothing
@@ -218,4 +237,30 @@ public String getFirstSubFieldOf(int index) {
return StringUtils.substringBefore(parts[index], "~");
}
}
+
+ /**
+ * Mash PHI in segments
+ * @param message The message that may contain an HL7 Segment containing PHI in it
+ * @return The masked message
+ */
+ public static String maskSegments(String message) {
+ // Identify segment starts in text messages using the case sensitive pattern /[^a-zA-Z0-9][A-Z]{3}\|/
+ // (a non-alphanumeric character, followed by three uppercase alphabetic characters followed by a vertical bar |).
+ // identify segment ends at either \r or \n or next segment start.
+ if (message == null) {
+ return null;
+ }
+ Matcher m = HL7Utils.SEGMENT_STARTS.matcher(message);
+ StringBuffer b = new StringBuffer();
+ for (boolean result = m.find(); result; result = m.find()) {
+ String seg = m.group(3);
+ if (!DEFAULT_ALLOWED_SEGMENTS.containsKey(seg)) {
+ m.appendReplacement(b, "$2$3|...");
+ } else {
+ m.appendReplacement(b, "$0");
+ }
+ }
+ m.appendTail(b);
+ return b.toString();
+ }
}
diff --git a/src/test/java/test/gov/cdc/izgateway/utils/TestHL7Utils.java b/src/test/java/test/gov/cdc/izgateway/utils/TestHL7Utils.java
new file mode 100644
index 0000000..50a464e
--- /dev/null
+++ b/src/test/java/test/gov/cdc/izgateway/utils/TestHL7Utils.java
@@ -0,0 +1,61 @@
+package test.gov.cdc.izgateway.utils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.commons.text.StringEscapeUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import gov.cdc.izgateway.utils.HL7Utils;
+
+class TestHL7Utils {
+
+ @ParameterizedTest
+ @MethodSource
+ void testMaskSegments(String message, String expected) {
+ String actual = HL7Utils.maskSegments(message);
+ assertEquals(expected, actual);
+ }
+
+ public static String[][] testMaskSegments() {
+ return new String[][] {
+ { "MSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "MSH|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { " MSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge\n and some more stuff",
+ " MSH|this is an msh\r followed by some stuff followed by a PID|... and some more stuff"
+ },
+ { "AMSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "AMSH|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { "1MSH|this is an msh\r followed by some stuff followed by a PID|withsomegunge\r",
+ "1MSH|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { "QPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge and \r some more stuff",
+ "QPD|... followed by some stuff followed by a PID|... some more stuff"
+ },
+ { " QPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ " QPD|... followed by some stuff followed by a PID|..."
+ },
+ { "AQPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge \r and some more stuff",
+ "AQPD|this is an msh\r followed by some stuff followed by a PID|... and some more stuff"
+ },
+ { "1QPD|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "1QPD|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { "ERR|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "ERR|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { "MSA|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "MSA|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { "QAK|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "QAK|this is an msh\r followed by some stuff followed by a PID|..."
+ },
+ { "ZA1|this is an msh\r followed by some stuff followed by a PID|withsomegunge",
+ "ZA1|... followed by some stuff followed by a PID|..."
+ }
+ };
+ }
+}