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}