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