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.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}