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                theResult.setMessages(newMessages);
087
088                return null; // keep processing
089        }
090
091        private int logResult(
092                        SingleValidationMessage inputMessage,
093                        int msgIdx,
094                        @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<Rule> firstMatchedDefinitionOpt) {
095                ourLog.atDebug()
096                                .setMessage("input message position: {} - matching result: {} - input messageId: {}")
097                                .addArgument(++msgIdx)
098                                .addArgument(firstMatchedDefinitionOpt
099                                                .map(theRule -> System.lineSeparator() + "   matched rule: " + theRule)
100                                                .orElse("no rule matched"))
101                                .addArgument(inputMessage.getMessageId())
102                                .log();
103                return msgIdx;
104        }
105
106        private Optional<Rule> findFirstMatchedDefinition(SingleValidationMessage theMessage) {
107                return myRules.stream()
108                                .filter(rule -> matchesMessageId(theMessage.getMessageId(), rule))
109                                .filter(rule -> rule.oldSeverities.contains(theMessage.getSeverity()))
110                                .filter(rule -> stringContainsAll(theMessage.getMessage(), rule.diagnosticFragmentsToMatch()))
111                                .findFirst();
112        }
113
114        private boolean matchesMessageId(String theMessageId, Rule theRule) {
115                boolean matched = false;
116                if (theMessageId != null) {
117                        matched = (theRule.messageId() != null && theRule.messageId.equals(theMessageId))
118                                        || (theRule.messagePattern != null
119                                                        && theRule.messagePattern.matcher(theMessageId).matches());
120                }
121
122                ourLog.atTrace()
123                                .setMessage("messageId match result: {} - input messageId: {} - matching rule: {}")
124                                .addArgument(matched)
125                                .addArgument(theMessageId)
126                                .addArgument(theRule)
127                                .log();
128
129                return matched;
130        }
131
132        private boolean stringContainsAll(String theMessage, Collection<String> theMatchingFragments) {
133                return theMatchingFragments.stream().allMatch(theMessage::contains);
134        }
135
136        public record Rule(
137                        String messageId,
138                        Pattern messagePattern,
139                        Collection<ResultSeverityEnum> oldSeverities,
140                        Collection<String> diagnosticFragmentsToMatch,
141                        ResultSeverityEnum newSeverity) {
142
143                public static Rule of(ValidationPostProcessingRuleJson theParamsDefinitionJson) {
144                        return new Rule(
145                                        theParamsDefinitionJson.getMsgId(),
146                                        theParamsDefinitionJson.getMsgIdRegexPattern(),
147                                        theParamsDefinitionJson.getOldSeverities(),
148                                        theParamsDefinitionJson.getMessageFragments(),
149                                        theParamsDefinitionJson.getNewSeverity());
150                }
151        }
152}