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.SingleValidationMessage;
026import ca.uhn.fhir.validation.ValidationResult;
027import jakarta.annotation.Nonnull;
028
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.List;
033import java.util.regex.Pattern;
034
035import static org.apache.commons.lang3.ObjectUtils.isEmpty;
036import static org.apache.commons.lang3.StringUtils.isNotBlank;
037
038@Interceptor
039public class ValidationMessageSuppressingInterceptor {
040
041        private List<Pattern> mySuppressPatterns = new ArrayList<>();
042
043        /**
044         * Constructor
045         */
046        public ValidationMessageSuppressingInterceptor() {
047                super();
048        }
049
050        /**
051         * Supplies one or more patterns to suppress. Any validation messages (of any severity) will be suppressed
052         * if they match this pattern. Patterns are in Java Regular Expression format (as defined by the {@link Pattern} class)
053         * and are treated as partial maches. They are also case insensitive.
054         * <p>
055         *    For example, a pattern of <code>loinc.*1234</code> would suppress the following message:<br/>
056         *    <code>The LOINC code 1234 is not valid</code>
057         * </p>
058         */
059        public ValidationMessageSuppressingInterceptor addMessageSuppressionPatterns(String... thePatterns) {
060                return addMessageSuppressionPatterns(Arrays.asList(thePatterns));
061        }
062
063        /**
064         * Supplies one or more patterns to suppress. Any validation messages (of any severity) will be suppressed
065         * if they match this pattern. Patterns are in Java Regular Expression format (as defined by the {@link Pattern} class)
066         * and are treated as partial maches. They are also case insensitive.
067         * <p>
068         *    For example, a pattern of <code>loinc.*1234</code> would suppress the following message:<br/>
069         *    <code>The LOINC code 1234 is not valid</code>
070         * </p>
071         */
072        public ValidationMessageSuppressingInterceptor addMessageSuppressionPatterns(List<String> thePatterns) {
073                for (String next : thePatterns) {
074                        if (isNotBlank(next)) {
075                                Pattern pattern = Pattern.compile(next, Pattern.CASE_INSENSITIVE);
076                                mySuppressPatterns.add(pattern);
077                        }
078                }
079                return this;
080        }
081
082        @Hook(Pointcut.VALIDATION_COMPLETED)
083        public ValidationResult handle(ValidationResult theResult) {
084
085                List<SingleValidationMessage> originalMessages = theResult.getMessages();
086                List<SingleValidationMessage> newMessages = new ArrayList<>(originalMessages.size());
087
088                for (SingleValidationMessage message : originalMessages) {
089                        boolean shouldSuppress = false;
090
091                        for (Pattern nextSuppressPattern : mySuppressPatterns) {
092
093                                if (message.hasSliceMessages()) {
094                                        List<String> sliceMessages = message.getSliceMessages();
095                                        List<String> filteredSliceMessages = filterSliceMessages(nextSuppressPattern, sliceMessages);
096                                        message.setSliceMessages(filteredSliceMessages);
097
098                                        if (isEmpty(filteredSliceMessages)) {
099                                                // all slice messages were suppressed, we should suppress the entire SingleValidationMessage
100                                                shouldSuppress = true;
101                                                break;
102                                        }
103                                } else {
104                                        String nextMessage = message.getMessage();
105                                        if (nextSuppressPattern.matcher(nextMessage).find()) {
106                                                shouldSuppress = true;
107                                                break;
108                                        }
109                                }
110                        }
111
112                        if (!shouldSuppress) {
113                                newMessages.add(message);
114                        }
115                }
116
117                theResult.setMessages(newMessages);
118
119                return null; // keep processing
120        }
121
122        private List<String> filterSliceMessages(
123                        @Nonnull Pattern thePattern, @Nonnull List<String> theOriginalSliceMessages) {
124                if (isEmpty(theOriginalSliceMessages)) {
125                        return Collections.emptyList();
126                }
127
128                List<String> filteredSliceMessages = new ArrayList<>();
129                for (String sliceMessage : theOriginalSliceMessages) {
130                        boolean shouldSuppress = thePattern.matcher(sliceMessage).find();
131                        if (!shouldSuppress) {
132                                filteredSliceMessages.add(sliceMessage);
133                        }
134                }
135
136                return filteredSliceMessages;
137        }
138}