001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 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.address;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.api.Hook;
027import ca.uhn.fhir.interceptor.api.Interceptor;
028import ca.uhn.fhir.interceptor.api.Pointcut;
029import ca.uhn.fhir.rest.api.server.RequestDetails;
030import ca.uhn.fhir.rest.server.interceptor.ConfigLoader;
031import ca.uhn.fhir.util.ExtensionUtil;
032import ca.uhn.fhir.util.TerserUtil;
033import org.apache.commons.lang3.Validate;
034import org.hl7.fhir.instance.model.api.IBase;
035import org.hl7.fhir.instance.model.api.IBaseExtension;
036import org.hl7.fhir.instance.model.api.IBaseResource;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import java.util.ArrayList;
041import java.util.List;
042import java.util.Properties;
043import java.util.stream.Collectors;
044
045@Interceptor
046public class AddressValidatingInterceptor {
047
048        private static final Logger ourLog = LoggerFactory.getLogger(AddressValidatingInterceptor.class);
049
050        public static final String ADDRESS_TYPE_NAME = "Address";
051        public static final String PROPERTY_VALIDATOR_CLASS = "validator.class";
052        public static final String PROPERTY_EXTENSION_URL = "extension.url";
053
054        public static final String ADDRESS_VALIDATION_DISABLED_HEADER = "HAPI-Address-Validation-Disabled";
055
056        private IAddressValidator myAddressValidator;
057
058        private Properties myProperties;
059
060        public AddressValidatingInterceptor() {
061                super();
062
063                ourLog.info("Starting AddressValidatingInterceptor {}", this);
064                myProperties = ConfigLoader.loadProperties("classpath:address-validation.properties");
065                start(myProperties);
066        }
067
068        public AddressValidatingInterceptor(Properties theProperties) {
069                super();
070                myProperties = theProperties;
071                start(theProperties);
072        }
073
074        public void start(Properties theProperties) {
075                if (!theProperties.containsKey(PROPERTY_VALIDATOR_CLASS)) {
076                        ourLog.info("Address validator class is not defined. Validation is disabled");
077                        return;
078                }
079
080                String validatorClassName = theProperties.getProperty(PROPERTY_VALIDATOR_CLASS);
081                Validate.notBlank(validatorClassName, "%s property can not be blank", PROPERTY_VALIDATOR_CLASS);
082
083                ourLog.info("Using address validator {}", validatorClassName);
084                try {
085                        Class validatorClass = Class.forName(validatorClassName);
086                        IAddressValidator addressValidator;
087                        try {
088                                addressValidator = (IAddressValidator)
089                                                validatorClass.getDeclaredConstructor(Properties.class).newInstance(theProperties);
090                        } catch (Exception e) {
091                                addressValidator = (IAddressValidator)
092                                                validatorClass.getDeclaredConstructor().newInstance();
093                        }
094                        setAddressValidator(addressValidator);
095                } catch (Exception e) {
096                        throw new RuntimeException(Msg.code(344) + "Unable to create validator", e);
097                }
098        }
099
100        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
101        public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
102                ourLog.debug("Validating address on for create {}, {}", theResource, theRequest);
103                handleRequest(theRequest, theResource);
104        }
105
106        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
107        public void resourcePreUpdate(
108                        RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
109                ourLog.debug("Validating address on for update {}, {}, {}", theOldResource, theNewResource, theRequest);
110                handleRequest(theRequest, theNewResource);
111        }
112
113        protected void handleRequest(RequestDetails theRequest, IBaseResource theResource) {
114                if (getAddressValidator() == null) {
115                        ourLog.debug("Address validator is not provided - validation disabled");
116                        return;
117                }
118
119                if (theRequest == null) {
120                        ourLog.debug("RequestDetails is null - unable to validate address for {}", theResource);
121                        return;
122                }
123
124                if (!theRequest.getHeaders(ADDRESS_VALIDATION_DISABLED_HEADER).isEmpty()) {
125                        ourLog.debug("Address validation is disabled for this request via header");
126                        return;
127                }
128
129                FhirContext ctx = theRequest.getFhirContext();
130                List<IBase> addresses = getAddresses(theResource, ctx).stream()
131                                .filter(this::isValidating)
132                                .collect(Collectors.toList());
133
134                if (!addresses.isEmpty()) {
135                        validateAddresses(theRequest, theResource, addresses);
136                }
137        }
138
139        /**
140         * Validates specified child addresses for the resource
141         *
142         * @return Returns true if all addresses are valid, or false if there is at least one invalid address
143         */
144        protected boolean validateAddresses(
145                        RequestDetails theRequest, IBaseResource theResource, List<IBase> theAddresses) {
146                boolean retVal = true;
147                for (IBase address : theAddresses) {
148                        retVal &= validateAddress(address, theRequest.getFhirContext());
149                }
150                return retVal;
151        }
152
153        private boolean isValidating(IBase theAddress) {
154                IBaseExtension ext = ExtensionUtil.getExtensionByUrl(theAddress, getExtensionUrl());
155                if (ext == null) {
156                        return true;
157                }
158                if (ext.getValue() == null || ext.getValue().isEmpty()) {
159                        return true;
160                }
161                return !"false".equals(ext.getValue().toString());
162        }
163
164        protected boolean validateAddress(IBase theAddress, FhirContext theFhirContext) {
165                ExtensionUtil.clearExtensionsByUrl(theAddress, getExtensionUrl());
166
167                try {
168                        AddressValidationResult validationResult = getAddressValidator().isValid(theAddress, theFhirContext);
169                        ourLog.debug("Validated address {}", validationResult);
170
171                        clearPossibleDuplicatesDueToTerserCloning(theAddress, theFhirContext);
172                        ExtensionUtil.setExtension(
173                                        theFhirContext, theAddress, getExtensionUrl(), "boolean", !validationResult.isValid());
174                        if (validationResult.getValidatedAddress() != null) {
175                                theFhirContext.newTerser().cloneInto(validationResult.getValidatedAddress(), theAddress, true);
176                        } else {
177                                ourLog.info("Validated address is not provided - skipping update on the target address instance");
178                        }
179                        return validationResult.isValid();
180                } catch (Exception ex) {
181                        ourLog.warn("Unable to validate address", ex);
182                        IBaseExtension extension = ExtensionUtil.getOrCreateExtension(theAddress, getExtensionUrl());
183                        IBaseExtension errorValue = ExtensionUtil.getOrCreateExtension(extension, "error");
184                        errorValue.setValue(TerserUtil.newElement(theFhirContext, "string", ex.getMessage()));
185                        return false;
186                }
187        }
188
189        private void clearPossibleDuplicatesDueToTerserCloning(IBase theAddress, FhirContext theFhirContext) {
190                TerserUtil.clearField(theFhirContext, "line", theAddress);
191                ExtensionUtil.clearExtensionsByUrl(theAddress, getExtensionUrl());
192        }
193
194        protected String getExtensionUrl() {
195                if (getProperties().containsKey(PROPERTY_EXTENSION_URL)) {
196                        return getProperties().getProperty(PROPERTY_EXTENSION_URL);
197                } else {
198                        return IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL;
199                }
200        }
201
202        protected List<IBase> getAddresses(IBaseResource theResource, final FhirContext theFhirContext) {
203                RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theResource);
204
205                List<IBase> retVal = new ArrayList<>();
206                for (BaseRuntimeChildDefinition c : definition.getChildren()) {
207                        Class childClass = c.getClass();
208                        List<IBase> allValues = c.getAccessor().getValues(theResource).stream()
209                                        .filter(v -> ADDRESS_TYPE_NAME.equals(v.getClass().getSimpleName()))
210                                        .collect(Collectors.toList());
211
212                        retVal.addAll(allValues);
213                }
214
215                return (List<IBase>) retVal;
216        }
217
218        public IAddressValidator getAddressValidator() {
219                return myAddressValidator;
220        }
221
222        public void setAddressValidator(IAddressValidator theAddressValidator) {
223                this.myAddressValidator = theAddressValidator;
224        }
225
226        public Properties getProperties() {
227                return myProperties;
228        }
229
230        public void setProperties(Properties theProperties) {
231                myProperties = theProperties;
232        }
233}