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}