
001/*- 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.interceptor.validation; 021 022import ca.uhn.fhir.interceptor.api.Hook; 023import ca.uhn.fhir.interceptor.api.Interceptor; 024import ca.uhn.fhir.interceptor.api.Pointcut; 025import ca.uhn.fhir.validation.ResultSeverityEnum; 026import ca.uhn.fhir.validation.SingleValidationMessage; 027import ca.uhn.fhir.validation.ValidationResult; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Collection; 034import java.util.List; 035import java.util.Optional; 036import java.util.regex.Pattern; 037 038@Interceptor 039public class ValidationMessagePostProcessingInterceptor { 040 041 private final Logger ourLog = LoggerFactory.getLogger(ValidationMessagePostProcessingInterceptor.class); 042 043 private final List<Rule> myRules = new ArrayList<>(); 044 045 /** 046 * Supplies one or more message definitions to post-process. 047 * Validation messages matching defined 'msgId' or 'msgIdRegex', 'oldSeverity' and (optionally) case-insensitive 048 * 'diagnosticsFragments' matching fragments, will have their severity replaced by the defined 'newSeverity'. 049 * 050 * @param theJsonDefinitions ValidationPostProcessingRuleJson rules 051 */ 052 public ValidationMessagePostProcessingInterceptor addPostProcessingPatterns( 053 ValidationPostProcessingRuleJson... theJsonDefinitions) { 054 return addPostProcessingPatterns(Arrays.asList(theJsonDefinitions)); 055 } 056 057 /** 058 * Supplies one or more message definitions to post-process. 059 * Validation messages matching defined 'msgId' or 'msgIdRegex', 'oldSeverity' and (optionally) case-insensitive 060 * 'diagnosticsFragments' matching fragments, will have their severity replaced by the defined 'newSeverity'. 061 * 062 * @param theJsonDefinitions list of ValidationPostProcessingRuleJson rules 063 */ 064 public ValidationMessagePostProcessingInterceptor addPostProcessingPatterns( 065 List<ValidationPostProcessingRuleJson> theJsonDefinitions) { 066 myRules.addAll(theJsonDefinitions.stream().map(Rule::of).toList()); 067 return this; 068 } 069 070 @Hook(Pointcut.VALIDATION_COMPLETED) 071 public ValidationResult handle(ValidationResult theResult) { 072 List<SingleValidationMessage> newMessages = 073 new ArrayList<>(theResult.getMessages().size()); 074 075 int msgIdx = 0; 076 for (SingleValidationMessage inputMessage : theResult.getMessages()) { 077 Optional<Rule> firstMatchedDefinitionOpt = findFirstMatchedDefinition(inputMessage); 078 msgIdx = logResult(inputMessage, msgIdx, firstMatchedDefinitionOpt); 079 080 firstMatchedDefinitionOpt.ifPresent( 081 theMatchedRule -> inputMessage.setSeverity(theMatchedRule.newSeverity())); 082 083 newMessages.add(inputMessage); 084 } 085 086 return new ValidationResult(theResult.getContext(), newMessages); 087 } 088 089 private int logResult( 090 SingleValidationMessage inputMessage, 091 int msgIdx, 092 @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<Rule> firstMatchedDefinitionOpt) { 093 ourLog.atDebug() 094 .setMessage("input message position: {} - matching result: {} - input messageId: {}") 095 .addArgument(++msgIdx) 096 .addArgument(firstMatchedDefinitionOpt 097 .map(theRule -> System.lineSeparator() + " matched rule: " + theRule) 098 .orElse("no rule matched")) 099 .addArgument(inputMessage.getMessageId()) 100 .log(); 101 return msgIdx; 102 } 103 104 private Optional<Rule> findFirstMatchedDefinition(SingleValidationMessage theMessage) { 105 return myRules.stream() 106 .filter(rule -> matchesMessageId(theMessage.getMessageId(), rule)) 107 .filter(rule -> rule.oldSeverities.contains(theMessage.getSeverity())) 108 .filter(rule -> stringContainsAll(theMessage.getMessage(), rule.diagnosticFragmentsToMatch())) 109 .findFirst(); 110 } 111 112 private boolean matchesMessageId(String theMessageId, Rule theRule) { 113 boolean matched = (theRule.messageId() != null && theRule.messageId.equals(theMessageId)) 114 || (theRule.messagePattern != null 115 && theRule.messagePattern.matcher(theMessageId).matches()); 116 117 ourLog.atTrace() 118 .setMessage("messageId match result: {} - input messageId: {} - matching rule: {}") 119 .addArgument(matched) 120 .addArgument(theMessageId) 121 .addArgument(theRule) 122 .log(); 123 124 return matched; 125 } 126 127 private boolean stringContainsAll(String theMessage, Collection<String> theMatchingFragments) { 128 return theMatchingFragments.stream().allMatch(theMessage::contains); 129 } 130 131 public record Rule( 132 String messageId, 133 Pattern messagePattern, 134 Collection<ResultSeverityEnum> oldSeverities, 135 Collection<String> diagnosticFragmentsToMatch, 136 ResultSeverityEnum newSeverity) { 137 138 public static Rule of(ValidationPostProcessingRuleJson theParamsDefinitionJson) { 139 return new Rule( 140 theParamsDefinitionJson.getMsgId(), 141 theParamsDefinitionJson.getMsgIdRegexPattern(), 142 theParamsDefinitionJson.getOldSeverities(), 143 theParamsDefinitionJson.getMessageFragments(), 144 theParamsDefinitionJson.getNewSeverity()); 145 } 146 } 147}