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}