
001/*- 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 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.fields; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.fhirpath.IFhirPath; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.api.Hook; 026import ca.uhn.fhir.interceptor.api.Interceptor; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.rest.api.server.RequestDetails; 029import ca.uhn.fhir.rest.server.interceptor.ConfigLoader; 030import ca.uhn.fhir.util.ExtensionUtil; 031import org.hl7.fhir.instance.model.api.IBase; 032import org.hl7.fhir.instance.model.api.IBaseResource; 033import org.hl7.fhir.instance.model.api.IPrimitiveType; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import java.util.List; 038import java.util.Map; 039 040import static ca.uhn.fhir.rest.server.interceptor.validation.fields.IValidator.VALIDATION_EXTENSION_URL; 041 042@Interceptor 043public class FieldValidatingInterceptor { 044 045 public static final String FHIR_PATH_VALUE = "value"; 046 047 public enum ValidatorType { 048 EMAIL; 049 } 050 051 private static final Logger ourLog = LoggerFactory.getLogger(FieldValidatingInterceptor.class); 052 053 public static final String VALIDATION_DISABLED_HEADER = "HAPI-Field-Validation-Disabled"; 054 public static final String PROPERTY_EXTENSION_URL = "validation.extension.url"; 055 056 private Map<String, String> myConfig; 057 058 public FieldValidatingInterceptor() { 059 super(); 060 061 ourLog.info("Starting FieldValidatingInterceptor {}", this); 062 myConfig = ConfigLoader.loadJson("classpath:field-validation-rules.json", Map.class); 063 } 064 065 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) 066 public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) { 067 ourLog.debug("Validating address on create for resource {} / request {}", theResource, theRequest); 068 handleRequest(theRequest, theResource); 069 } 070 071 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) 072 public void resourcePreUpdate( 073 RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { 074 ourLog.debug( 075 "Validating address on update for resource {} / old resource {} / request {}", 076 theOldResource, 077 theNewResource, 078 theRequest); 079 handleRequest(theRequest, theNewResource); 080 } 081 082 protected void handleRequest(RequestDetails theRequest, IBaseResource theResource) { 083 if (theRequest == null) { 084 ourLog.debug("RequestDetails is null - unable to validate {}", theResource); 085 return; 086 } 087 088 if (!theRequest.getHeaders(VALIDATION_DISABLED_HEADER).isEmpty()) { 089 ourLog.debug("Address validation is disabled for this request via header"); 090 return; 091 } 092 093 FhirContext ctx = theRequest.getFhirContext(); 094 IFhirPath fhirPath = ctx.newFhirPath(); 095 096 for (Map.Entry<String, String> e : myConfig.entrySet()) { 097 IValidator validator = getValidator(e.getValue()); 098 if (validator == null) { 099 continue; 100 } 101 102 List<IBase> fields = fhirPath.evaluate(theResource, e.getKey(), IBase.class); 103 for (IBase field : fields) { 104 105 List<IPrimitiveType> values = fhirPath.evaluate(field, FHIR_PATH_VALUE, IPrimitiveType.class); 106 boolean isValid = true; 107 for (IPrimitiveType value : values) { 108 String valueAsString = value.getValueAsString(); 109 isValid = validator.isValid(valueAsString); 110 ourLog.debug("Field {} at path {} validated {}", value, e.getKey(), isValid); 111 if (!isValid) { 112 break; 113 } 114 } 115 setValidationStatus(ctx, field, isValid); 116 } 117 } 118 } 119 120 private void setValidationStatus(FhirContext ctx, IBase theBase, boolean isValid) { 121 ExtensionUtil.clearExtensionsByUrl(theBase, getValidationExtensionUrl()); 122 ExtensionUtil.setExtension(ctx, theBase, getValidationExtensionUrl(), "boolean", !isValid); 123 } 124 125 private String getValidationExtensionUrl() { 126 if (myConfig.containsKey(PROPERTY_EXTENSION_URL)) { 127 return myConfig.get(PROPERTY_EXTENSION_URL); 128 } 129 return VALIDATION_EXTENSION_URL; 130 } 131 132 private IValidator getValidator(String theValue) { 133 if (PROPERTY_EXTENSION_URL.equals(theValue)) { 134 return null; 135 } 136 137 if (ValidatorType.EMAIL.name().equals(theValue)) { 138 return new EmailValidator(); 139 } 140 141 try { 142 return (IValidator) Class.forName(theValue).getDeclaredConstructor().newInstance(); 143 } catch (Exception e) { 144 throw new IllegalStateException( 145 Msg.code(348) + String.format("Unable to create validator for %s", theValue), e); 146 } 147 } 148 149 public Map<String, String> getConfig() { 150 return myConfig; 151 } 152 153 public void setConfig(Map<String, String> theConfig) { 154 myConfig = theConfig; 155 } 156}