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 = false;
114                if (theMessageId != null) {
115                        matched = (theRule.messageId() != null && theRule.messageId.equals(theMessageId))
116                                        || (theRule.messagePattern != null
117                                                        && theRule.messagePattern.matcher(theMessageId).matches());
118                }
119
120                ourLog.atTrace()
121                                .setMessage("messageId match result: {} - input messageId: {} - matching rule: {}")
122                                .addArgument(matched)
123                                .addArgument(theMessageId)
124                                .addArgument(theRule)
125                                .log();
126
127                return matched;
128        }
129
130        private boolean stringContainsAll(String theMessage, Collection<String> theMatchingFragments) {
131                return theMatchingFragments.stream().allMatch(theMessage::contains);
132        }
133
134        public record Rule(
135                        String messageId,
136                        Pattern messagePattern,
137                        Collection<ResultSeverityEnum> oldSeverities,
138                        Collection<String> diagnosticFragmentsToMatch,
139                        ResultSeverityEnum newSeverity) {
140
141                public static Rule of(ValidationPostProcessingRuleJson theParamsDefinitionJson) {
142                        return new Rule(
143                                        theParamsDefinitionJson.getMsgId(),
144                                        theParamsDefinitionJson.getMsgIdRegexPattern(),
145                                        theParamsDefinitionJson.getOldSeverities(),
146                                        theParamsDefinitionJson.getMessageFragments(),
147                                        theParamsDefinitionJson.getNewSeverity());
148                }
149        }
150}