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