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