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}